hexdag 0.5.0.dev1__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.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- hexdag_plugins/storage/vector/pgvector.py +502 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Pre-DAG and Post-DAG hook management for orchestrator lifecycle.
|
|
2
|
+
|
|
3
|
+
Hooks provide extensibility points before and after DAG execution for:
|
|
4
|
+
- Health checking adapters
|
|
5
|
+
- Loading secrets from KeyVault into memory
|
|
6
|
+
- Environment validation
|
|
7
|
+
- Resource cleanup
|
|
8
|
+
- Metrics export
|
|
9
|
+
- Notifications
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from types import MappingProxyType
|
|
20
|
+
|
|
21
|
+
from hexdag.core.orchestration.models import NodeExecutionContext
|
|
22
|
+
from hexdag.core.ports.observer_manager import ObserverManagerPort
|
|
23
|
+
|
|
24
|
+
from hexdag.core.logging import get_logger
|
|
25
|
+
from hexdag.core.orchestration.components.adapter_lifecycle_manager import (
|
|
26
|
+
AdapterLifecycleManager,
|
|
27
|
+
)
|
|
28
|
+
from hexdag.core.orchestration.components.health_check_manager import HealthCheckManager
|
|
29
|
+
from hexdag.core.orchestration.components.secret_manager import SecretManager
|
|
30
|
+
from hexdag.core.orchestration.hook_context import (
|
|
31
|
+
PipelineStatus,
|
|
32
|
+
PostHookContext,
|
|
33
|
+
PostHookManagerProtocol,
|
|
34
|
+
PreHookContext,
|
|
35
|
+
PreHookManagerProtocol,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
# Constants to replace magic values
|
|
41
|
+
HEALTH_CHECK_LATENCY_PRECISION = 1 # Decimal places for latency display
|
|
42
|
+
DEFAULT_SECRET_PREFIX = "secret:" # nosec B105 - Not a password, it's a key prefix for memory storage
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"HookConfig",
|
|
46
|
+
"PostDagHookConfig",
|
|
47
|
+
"PreDagHookManager",
|
|
48
|
+
"PostDagHookManager",
|
|
49
|
+
"PipelineStatus",
|
|
50
|
+
"PreHookContext",
|
|
51
|
+
"PostHookContext",
|
|
52
|
+
"PreHookManagerProtocol",
|
|
53
|
+
"PostHookManagerProtocol",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True, slots=True)
|
|
58
|
+
class HookConfig:
|
|
59
|
+
"""Configuration for pre-DAG hooks.
|
|
60
|
+
|
|
61
|
+
Attributes
|
|
62
|
+
----------
|
|
63
|
+
enable_health_checks : bool
|
|
64
|
+
Run health checks on all adapters before pipeline execution
|
|
65
|
+
health_check_fail_fast : bool
|
|
66
|
+
If True, unhealthy adapters block pipeline execution
|
|
67
|
+
health_check_warn_only : bool
|
|
68
|
+
If True, log warnings for unhealthy adapters but don't block
|
|
69
|
+
enable_secret_injection : bool
|
|
70
|
+
Load secrets from SecretPort into Memory before execution
|
|
71
|
+
secret_keys : list[str] | None
|
|
72
|
+
Specific secret keys to load. If None, loads all available secrets.
|
|
73
|
+
secret_prefix : str
|
|
74
|
+
Prefix for secret keys in memory (default: "secret:")
|
|
75
|
+
custom_hooks : list[Callable]
|
|
76
|
+
User-defined pre-DAG hooks. Each receives (ports, context) and returns Any.
|
|
77
|
+
|
|
78
|
+
Examples
|
|
79
|
+
--------
|
|
80
|
+
Example usage::
|
|
81
|
+
|
|
82
|
+
config = HookConfig(
|
|
83
|
+
enable_health_checks=True,
|
|
84
|
+
health_check_fail_fast=True,
|
|
85
|
+
enable_secret_injection=True,
|
|
86
|
+
secret_keys=["OPENAI_API_KEY", "DB_PASSWORD"]
|
|
87
|
+
)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
enable_health_checks: bool = True
|
|
91
|
+
health_check_fail_fast: bool = False
|
|
92
|
+
health_check_warn_only: bool = True
|
|
93
|
+
enable_secret_injection: bool = True
|
|
94
|
+
secret_keys: list[str] | None = None
|
|
95
|
+
secret_prefix: str = DEFAULT_SECRET_PREFIX
|
|
96
|
+
custom_hooks: list[Callable] = field(default_factory=list)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True, slots=True)
|
|
100
|
+
class PostDagHookConfig:
|
|
101
|
+
"""Configuration for post-DAG hooks.
|
|
102
|
+
|
|
103
|
+
Attributes
|
|
104
|
+
----------
|
|
105
|
+
enable_adapter_cleanup : bool
|
|
106
|
+
Call adapter.aclose() or adapter.ashutdown() if available
|
|
107
|
+
enable_secret_cleanup : bool
|
|
108
|
+
Remove secrets from Memory after pipeline execution
|
|
109
|
+
enable_checkpoint_save : bool
|
|
110
|
+
Save final checkpoint state
|
|
111
|
+
checkpoint_on_failure : bool
|
|
112
|
+
Save checkpoint even if pipeline fails (useful for debugging)
|
|
113
|
+
enable_metrics_export : bool
|
|
114
|
+
Export pipeline metrics to configured backends
|
|
115
|
+
metrics_backends : list[str]
|
|
116
|
+
List of metric backend names (e.g., ["prometheus", "datadog"])
|
|
117
|
+
enable_notifications : bool
|
|
118
|
+
Send notifications about pipeline completion
|
|
119
|
+
notification_channels : list[str]
|
|
120
|
+
List of notification channels (e.g., ["slack", "email"])
|
|
121
|
+
custom_hooks : list[Callable]
|
|
122
|
+
User-defined post-DAG hooks
|
|
123
|
+
run_on_success : bool
|
|
124
|
+
Run hooks when pipeline succeeds
|
|
125
|
+
run_on_failure : bool
|
|
126
|
+
Run hooks when pipeline fails
|
|
127
|
+
run_on_cancellation : bool
|
|
128
|
+
Run hooks when pipeline is cancelled
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
enable_adapter_cleanup: bool = True
|
|
132
|
+
enable_secret_cleanup: bool = True
|
|
133
|
+
enable_checkpoint_save: bool = False
|
|
134
|
+
checkpoint_on_failure: bool = True
|
|
135
|
+
enable_metrics_export: bool = False
|
|
136
|
+
metrics_backends: list[str] = field(default_factory=list)
|
|
137
|
+
enable_notifications: bool = False
|
|
138
|
+
notification_channels: list[str] = field(default_factory=list)
|
|
139
|
+
custom_hooks: list[Callable] = field(default_factory=list)
|
|
140
|
+
run_on_success: bool = True
|
|
141
|
+
run_on_failure: bool = True
|
|
142
|
+
run_on_cancellation: bool = True
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class PreDagHookManager:
|
|
146
|
+
"""Manages pre-DAG hook execution before pipeline starts.
|
|
147
|
+
|
|
148
|
+
Pre-DAG hooks execute BEFORE the PipelineStarted event and include:
|
|
149
|
+
1. Health checks on all adapters
|
|
150
|
+
2. Secret injection from KeyVault/SecretPort into Memory
|
|
151
|
+
3. Custom user-defined setup hooks
|
|
152
|
+
|
|
153
|
+
Examples
|
|
154
|
+
--------
|
|
155
|
+
Example usage::
|
|
156
|
+
|
|
157
|
+
config = HookConfig(enable_health_checks=True)
|
|
158
|
+
manager = PreDagHookManager(config)
|
|
159
|
+
results = await manager.execute_hooks(
|
|
160
|
+
ports={"llm": openai, "database": postgres},
|
|
161
|
+
context=context,
|
|
162
|
+
observer_manager=observer,
|
|
163
|
+
pipeline_name="my_pipeline"
|
|
164
|
+
)
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
def __init__(self, config: HookConfig | None = None):
|
|
168
|
+
"""Initialize pre-DAG hook manager.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
config : HookConfig | None
|
|
173
|
+
Hook configuration. If None, uses default configuration.
|
|
174
|
+
"""
|
|
175
|
+
self.config = config or HookConfig()
|
|
176
|
+
|
|
177
|
+
self._health_check_manager = HealthCheckManager(
|
|
178
|
+
fail_fast=self.config.health_check_fail_fast,
|
|
179
|
+
warn_only=self.config.health_check_warn_only,
|
|
180
|
+
)
|
|
181
|
+
self._secret_manager = SecretManager(
|
|
182
|
+
secret_keys=self.config.secret_keys,
|
|
183
|
+
secret_prefix=self.config.secret_prefix,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def get_secret_manager(self) -> SecretManager:
|
|
187
|
+
"""Get the secret manager for post-hook cleanup access.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
SecretManager
|
|
192
|
+
The secret manager instance used for secret lifecycle management
|
|
193
|
+
"""
|
|
194
|
+
return self._secret_manager
|
|
195
|
+
|
|
196
|
+
async def execute_hooks(
|
|
197
|
+
self,
|
|
198
|
+
context: NodeExecutionContext,
|
|
199
|
+
pipeline_name: str,
|
|
200
|
+
) -> dict[str, Any]:
|
|
201
|
+
"""Execute all pre-DAG hooks in order."""
|
|
202
|
+
|
|
203
|
+
from hexdag.core.context import (
|
|
204
|
+
get_observer_manager,
|
|
205
|
+
get_port,
|
|
206
|
+
get_ports,
|
|
207
|
+
)
|
|
208
|
+
from hexdag.core.orchestration.components import OrchestratorError
|
|
209
|
+
|
|
210
|
+
results: dict[str, Any] = {}
|
|
211
|
+
ports: MappingProxyType[str, Any] | dict[Any, Any] = get_ports() or {}
|
|
212
|
+
observer_manager = get_observer_manager()
|
|
213
|
+
|
|
214
|
+
# 1. Health checks
|
|
215
|
+
if self.config.enable_health_checks:
|
|
216
|
+
logger.info(f"Running health checks for pipeline '{pipeline_name}'")
|
|
217
|
+
health_results = await self._health_check_manager.check_all_adapters(
|
|
218
|
+
ports=dict(ports), observer_manager=observer_manager, pipeline_name=pipeline_name
|
|
219
|
+
)
|
|
220
|
+
results["health_checks"] = health_results
|
|
221
|
+
|
|
222
|
+
# Check for critical failures
|
|
223
|
+
if unhealthy := self._health_check_manager.get_unhealthy_adapters(health_results):
|
|
224
|
+
unhealthy_names = [h.adapter_name for h in unhealthy]
|
|
225
|
+
error_msg = f"Unhealthy adapters: {unhealthy_names}"
|
|
226
|
+
|
|
227
|
+
if self.config.health_check_fail_fast:
|
|
228
|
+
logger.error(f"Health check failed - blocking pipeline: {error_msg}")
|
|
229
|
+
raise OrchestratorError(f"Health check failed: {error_msg}")
|
|
230
|
+
if self.config.health_check_warn_only:
|
|
231
|
+
logger.warning(f"Health check issues detected: {error_msg}")
|
|
232
|
+
else:
|
|
233
|
+
logger.info(f"Health check issues: {error_msg}")
|
|
234
|
+
|
|
235
|
+
# 2. Secret injection
|
|
236
|
+
if self.config.enable_secret_injection:
|
|
237
|
+
logger.info(f"Loading secrets for pipeline '{pipeline_name}'")
|
|
238
|
+
secret_port = get_port("secret")
|
|
239
|
+
memory = get_port("memory")
|
|
240
|
+
secret_results = await self._secret_manager.load_secrets(
|
|
241
|
+
secret_port=secret_port, memory=memory, dag_id=context.dag_id
|
|
242
|
+
)
|
|
243
|
+
results["secrets_loaded"] = secret_results
|
|
244
|
+
|
|
245
|
+
# 3. Custom hooks
|
|
246
|
+
for hook in self.config.custom_hooks:
|
|
247
|
+
hook_name = hook.__name__
|
|
248
|
+
logger.info(f"Running custom pre-DAG hook: {hook_name}")
|
|
249
|
+
try:
|
|
250
|
+
hook_result = await hook(ports, context)
|
|
251
|
+
results[hook_name] = hook_result
|
|
252
|
+
except (RuntimeError, ValueError, KeyError, TypeError) as e:
|
|
253
|
+
# Specific hook errors - these are expected failure modes
|
|
254
|
+
logger.error(f"Custom hook '{hook_name}' failed: {e}", exc_info=True)
|
|
255
|
+
results[hook_name] = {"error": str(e)}
|
|
256
|
+
raise
|
|
257
|
+
|
|
258
|
+
return results
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class PostDagHookManager:
|
|
262
|
+
"""Manages post-DAG hook execution after pipeline completes.
|
|
263
|
+
|
|
264
|
+
Post-DAG hooks execute AFTER the pipeline completes (success/failure/cancellation)
|
|
265
|
+
and include:
|
|
266
|
+
1. Checkpoint saving
|
|
267
|
+
2. Metrics export
|
|
268
|
+
3. Notifications
|
|
269
|
+
4. Custom cleanup hooks
|
|
270
|
+
5. Secret cleanup (security)
|
|
271
|
+
6. Adapter cleanup (close connections)
|
|
272
|
+
|
|
273
|
+
These hooks run in a finally block to ensure cleanup happens even on failure.
|
|
274
|
+
|
|
275
|
+
Examples
|
|
276
|
+
--------
|
|
277
|
+
Example usage::
|
|
278
|
+
|
|
279
|
+
config = PostDagHookConfig(enable_secret_cleanup=True)
|
|
280
|
+
manager = PostDagHookManager(config)
|
|
281
|
+
results = await manager.execute_hooks(
|
|
282
|
+
ports=ports,
|
|
283
|
+
context=context,
|
|
284
|
+
observer_manager=observer,
|
|
285
|
+
pipeline_name="my_pipeline",
|
|
286
|
+
pipeline_status="success",
|
|
287
|
+
node_results=results,
|
|
288
|
+
duration_ms=1500.0
|
|
289
|
+
)
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
def __init__(
|
|
293
|
+
self,
|
|
294
|
+
config: PostDagHookConfig | None = None,
|
|
295
|
+
pre_hook_manager: PreDagHookManager | None = None,
|
|
296
|
+
):
|
|
297
|
+
"""Initialize post-DAG hook manager.
|
|
298
|
+
|
|
299
|
+
Parameters
|
|
300
|
+
----------
|
|
301
|
+
config : PostDagHookConfig | None
|
|
302
|
+
Hook configuration. If None, uses default configuration.
|
|
303
|
+
pre_hook_manager : PreDagHookManager | None
|
|
304
|
+
Reference to pre-hook manager for accessing secret manager
|
|
305
|
+
"""
|
|
306
|
+
self.config = config or PostDagHookConfig()
|
|
307
|
+
self._pre_hook_manager = pre_hook_manager
|
|
308
|
+
|
|
309
|
+
self._adapter_lifecycle_manager = AdapterLifecycleManager()
|
|
310
|
+
|
|
311
|
+
async def execute_hooks(
|
|
312
|
+
self,
|
|
313
|
+
context: NodeExecutionContext,
|
|
314
|
+
pipeline_name: str,
|
|
315
|
+
pipeline_status: Literal["success", "failed", "cancelled"],
|
|
316
|
+
node_results: dict[str, Any],
|
|
317
|
+
error: BaseException | None = None,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
"""Execute all post-DAG hooks."""
|
|
320
|
+
from hexdag.core.context import (
|
|
321
|
+
get_observer_manager,
|
|
322
|
+
get_port,
|
|
323
|
+
get_ports,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
results: dict[str, Any] = {}
|
|
327
|
+
ports: MappingProxyType[str, Any] | dict[Any, Any] = get_ports() or {}
|
|
328
|
+
observer_manager = get_observer_manager()
|
|
329
|
+
|
|
330
|
+
should_run = (
|
|
331
|
+
(pipeline_status == "success" and self.config.run_on_success)
|
|
332
|
+
or (pipeline_status == "failed" and self.config.run_on_failure)
|
|
333
|
+
or (pipeline_status == "cancelled" and self.config.run_on_cancellation)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if not should_run:
|
|
337
|
+
logger.debug(f"Skipping post-DAG hooks for status: {pipeline_status}")
|
|
338
|
+
return {"skipped": True, "reason": f"Not configured for {pipeline_status}"}
|
|
339
|
+
|
|
340
|
+
logger.info(f"Running post-DAG hooks for pipeline '{pipeline_name}' ({pipeline_status})")
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
# 1. Save checkpoint (if enabled)
|
|
344
|
+
if self.config.enable_checkpoint_save and (
|
|
345
|
+
pipeline_status == "success" or self.config.checkpoint_on_failure
|
|
346
|
+
):
|
|
347
|
+
try:
|
|
348
|
+
checkpoint_result = await self._save_checkpoint(
|
|
349
|
+
dict(ports), context, node_results, pipeline_status, observer_manager
|
|
350
|
+
)
|
|
351
|
+
results["checkpoint"] = checkpoint_result
|
|
352
|
+
except Exception as e:
|
|
353
|
+
# Catch all checkpoint errors - don't let them block cleanup
|
|
354
|
+
logger.error(f"Checkpoint save failed: {e}", exc_info=True)
|
|
355
|
+
results["checkpoint"] = {"error": str(e)}
|
|
356
|
+
|
|
357
|
+
# 2. Custom hooks (user-defined)
|
|
358
|
+
for hook in self.config.custom_hooks:
|
|
359
|
+
hook_name = hook.__name__
|
|
360
|
+
try:
|
|
361
|
+
logger.debug(f"Running custom post-DAG hook: {hook_name}")
|
|
362
|
+
hook_result = await hook(ports, context, node_results, pipeline_status, error)
|
|
363
|
+
results[hook_name] = hook_result
|
|
364
|
+
except Exception as e:
|
|
365
|
+
# Catch ALL exceptions from custom hooks - don't let them block cleanup
|
|
366
|
+
logger.error(f"Custom hook '{hook_name}' failed: {e}", exc_info=True)
|
|
367
|
+
results[hook_name] = {"error": str(e)}
|
|
368
|
+
|
|
369
|
+
finally:
|
|
370
|
+
# CRITICAL CLEANUP: Always run these, even if above hooks fail
|
|
371
|
+
# 3. Secret cleanup (security - do this before adapter cleanup)
|
|
372
|
+
if self.config.enable_secret_cleanup and self._pre_hook_manager:
|
|
373
|
+
try:
|
|
374
|
+
secret_manager = self._pre_hook_manager.get_secret_manager()
|
|
375
|
+
memory = get_port("memory")
|
|
376
|
+
secret_cleanup = await secret_manager.cleanup_secrets(
|
|
377
|
+
memory=memory, dag_id=context.dag_id
|
|
378
|
+
)
|
|
379
|
+
results["secret_cleanup"] = secret_cleanup
|
|
380
|
+
except Exception as e:
|
|
381
|
+
# Catch ALL exceptions - secret cleanup must be robust
|
|
382
|
+
logger.error(f"Secret cleanup failed: {e}", exc_info=True)
|
|
383
|
+
results["secret_cleanup"] = {"error": str(e)}
|
|
384
|
+
|
|
385
|
+
# 4. Adapter cleanup (close connections - do this last)
|
|
386
|
+
if self.config.enable_adapter_cleanup:
|
|
387
|
+
try:
|
|
388
|
+
adapter_cleanup = await self._adapter_lifecycle_manager.cleanup_all_adapters(
|
|
389
|
+
ports=dict(ports), observer_manager=observer_manager
|
|
390
|
+
)
|
|
391
|
+
results["adapter_cleanup"] = adapter_cleanup
|
|
392
|
+
except Exception as e:
|
|
393
|
+
# Catch ALL exceptions - adapter cleanup must be robust
|
|
394
|
+
logger.error(f"Adapter cleanup failed: {e}", exc_info=True)
|
|
395
|
+
results["adapter_cleanup"] = {"error": str(e)}
|
|
396
|
+
|
|
397
|
+
return results
|
|
398
|
+
|
|
399
|
+
async def _save_checkpoint(
|
|
400
|
+
self,
|
|
401
|
+
ports: dict[str, Any],
|
|
402
|
+
context: NodeExecutionContext,
|
|
403
|
+
node_results: dict[str, Any],
|
|
404
|
+
status: str,
|
|
405
|
+
observer_manager: ObserverManagerPort | None,
|
|
406
|
+
) -> dict[str, Any]:
|
|
407
|
+
"""Save final checkpoint state."""
|
|
408
|
+
from hexdag.core.context import get_port
|
|
409
|
+
from hexdag.core.orchestration.components import CheckpointManager
|
|
410
|
+
from hexdag.core.orchestration.models import CheckpointState
|
|
411
|
+
|
|
412
|
+
memory = get_port("memory")
|
|
413
|
+
if not memory:
|
|
414
|
+
logger.debug("No memory port available for checkpoint save")
|
|
415
|
+
return {"skipped": "No memory port available"}
|
|
416
|
+
|
|
417
|
+
checkpoint_mgr = CheckpointManager(storage=memory)
|
|
418
|
+
|
|
419
|
+
from datetime import UTC, datetime
|
|
420
|
+
|
|
421
|
+
state = CheckpointState(
|
|
422
|
+
run_id=context.dag_id,
|
|
423
|
+
dag_id=context.dag_id,
|
|
424
|
+
graph_snapshot={}, # Graph snapshot not available here
|
|
425
|
+
initial_input=None, # Initial input not available here
|
|
426
|
+
node_results=node_results,
|
|
427
|
+
completed_node_ids=list(node_results.keys()),
|
|
428
|
+
failed_node_ids=[],
|
|
429
|
+
created_at=datetime.now(UTC),
|
|
430
|
+
updated_at=datetime.now(UTC),
|
|
431
|
+
metadata={"pipeline_status": status},
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
await checkpoint_mgr.save(state)
|
|
435
|
+
logger.info(f"Saved checkpoint for run_id: {context.dag_id}")
|
|
436
|
+
|
|
437
|
+
return {"saved": True, "run_id": context.dag_id, "node_count": len(node_results)}
|