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
crackerjack/errors.py
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
> Crackerjack Docs: [Main](<../../README.md>) | [Crackerjack Package](<../README.md>) | [Events](<./README.md>)
|
|
2
|
+
|
|
3
|
+
# Events
|
|
4
|
+
|
|
5
|
+
Event types, signals, and dispatch-related helpers.
|
|
6
|
+
|
|
7
|
+
## Related
|
|
8
|
+
|
|
9
|
+
- [Crackerjack Package](<../README.md>) - Parent package
|
|
10
|
+
- [Monitoring](<../monitoring/README.md>) - Observability and metrics
|
|
11
|
+
- [Coordinators](../coordinators/README.md) - Event coordination and orchestration
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Event bus utilities for Crackerjack."""
|
|
2
|
+
|
|
3
|
+
from .telemetry import WorkflowEventTelemetry, register_default_subscribers
|
|
4
|
+
from .workflow_bus import (
|
|
5
|
+
WorkflowEvent,
|
|
6
|
+
WorkflowEventBus,
|
|
7
|
+
WorkflowEventDispatchResult,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"WorkflowEvent",
|
|
12
|
+
"WorkflowEventBus",
|
|
13
|
+
"WorkflowEventDispatchResult",
|
|
14
|
+
"WorkflowEventTelemetry",
|
|
15
|
+
"register_default_subscribers",
|
|
16
|
+
]
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Telemetry collection for workflow events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import typing as t
|
|
8
|
+
from collections import Counter, deque
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from acb.events import Event, EventHandlerResult
|
|
16
|
+
|
|
17
|
+
from .workflow_bus import WorkflowEvent
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover - typing helper
|
|
20
|
+
from .workflow_bus import WorkflowEventBus
|
|
21
|
+
|
|
22
|
+
HistoryEntry = dict[str, t.Any]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class WorkflowEventTelemetry:
|
|
27
|
+
"""Collects lightweight metrics about workflow events."""
|
|
28
|
+
|
|
29
|
+
max_history: int = 100
|
|
30
|
+
state_file: Path | None = None
|
|
31
|
+
rollup_interval_seconds: float = 3600.0
|
|
32
|
+
rollup_file: Path | None = None
|
|
33
|
+
_lock: asyncio.Lock = field(default_factory=asyncio.Lock, init=False)
|
|
34
|
+
_counts: Counter[str] = field(default_factory=Counter, init=False)
|
|
35
|
+
_history: deque[HistoryEntry] = field(init=False)
|
|
36
|
+
_last_error: HistoryEntry | None = field(default=None, init=False)
|
|
37
|
+
_persist_task: asyncio.Task[None] | None = field(
|
|
38
|
+
default=None, init=False, repr=False
|
|
39
|
+
)
|
|
40
|
+
_rollup_task: asyncio.Task[None] | None = field(
|
|
41
|
+
default=None, init=False, repr=False
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def __post_init__(self) -> None:
|
|
45
|
+
self._history = deque(maxlen=self.max_history)
|
|
46
|
+
if self.rollup_file is None and self.state_file is not None:
|
|
47
|
+
self.rollup_file = self.state_file.with_name(
|
|
48
|
+
f"{self.state_file.stem}_rollups.jsonl"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async def handle_event(self, event: Event) -> EventHandlerResult:
|
|
52
|
+
"""Event bus subscriber that records event metadata."""
|
|
53
|
+
entry: HistoryEntry = {
|
|
54
|
+
"event_type": event.metadata.event_type,
|
|
55
|
+
"timestamp": event.metadata.timestamp.isoformat(),
|
|
56
|
+
"source": event.metadata.source,
|
|
57
|
+
"payload": event.payload,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async with self._lock:
|
|
61
|
+
self._counts[event.metadata.event_type] += 1
|
|
62
|
+
self._history.append(entry)
|
|
63
|
+
if event.metadata.event_type == WorkflowEvent.WORKFLOW_FAILED.value:
|
|
64
|
+
self._last_error = entry
|
|
65
|
+
|
|
66
|
+
if self.state_file is not None:
|
|
67
|
+
await self._schedule_persist()
|
|
68
|
+
await self._ensure_rollup_task()
|
|
69
|
+
|
|
70
|
+
return EventHandlerResult(success=True)
|
|
71
|
+
|
|
72
|
+
async def snapshot(self) -> dict[str, t.Any]:
|
|
73
|
+
"""Return a snapshot of the telemetry counters and history."""
|
|
74
|
+
async with self._lock:
|
|
75
|
+
return {
|
|
76
|
+
"counts": dict(self._counts),
|
|
77
|
+
"recent_events": list(self._history),
|
|
78
|
+
"last_error": self._last_error,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async def reset(self) -> None:
|
|
82
|
+
"""Reset telemetry counters."""
|
|
83
|
+
async with self._lock:
|
|
84
|
+
self._counts.clear()
|
|
85
|
+
self._history.clear()
|
|
86
|
+
self._last_error = None
|
|
87
|
+
if self.state_file and self.state_file.exists():
|
|
88
|
+
with suppress(Exception):
|
|
89
|
+
self.state_file.unlink()
|
|
90
|
+
|
|
91
|
+
async def shutdown(self) -> None:
|
|
92
|
+
"""Shutdown background persistence tasks."""
|
|
93
|
+
if self._persist_task and not self._persist_task.done():
|
|
94
|
+
self._persist_task.cancel()
|
|
95
|
+
with suppress(asyncio.CancelledError):
|
|
96
|
+
await self._persist_task
|
|
97
|
+
if self._rollup_task and not self._rollup_task.done():
|
|
98
|
+
self._rollup_task.cancel()
|
|
99
|
+
with suppress(asyncio.CancelledError):
|
|
100
|
+
await self._rollup_task
|
|
101
|
+
|
|
102
|
+
async def _schedule_persist(self) -> None:
|
|
103
|
+
if self.state_file is None:
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if self._persist_task and not self._persist_task.done():
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
async def _persist() -> None:
|
|
110
|
+
snapshot = await self.snapshot()
|
|
111
|
+
state_file = self.state_file
|
|
112
|
+
if state_file is None:
|
|
113
|
+
return
|
|
114
|
+
with suppress(Exception):
|
|
115
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
await asyncio.to_thread(
|
|
117
|
+
state_file.write_text,
|
|
118
|
+
json.dumps(snapshot, indent=2),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self._persist_task = asyncio.create_task(_persist())
|
|
122
|
+
|
|
123
|
+
async def _ensure_rollup_task(self) -> None:
|
|
124
|
+
if self.rollup_file is None or self.rollup_interval_seconds <= 0:
|
|
125
|
+
return
|
|
126
|
+
loop = asyncio.get_running_loop()
|
|
127
|
+
if self._rollup_task and not self._rollup_task.done():
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
async def _rollup_loop() -> None:
|
|
131
|
+
try:
|
|
132
|
+
while True:
|
|
133
|
+
await asyncio.sleep(self.rollup_interval_seconds)
|
|
134
|
+
await self._persist_rollup()
|
|
135
|
+
except asyncio.CancelledError: # pragma: no cover - loop cancellation
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
self._rollup_task = loop.create_task(_rollup_loop())
|
|
139
|
+
|
|
140
|
+
async def _persist_rollup(self) -> None:
|
|
141
|
+
if self.rollup_file is None:
|
|
142
|
+
return
|
|
143
|
+
snapshot = await self.snapshot()
|
|
144
|
+
rollup_entry = {
|
|
145
|
+
"timestamp": datetime.now().isoformat(),
|
|
146
|
+
"counts": snapshot["counts"],
|
|
147
|
+
"total_events": sum(snapshot["counts"].values()),
|
|
148
|
+
"last_error": snapshot["last_error"],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
rollup_file = self.rollup_file
|
|
152
|
+
|
|
153
|
+
def _write() -> None:
|
|
154
|
+
if rollup_file is None:
|
|
155
|
+
return
|
|
156
|
+
rollup_file.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
with rollup_file.open("a", encoding="utf-8") as fh:
|
|
158
|
+
fh.write(json.dumps(rollup_entry))
|
|
159
|
+
fh.write("\n")
|
|
160
|
+
|
|
161
|
+
await asyncio.to_thread(_write)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def register_default_subscribers(
|
|
165
|
+
event_bus: WorkflowEventBus,
|
|
166
|
+
telemetry: WorkflowEventTelemetry,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Attach default subscribers to the workflow event bus."""
|
|
169
|
+
event_bus.register_logging_handler()
|
|
170
|
+
event_bus.subscribe(
|
|
171
|
+
event_type=None,
|
|
172
|
+
handler=telemetry.handle_event,
|
|
173
|
+
description="workflow.telemetry",
|
|
174
|
+
max_concurrent=1,
|
|
175
|
+
)
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""Workflow event bus built on top of ACB event primitives.
|
|
2
|
+
|
|
3
|
+
Provides a lightweight in-process event bus that uses ``acb.events`` data
|
|
4
|
+
structures so the rest of the codebase can adopt the forthcoming ACB EventBus
|
|
5
|
+
without waiting on external messaging infrastructure. Subscribers register
|
|
6
|
+
handlers for workflow event types and publishers emit events that are dispatched
|
|
7
|
+
asynchronously.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
import threading
|
|
15
|
+
import typing as t
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from enum import Enum
|
|
18
|
+
|
|
19
|
+
from acb.events import (
|
|
20
|
+
Event,
|
|
21
|
+
EventHandlerResult,
|
|
22
|
+
FunctionalEventHandler,
|
|
23
|
+
create_event,
|
|
24
|
+
)
|
|
25
|
+
from acb.events._base import EventSubscription
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
HandlerCallable = t.Callable[
|
|
30
|
+
[Event], t.Awaitable[EventHandlerResult] | EventHandlerResult
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WorkflowEvent(str, Enum):
|
|
35
|
+
"""Standard workflow events emitted inside Crackerjack."""
|
|
36
|
+
|
|
37
|
+
WORKFLOW_STARTED = "workflow.started"
|
|
38
|
+
WORKFLOW_SESSION_INITIALIZING = "workflow.session.initializing"
|
|
39
|
+
WORKFLOW_SESSION_READY = "workflow.session.ready"
|
|
40
|
+
CONFIG_PHASE_STARTED = "workflow.config.started"
|
|
41
|
+
CONFIG_PHASE_COMPLETED = "workflow.config.completed"
|
|
42
|
+
WORKFLOW_COMPLETED = "workflow.completed"
|
|
43
|
+
WORKFLOW_FAILED = "workflow.failed"
|
|
44
|
+
WORKFLOW_INTERRUPTED = "workflow.interrupted"
|
|
45
|
+
|
|
46
|
+
QUALITY_PHASE_STARTED = "workflow.quality.started"
|
|
47
|
+
QUALITY_PHASE_COMPLETED = "workflow.quality.completed"
|
|
48
|
+
|
|
49
|
+
PUBLISH_PHASE_STARTED = "workflow.publish.started"
|
|
50
|
+
PUBLISH_PHASE_COMPLETED = "workflow.publish.completed"
|
|
51
|
+
|
|
52
|
+
COMMIT_PHASE_STARTED = "workflow.commit.started"
|
|
53
|
+
COMMIT_PHASE_COMPLETED = "workflow.commit.completed"
|
|
54
|
+
|
|
55
|
+
HOOK_STRATEGY_STARTED = "hooks.strategy.started"
|
|
56
|
+
HOOK_STRATEGY_COMPLETED = "hooks.strategy.completed"
|
|
57
|
+
HOOK_STRATEGY_FAILED = "hooks.strategy.failed"
|
|
58
|
+
|
|
59
|
+
HOOK_EXECUTION_STARTED = "hooks.execution.started"
|
|
60
|
+
HOOK_EXECUTION_COMPLETED = "hooks.execution.completed"
|
|
61
|
+
HOOK_EXECUTION_FAILED = "hooks.execution.failed"
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str: # pragma: no cover - trivial
|
|
64
|
+
return str(self.value)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class WorkflowEventDispatchResult:
|
|
69
|
+
"""Result returned from publishing an event."""
|
|
70
|
+
|
|
71
|
+
event: Event
|
|
72
|
+
results: list[EventHandlerResult] = field(default_factory=list)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class _SubscriptionEntry:
|
|
77
|
+
"""Internal representation of a subscription with concurrency control."""
|
|
78
|
+
|
|
79
|
+
subscription: EventSubscription
|
|
80
|
+
description: str | None = None
|
|
81
|
+
semaphore: asyncio.Semaphore = field(default_factory=lambda: asyncio.Semaphore(1))
|
|
82
|
+
max_retries: int = 0
|
|
83
|
+
retry_backoff: float = 0.5
|
|
84
|
+
|
|
85
|
+
def __post_init__(self) -> None:
|
|
86
|
+
max_concurrent = self.subscription.max_concurrent or 1
|
|
87
|
+
self.semaphore = asyncio.Semaphore(max_concurrent)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class WorkflowEventBus:
|
|
91
|
+
"""In-process event bus compatible with ACB event handlers."""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self._subscriptions: dict[str | None, dict[str, _SubscriptionEntry]] = {}
|
|
95
|
+
self._lock = threading.RLock()
|
|
96
|
+
self._default_handlers_registered = False
|
|
97
|
+
|
|
98
|
+
def subscribe(
|
|
99
|
+
self,
|
|
100
|
+
event_type: WorkflowEvent | str | None,
|
|
101
|
+
handler: HandlerCallable,
|
|
102
|
+
*,
|
|
103
|
+
predicate: t.Callable[[Event], bool] | None = None,
|
|
104
|
+
max_concurrent: int | None = None,
|
|
105
|
+
description: str | None = None,
|
|
106
|
+
max_retries: int = 0,
|
|
107
|
+
retry_backoff: float = 0.5,
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Register a handler for an event type.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
event_type: WorkflowEvent enum value, string, or ``None`` for wildcard.
|
|
113
|
+
handler: Callable invoked when the event is published.
|
|
114
|
+
predicate: Optional additional filter predicate.
|
|
115
|
+
max_concurrent: Maximum concurrent invocations for this handler.
|
|
116
|
+
description: Optional human readable identifier.
|
|
117
|
+
max_retries: Number of retry attempts when a handler raises.
|
|
118
|
+
retry_backoff: Initial backoff delay (seconds) between retries; doubles each attempt.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Subscription ID string.
|
|
122
|
+
"""
|
|
123
|
+
event_type_value = (
|
|
124
|
+
event_type.value if isinstance(event_type, WorkflowEvent) else event_type
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
event_handler = FunctionalEventHandler(
|
|
128
|
+
handler,
|
|
129
|
+
event_type=event_type_value,
|
|
130
|
+
predicate=predicate,
|
|
131
|
+
)
|
|
132
|
+
subscription = EventSubscription(
|
|
133
|
+
handler=event_handler,
|
|
134
|
+
event_type=event_type_value,
|
|
135
|
+
predicate=predicate,
|
|
136
|
+
max_concurrent=max_concurrent or 1,
|
|
137
|
+
)
|
|
138
|
+
entry = _SubscriptionEntry(
|
|
139
|
+
subscription=subscription,
|
|
140
|
+
description=description,
|
|
141
|
+
max_retries=max_retries,
|
|
142
|
+
retry_backoff=retry_backoff,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
key = event_type_value
|
|
146
|
+
|
|
147
|
+
with self._lock:
|
|
148
|
+
bucket = self._subscriptions.setdefault(key, {})
|
|
149
|
+
bucket[str(subscription.subscription_id)] = entry
|
|
150
|
+
|
|
151
|
+
logger.debug(
|
|
152
|
+
"Registered workflow event subscription",
|
|
153
|
+
extra={
|
|
154
|
+
"event_type": event_type_value or "*",
|
|
155
|
+
"subscription_id": str(subscription.subscription_id),
|
|
156
|
+
"description": description,
|
|
157
|
+
"max_concurrent": max_concurrent or 1,
|
|
158
|
+
"max_retries": max_retries,
|
|
159
|
+
"retry_backoff": retry_backoff,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return str(subscription.subscription_id)
|
|
164
|
+
|
|
165
|
+
def unsubscribe(self, subscription_id: str) -> bool:
|
|
166
|
+
"""Remove a subscription by ID."""
|
|
167
|
+
with self._lock:
|
|
168
|
+
for bucket in self._subscriptions.values():
|
|
169
|
+
if subscription_id in bucket:
|
|
170
|
+
del bucket[subscription_id]
|
|
171
|
+
logger.debug(
|
|
172
|
+
"Removed workflow event subscription",
|
|
173
|
+
extra={"subscription_id": subscription_id},
|
|
174
|
+
)
|
|
175
|
+
return True
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def list_subscriptions(self) -> list[dict[str, t.Any]]:
|
|
179
|
+
"""Return summary information for registered subscriptions."""
|
|
180
|
+
data: list[dict[str, t.Any]] = []
|
|
181
|
+
with self._lock:
|
|
182
|
+
for event_type, bucket in self._subscriptions.items():
|
|
183
|
+
for subscription_id, entry in bucket.items():
|
|
184
|
+
data.append(
|
|
185
|
+
{
|
|
186
|
+
"subscription_id": subscription_id,
|
|
187
|
+
"event_type": event_type or "*",
|
|
188
|
+
"description": entry.description,
|
|
189
|
+
"max_concurrent": entry.subscription.max_concurrent,
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
return data
|
|
193
|
+
|
|
194
|
+
async def publish(
|
|
195
|
+
self,
|
|
196
|
+
event_type: WorkflowEvent | str,
|
|
197
|
+
payload: dict[str, t.Any] | None = None,
|
|
198
|
+
*,
|
|
199
|
+
source: str = "crackerjack.workflow",
|
|
200
|
+
**metadata: t.Any,
|
|
201
|
+
) -> WorkflowEventDispatchResult:
|
|
202
|
+
"""Publish an event to subscribed handlers."""
|
|
203
|
+
event_type_value = (
|
|
204
|
+
event_type.value if isinstance(event_type, WorkflowEvent) else event_type
|
|
205
|
+
)
|
|
206
|
+
payload = payload or {}
|
|
207
|
+
event = create_event(event_type_value, source, payload, **metadata)
|
|
208
|
+
|
|
209
|
+
entries = self._collect_subscriptions(event)
|
|
210
|
+
if not entries:
|
|
211
|
+
logger.debug(
|
|
212
|
+
"Workflow event published with no subscribers",
|
|
213
|
+
extra={"event_type": event_type_value},
|
|
214
|
+
)
|
|
215
|
+
return WorkflowEventDispatchResult(event=event, results=[])
|
|
216
|
+
|
|
217
|
+
tasks = [self._invoke_subscription(entry, event) for entry in entries]
|
|
218
|
+
raw_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
219
|
+
|
|
220
|
+
results: list[EventHandlerResult] = []
|
|
221
|
+
for entry, result in zip(entries, raw_results, strict=False):
|
|
222
|
+
if isinstance(result, EventHandlerResult):
|
|
223
|
+
results.append(result)
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
if isinstance(result, Exception):
|
|
227
|
+
logger.exception(
|
|
228
|
+
"Workflow event handler raised an exception",
|
|
229
|
+
extra={
|
|
230
|
+
"event_type": event_type_value,
|
|
231
|
+
"subscription_id": entry.subscription.subscription_id,
|
|
232
|
+
"description": entry.description,
|
|
233
|
+
},
|
|
234
|
+
exc_info=result,
|
|
235
|
+
)
|
|
236
|
+
results.append(
|
|
237
|
+
EventHandlerResult(
|
|
238
|
+
success=False,
|
|
239
|
+
error_message=str(result),
|
|
240
|
+
metadata={
|
|
241
|
+
"subscription_id": str(entry.subscription.subscription_id)
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Allow handlers to return truthy values instead of EventHandlerResult
|
|
248
|
+
success = True if result is None else bool(result)
|
|
249
|
+
results.append(
|
|
250
|
+
EventHandlerResult(
|
|
251
|
+
success=success,
|
|
252
|
+
metadata={
|
|
253
|
+
"subscription_id": str(entry.subscription.subscription_id)
|
|
254
|
+
},
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return WorkflowEventDispatchResult(event=event, results=results)
|
|
259
|
+
|
|
260
|
+
def register_logging_handler(self) -> None:
|
|
261
|
+
"""Install a default debug logging handler (idempotent)."""
|
|
262
|
+
if self._default_handlers_registered:
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
async def _log_event(event: Event) -> EventHandlerResult:
|
|
266
|
+
logger.debug(
|
|
267
|
+
"Workflow event dispatched",
|
|
268
|
+
extra={
|
|
269
|
+
"event_type": event.metadata.event_type,
|
|
270
|
+
"source": event.metadata.source,
|
|
271
|
+
"payload": event.payload,
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
return EventHandlerResult(success=True)
|
|
275
|
+
|
|
276
|
+
self.subscribe(
|
|
277
|
+
event_type=None,
|
|
278
|
+
handler=_log_event,
|
|
279
|
+
description="workflow.logging",
|
|
280
|
+
)
|
|
281
|
+
self._default_handlers_registered = True
|
|
282
|
+
|
|
283
|
+
def _collect_subscriptions(self, event: Event) -> list[_SubscriptionEntry]:
|
|
284
|
+
with self._lock:
|
|
285
|
+
specific = list(
|
|
286
|
+
self._subscriptions.get(event.metadata.event_type, {}).values()
|
|
287
|
+
)
|
|
288
|
+
wildcard = list(self._subscriptions.get(None, {}).values())
|
|
289
|
+
return specific + wildcard
|
|
290
|
+
|
|
291
|
+
async def _invoke_subscription(
|
|
292
|
+
self,
|
|
293
|
+
entry: _SubscriptionEntry,
|
|
294
|
+
event: Event,
|
|
295
|
+
) -> EventHandlerResult:
|
|
296
|
+
handler = entry.subscription.handler
|
|
297
|
+
if not handler.can_handle(event):
|
|
298
|
+
return EventHandlerResult(
|
|
299
|
+
success=True,
|
|
300
|
+
metadata={
|
|
301
|
+
"subscription_id": str(entry.subscription.subscription_id),
|
|
302
|
+
"skipped": True,
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
async with entry.semaphore:
|
|
307
|
+
attempt = 0
|
|
308
|
+
delay = max(entry.retry_backoff, 0.0)
|
|
309
|
+
while True:
|
|
310
|
+
try:
|
|
311
|
+
return await handler.handle(event)
|
|
312
|
+
except Exception as exc:
|
|
313
|
+
attempt += 1
|
|
314
|
+
if attempt > entry.max_retries:
|
|
315
|
+
logger.exception(
|
|
316
|
+
"Workflow event handler failed after retries",
|
|
317
|
+
extra={
|
|
318
|
+
"subscription_id": str(
|
|
319
|
+
entry.subscription.subscription_id
|
|
320
|
+
),
|
|
321
|
+
"description": entry.description,
|
|
322
|
+
"event_type": event.metadata.event_type,
|
|
323
|
+
"attempts": attempt,
|
|
324
|
+
},
|
|
325
|
+
exc_info=exc,
|
|
326
|
+
)
|
|
327
|
+
raise
|
|
328
|
+
|
|
329
|
+
logger.warning(
|
|
330
|
+
"Workflow event handler raised; retrying",
|
|
331
|
+
extra={
|
|
332
|
+
"subscription_id": str(entry.subscription.subscription_id),
|
|
333
|
+
"description": entry.description,
|
|
334
|
+
"event_type": event.metadata.event_type,
|
|
335
|
+
"attempt": attempt,
|
|
336
|
+
"max_retries": entry.max_retries,
|
|
337
|
+
"retry_delay": delay,
|
|
338
|
+
},
|
|
339
|
+
exc_info=exc,
|
|
340
|
+
)
|
|
341
|
+
if delay > 0:
|
|
342
|
+
await asyncio.sleep(delay)
|
|
343
|
+
delay *= 2
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
__all__ = ["WorkflowEventBus", "WorkflowEvent", "WorkflowEventDispatchResult"]
|