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.
Files changed (261) hide show
  1. hexdag/__init__.py +116 -0
  2. hexdag/__main__.py +30 -0
  3. hexdag/adapters/executors/__init__.py +5 -0
  4. hexdag/adapters/executors/local_executor.py +316 -0
  5. hexdag/builtin/__init__.py +6 -0
  6. hexdag/builtin/adapters/__init__.py +51 -0
  7. hexdag/builtin/adapters/anthropic/__init__.py +5 -0
  8. hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
  9. hexdag/builtin/adapters/database/__init__.py +6 -0
  10. hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
  11. hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
  12. hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
  13. hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
  14. hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
  15. hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
  16. hexdag/builtin/adapters/local/README.md +59 -0
  17. hexdag/builtin/adapters/local/__init__.py +7 -0
  18. hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
  19. hexdag/builtin/adapters/memory/__init__.py +47 -0
  20. hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
  21. hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
  22. hexdag/builtin/adapters/memory/schemas.py +57 -0
  23. hexdag/builtin/adapters/memory/session_memory.py +178 -0
  24. hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
  25. hexdag/builtin/adapters/memory/state_memory.py +280 -0
  26. hexdag/builtin/adapters/mock/README.md +89 -0
  27. hexdag/builtin/adapters/mock/__init__.py +15 -0
  28. hexdag/builtin/adapters/mock/hexdag.toml +50 -0
  29. hexdag/builtin/adapters/mock/mock_database.py +225 -0
  30. hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
  31. hexdag/builtin/adapters/mock/mock_llm.py +177 -0
  32. hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
  33. hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
  34. hexdag/builtin/adapters/openai/__init__.py +5 -0
  35. hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
  36. hexdag/builtin/adapters/secret/__init__.py +7 -0
  37. hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
  38. hexdag/builtin/adapters/unified_tool_router.py +280 -0
  39. hexdag/builtin/macros/__init__.py +17 -0
  40. hexdag/builtin/macros/conversation_agent.py +390 -0
  41. hexdag/builtin/macros/llm_macro.py +151 -0
  42. hexdag/builtin/macros/reasoning_agent.py +423 -0
  43. hexdag/builtin/macros/tool_macro.py +380 -0
  44. hexdag/builtin/nodes/__init__.py +38 -0
  45. hexdag/builtin/nodes/_discovery.py +123 -0
  46. hexdag/builtin/nodes/agent_node.py +696 -0
  47. hexdag/builtin/nodes/base_node_factory.py +242 -0
  48. hexdag/builtin/nodes/composite_node.py +926 -0
  49. hexdag/builtin/nodes/data_node.py +201 -0
  50. hexdag/builtin/nodes/expression_node.py +487 -0
  51. hexdag/builtin/nodes/function_node.py +454 -0
  52. hexdag/builtin/nodes/llm_node.py +491 -0
  53. hexdag/builtin/nodes/loop_node.py +920 -0
  54. hexdag/builtin/nodes/mapped_input.py +518 -0
  55. hexdag/builtin/nodes/port_call_node.py +269 -0
  56. hexdag/builtin/nodes/tool_call_node.py +195 -0
  57. hexdag/builtin/nodes/tool_utils.py +390 -0
  58. hexdag/builtin/prompts/__init__.py +68 -0
  59. hexdag/builtin/prompts/base.py +422 -0
  60. hexdag/builtin/prompts/chat_prompts.py +303 -0
  61. hexdag/builtin/prompts/error_correction_prompts.py +320 -0
  62. hexdag/builtin/prompts/tool_prompts.py +160 -0
  63. hexdag/builtin/tools/builtin_tools.py +84 -0
  64. hexdag/builtin/tools/database_tools.py +164 -0
  65. hexdag/cli/__init__.py +17 -0
  66. hexdag/cli/__main__.py +7 -0
  67. hexdag/cli/commands/__init__.py +27 -0
  68. hexdag/cli/commands/build_cmd.py +812 -0
  69. hexdag/cli/commands/create_cmd.py +208 -0
  70. hexdag/cli/commands/docs_cmd.py +293 -0
  71. hexdag/cli/commands/generate_types_cmd.py +252 -0
  72. hexdag/cli/commands/init_cmd.py +188 -0
  73. hexdag/cli/commands/pipeline_cmd.py +494 -0
  74. hexdag/cli/commands/plugin_dev_cmd.py +529 -0
  75. hexdag/cli/commands/plugins_cmd.py +441 -0
  76. hexdag/cli/commands/studio_cmd.py +101 -0
  77. hexdag/cli/commands/validate_cmd.py +221 -0
  78. hexdag/cli/main.py +84 -0
  79. hexdag/core/__init__.py +83 -0
  80. hexdag/core/config/__init__.py +20 -0
  81. hexdag/core/config/loader.py +479 -0
  82. hexdag/core/config/models.py +150 -0
  83. hexdag/core/configurable.py +294 -0
  84. hexdag/core/context/__init__.py +37 -0
  85. hexdag/core/context/execution_context.py +378 -0
  86. hexdag/core/docs/__init__.py +26 -0
  87. hexdag/core/docs/extractors.py +678 -0
  88. hexdag/core/docs/generators.py +890 -0
  89. hexdag/core/docs/models.py +120 -0
  90. hexdag/core/domain/__init__.py +10 -0
  91. hexdag/core/domain/dag.py +1225 -0
  92. hexdag/core/exceptions.py +234 -0
  93. hexdag/core/expression_parser.py +569 -0
  94. hexdag/core/logging.py +449 -0
  95. hexdag/core/models/__init__.py +17 -0
  96. hexdag/core/models/base.py +138 -0
  97. hexdag/core/orchestration/__init__.py +46 -0
  98. hexdag/core/orchestration/body_executor.py +481 -0
  99. hexdag/core/orchestration/components/__init__.py +97 -0
  100. hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
  101. hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
  102. hexdag/core/orchestration/components/execution_coordinator.py +360 -0
  103. hexdag/core/orchestration/components/health_check_manager.py +176 -0
  104. hexdag/core/orchestration/components/input_mapper.py +143 -0
  105. hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
  106. hexdag/core/orchestration/components/node_executor.py +377 -0
  107. hexdag/core/orchestration/components/secret_manager.py +202 -0
  108. hexdag/core/orchestration/components/wave_executor.py +158 -0
  109. hexdag/core/orchestration/constants.py +17 -0
  110. hexdag/core/orchestration/events/README.md +312 -0
  111. hexdag/core/orchestration/events/__init__.py +104 -0
  112. hexdag/core/orchestration/events/batching.py +330 -0
  113. hexdag/core/orchestration/events/decorators.py +139 -0
  114. hexdag/core/orchestration/events/events.py +573 -0
  115. hexdag/core/orchestration/events/observers/__init__.py +30 -0
  116. hexdag/core/orchestration/events/observers/core_observers.py +690 -0
  117. hexdag/core/orchestration/events/observers/models.py +111 -0
  118. hexdag/core/orchestration/events/taxonomy.py +269 -0
  119. hexdag/core/orchestration/hook_context.py +237 -0
  120. hexdag/core/orchestration/hooks.py +437 -0
  121. hexdag/core/orchestration/models.py +418 -0
  122. hexdag/core/orchestration/orchestrator.py +910 -0
  123. hexdag/core/orchestration/orchestrator_factory.py +275 -0
  124. hexdag/core/orchestration/port_wrappers.py +327 -0
  125. hexdag/core/orchestration/prompt/__init__.py +32 -0
  126. hexdag/core/orchestration/prompt/template.py +332 -0
  127. hexdag/core/pipeline_builder/__init__.py +21 -0
  128. hexdag/core/pipeline_builder/component_instantiator.py +386 -0
  129. hexdag/core/pipeline_builder/include_tag.py +265 -0
  130. hexdag/core/pipeline_builder/pipeline_config.py +133 -0
  131. hexdag/core/pipeline_builder/py_tag.py +223 -0
  132. hexdag/core/pipeline_builder/tag_discovery.py +268 -0
  133. hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
  134. hexdag/core/pipeline_builder/yaml_validator.py +569 -0
  135. hexdag/core/ports/__init__.py +65 -0
  136. hexdag/core/ports/api_call.py +133 -0
  137. hexdag/core/ports/database.py +489 -0
  138. hexdag/core/ports/embedding.py +215 -0
  139. hexdag/core/ports/executor.py +237 -0
  140. hexdag/core/ports/file_storage.py +117 -0
  141. hexdag/core/ports/healthcheck.py +87 -0
  142. hexdag/core/ports/llm.py +551 -0
  143. hexdag/core/ports/memory.py +70 -0
  144. hexdag/core/ports/observer_manager.py +130 -0
  145. hexdag/core/ports/secret.py +145 -0
  146. hexdag/core/ports/tool_router.py +94 -0
  147. hexdag/core/ports_builder.py +623 -0
  148. hexdag/core/protocols.py +273 -0
  149. hexdag/core/resolver.py +304 -0
  150. hexdag/core/schema/__init__.py +9 -0
  151. hexdag/core/schema/generator.py +742 -0
  152. hexdag/core/secrets.py +242 -0
  153. hexdag/core/types.py +413 -0
  154. hexdag/core/utils/async_warnings.py +206 -0
  155. hexdag/core/utils/schema_conversion.py +78 -0
  156. hexdag/core/utils/sql_validation.py +86 -0
  157. hexdag/core/validation/secure_json.py +148 -0
  158. hexdag/core/yaml_macro.py +517 -0
  159. hexdag/mcp_server.py +3120 -0
  160. hexdag/studio/__init__.py +10 -0
  161. hexdag/studio/build_ui.py +92 -0
  162. hexdag/studio/server/__init__.py +1 -0
  163. hexdag/studio/server/main.py +100 -0
  164. hexdag/studio/server/routes/__init__.py +9 -0
  165. hexdag/studio/server/routes/execute.py +208 -0
  166. hexdag/studio/server/routes/export.py +558 -0
  167. hexdag/studio/server/routes/files.py +207 -0
  168. hexdag/studio/server/routes/plugins.py +419 -0
  169. hexdag/studio/server/routes/validate.py +220 -0
  170. hexdag/studio/ui/index.html +13 -0
  171. hexdag/studio/ui/package-lock.json +2992 -0
  172. hexdag/studio/ui/package.json +31 -0
  173. hexdag/studio/ui/postcss.config.js +6 -0
  174. hexdag/studio/ui/public/hexdag.svg +5 -0
  175. hexdag/studio/ui/src/App.tsx +251 -0
  176. hexdag/studio/ui/src/components/Canvas.tsx +408 -0
  177. hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
  178. hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
  179. hexdag/studio/ui/src/components/Header.tsx +181 -0
  180. hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
  181. hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
  182. hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
  183. hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
  184. hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
  185. hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
  186. hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
  187. hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
  188. hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
  189. hexdag/studio/ui/src/components/index.ts +8 -0
  190. hexdag/studio/ui/src/index.css +92 -0
  191. hexdag/studio/ui/src/main.tsx +10 -0
  192. hexdag/studio/ui/src/types/index.ts +123 -0
  193. hexdag/studio/ui/src/vite-env.d.ts +1 -0
  194. hexdag/studio/ui/tailwind.config.js +29 -0
  195. hexdag/studio/ui/tsconfig.json +37 -0
  196. hexdag/studio/ui/tsconfig.node.json +13 -0
  197. hexdag/studio/ui/vite.config.ts +35 -0
  198. hexdag/visualization/__init__.py +69 -0
  199. hexdag/visualization/dag_visualizer.py +1020 -0
  200. hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
  201. hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
  202. hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
  203. hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
  204. hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
  205. hexdag_plugins/.gitignore +43 -0
  206. hexdag_plugins/README.md +73 -0
  207. hexdag_plugins/__init__.py +1 -0
  208. hexdag_plugins/azure/LICENSE +21 -0
  209. hexdag_plugins/azure/README.md +414 -0
  210. hexdag_plugins/azure/__init__.py +21 -0
  211. hexdag_plugins/azure/azure_blob_adapter.py +450 -0
  212. hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
  213. hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
  214. hexdag_plugins/azure/azure_openai_adapter.py +415 -0
  215. hexdag_plugins/azure/pyproject.toml +107 -0
  216. hexdag_plugins/azure/tests/__init__.py +1 -0
  217. hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
  218. hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
  219. hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
  220. hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
  221. hexdag_plugins/hexdag_etl/README.md +168 -0
  222. hexdag_plugins/hexdag_etl/__init__.py +53 -0
  223. hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
  224. hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
  225. hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
  226. hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
  227. hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
  228. hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
  229. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
  230. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
  231. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
  232. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
  233. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
  234. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
  235. hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
  236. hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
  237. hexdag_plugins/hexdag_etl/test_transform.py +54 -0
  238. hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
  239. hexdag_plugins/mysql_adapter/LICENSE +21 -0
  240. hexdag_plugins/mysql_adapter/README.md +224 -0
  241. hexdag_plugins/mysql_adapter/__init__.py +6 -0
  242. hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
  243. hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
  244. hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
  245. hexdag_plugins/storage/README.md +184 -0
  246. hexdag_plugins/storage/__init__.py +19 -0
  247. hexdag_plugins/storage/file/__init__.py +5 -0
  248. hexdag_plugins/storage/file/local.py +325 -0
  249. hexdag_plugins/storage/ports/__init__.py +5 -0
  250. hexdag_plugins/storage/ports/vector_store.py +236 -0
  251. hexdag_plugins/storage/sql/__init__.py +7 -0
  252. hexdag_plugins/storage/sql/base.py +187 -0
  253. hexdag_plugins/storage/sql/mysql.py +27 -0
  254. hexdag_plugins/storage/sql/postgresql.py +27 -0
  255. hexdag_plugins/storage/tests/__init__.py +1 -0
  256. hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
  257. hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
  258. hexdag_plugins/storage/vector/__init__.py +7 -0
  259. hexdag_plugins/storage/vector/chromadb.py +223 -0
  260. hexdag_plugins/storage/vector/in_memory.py +285 -0
  261. 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)}