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,583 @@
1
+ """Unified lifecycle manager for pipeline setup and cleanup.
2
+
3
+ This module consolidates pre-DAG and post-DAG hook management into a single
4
+ component that handles the complete pipeline lifecycle:
5
+
6
+ - Health checks on adapters
7
+ - Secret injection from KeyVault/SecretPort
8
+ - Custom user hooks
9
+ - Checkpoint saving
10
+ - Secret cleanup (security)
11
+ - Adapter cleanup (connections)
12
+
13
+ The LifecycleManager replaces the separate PreDagHookManager, PostDagHookManager,
14
+ HealthCheckManager, SecretManager, and AdapterLifecycleManager components.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from enum import StrEnum
21
+ from typing import TYPE_CHECKING, Any, Literal
22
+
23
+ if TYPE_CHECKING:
24
+ from collections.abc import Callable
25
+ from types import MappingProxyType
26
+
27
+ from hexdag.core.orchestration.models import NodeExecutionContext
28
+ from hexdag.core.ports.memory import Memory
29
+ from hexdag.core.ports.observer_manager import ObserverManagerPort
30
+ from hexdag.core.ports.secret import SecretPort
31
+
32
+ from hexdag.core.logging import get_logger
33
+ from hexdag.core.orchestration.events import HealthCheckCompleted
34
+ from hexdag.core.ports.healthcheck import HealthStatus
35
+ from hexdag.core.protocols import HealthCheckable
36
+
37
+ logger = get_logger(__name__)
38
+
39
+ # Constants
40
+ DEFAULT_SECRET_PREFIX = "secret:" # nosec B105 - Not a password, it's a key prefix
41
+ MANAGER_PORT_NAMES = frozenset({"observer_manager"})
42
+ LATENCY_PRECISION = 1
43
+ CLEANUP_METHODS = ["aclose", "ashutdown", "cleanup"]
44
+
45
+ __all__ = [
46
+ "HookConfig",
47
+ "LifecycleManager",
48
+ "PipelineStatus",
49
+ "PostDagHookConfig",
50
+ ]
51
+
52
+
53
+ class PipelineStatus(StrEnum):
54
+ """Pipeline execution status enumeration."""
55
+
56
+ SUCCESS = "success"
57
+ FAILED = "failed"
58
+ CANCELLED = "cancelled"
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class HookConfig:
63
+ """Configuration for pre-DAG hooks.
64
+
65
+ Attributes
66
+ ----------
67
+ enable_health_checks : bool
68
+ Run health checks on all adapters before pipeline execution
69
+ health_check_fail_fast : bool
70
+ If True, unhealthy adapters block pipeline execution
71
+ health_check_warn_only : bool
72
+ If True, log warnings for unhealthy adapters but don't block
73
+ enable_secret_injection : bool
74
+ Load secrets from SecretPort into Memory before execution
75
+ secret_keys : list[str] | None
76
+ Specific secret keys to load. If None, loads all available secrets.
77
+ secret_prefix : str
78
+ Prefix for secret keys in memory (default: "secret:")
79
+ custom_hooks : list[Callable]
80
+ User-defined pre-DAG hooks. Each receives (ports, context) and returns Any.
81
+ """
82
+
83
+ enable_health_checks: bool = True
84
+ health_check_fail_fast: bool = False
85
+ health_check_warn_only: bool = True
86
+ enable_secret_injection: bool = True
87
+ secret_keys: list[str] | None = None
88
+ secret_prefix: str = DEFAULT_SECRET_PREFIX
89
+ custom_hooks: list[Callable] = field(default_factory=list)
90
+
91
+
92
+ @dataclass(frozen=True, slots=True)
93
+ class PostDagHookConfig:
94
+ """Configuration for post-DAG hooks.
95
+
96
+ Attributes
97
+ ----------
98
+ enable_adapter_cleanup : bool
99
+ Call adapter.aclose() or adapter.ashutdown() if available
100
+ enable_secret_cleanup : bool
101
+ Remove secrets from Memory after pipeline execution
102
+ enable_checkpoint_save : bool
103
+ Save final checkpoint state
104
+ checkpoint_on_failure : bool
105
+ Save checkpoint even if pipeline fails (useful for debugging)
106
+ custom_hooks : list[Callable]
107
+ User-defined post-DAG hooks
108
+ run_on_success : bool
109
+ Run hooks when pipeline succeeds
110
+ run_on_failure : bool
111
+ Run hooks when pipeline fails
112
+ run_on_cancellation : bool
113
+ Run hooks when pipeline is cancelled
114
+ """
115
+
116
+ enable_adapter_cleanup: bool = True
117
+ enable_secret_cleanup: bool = True
118
+ enable_checkpoint_save: bool = False
119
+ checkpoint_on_failure: bool = True
120
+ custom_hooks: list[Callable] = field(default_factory=list)
121
+ run_on_success: bool = True
122
+ run_on_failure: bool = True
123
+ run_on_cancellation: bool = True
124
+
125
+
126
+ class LifecycleManager:
127
+ """Unified manager for pipeline lifecycle: setup, health checks, secrets, cleanup.
128
+
129
+ This manager consolidates all pre-DAG and post-DAG operations:
130
+
131
+ Pre-execution (pre_execute):
132
+ 1. Health checks on all adapters
133
+ 2. Secret injection from SecretPort into Memory
134
+ 3. Custom user-defined setup hooks
135
+
136
+ Post-execution (post_execute):
137
+ 1. Checkpoint saving (if enabled)
138
+ 2. Custom cleanup hooks
139
+ 3. Secret cleanup (CRITICAL - always runs in finally)
140
+ 4. Adapter cleanup (CRITICAL - always runs in finally)
141
+
142
+ Examples
143
+ --------
144
+ Basic usage::
145
+
146
+ manager = LifecycleManager(
147
+ pre_config=HookConfig(enable_health_checks=True),
148
+ post_config=PostDagHookConfig(enable_secret_cleanup=True)
149
+ )
150
+
151
+ # Before pipeline execution
152
+ pre_results = await manager.pre_execute(context, "my_pipeline")
153
+
154
+ # ... pipeline runs ...
155
+
156
+ # After pipeline execution (always call, even on failure)
157
+ post_results = await manager.post_execute(
158
+ context, "my_pipeline", "success", node_results
159
+ )
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ pre_config: HookConfig | None = None,
165
+ post_config: PostDagHookConfig | None = None,
166
+ ):
167
+ """Initialize lifecycle manager.
168
+
169
+ Parameters
170
+ ----------
171
+ pre_config : HookConfig | None
172
+ Configuration for pre-DAG hooks. Uses defaults if None.
173
+ post_config : PostDagHookConfig | None
174
+ Configuration for post-DAG hooks. Uses defaults if None.
175
+ """
176
+ self.pre_config = pre_config or HookConfig()
177
+ self.post_config = post_config or PostDagHookConfig()
178
+ self._loaded_secret_keys: dict[str, list[str]] = {} # dag_id -> memory_keys
179
+
180
+ # ========================================================================
181
+ # Pre-execution
182
+ # ========================================================================
183
+
184
+ async def pre_execute(
185
+ self,
186
+ context: NodeExecutionContext,
187
+ pipeline_name: str,
188
+ ) -> dict[str, Any]:
189
+ """Execute all pre-DAG lifecycle tasks.
190
+
191
+ Parameters
192
+ ----------
193
+ context : NodeExecutionContext
194
+ Execution context for this pipeline run
195
+ pipeline_name : str
196
+ Name of the pipeline being executed
197
+
198
+ Returns
199
+ -------
200
+ dict[str, Any]
201
+ Results from all pre-execution tasks
202
+ """
203
+ from hexdag.core.context import get_observer_manager, get_port, get_ports
204
+ from hexdag.core.exceptions import OrchestratorError
205
+
206
+ results: dict[str, Any] = {}
207
+ ports: MappingProxyType[str, Any] | dict[Any, Any] = get_ports() or {}
208
+ observer_manager = get_observer_manager()
209
+
210
+ # 1. Health checks
211
+ if self.pre_config.enable_health_checks:
212
+ logger.info(f"Running health checks for pipeline '{pipeline_name}'")
213
+ health_results = await self._check_all_adapters(
214
+ ports=dict(ports),
215
+ observer_manager=observer_manager,
216
+ pipeline_name=pipeline_name,
217
+ )
218
+ results["health_checks"] = health_results
219
+
220
+ # Check for critical failures
221
+ if unhealthy := self._get_unhealthy_adapters(health_results):
222
+ unhealthy_names = [h.adapter_name for h in unhealthy]
223
+ error_msg = f"Unhealthy adapters: {unhealthy_names}"
224
+
225
+ if self.pre_config.health_check_fail_fast:
226
+ logger.error(f"Health check failed - blocking pipeline: {error_msg}")
227
+ raise OrchestratorError(f"Health check failed: {error_msg}")
228
+ if self.pre_config.health_check_warn_only:
229
+ logger.warning(f"Health check issues detected: {error_msg}")
230
+ else:
231
+ logger.info(f"Health check issues: {error_msg}")
232
+
233
+ # 2. Secret injection
234
+ if self.pre_config.enable_secret_injection:
235
+ logger.info(f"Loading secrets for pipeline '{pipeline_name}'")
236
+ secret_port = get_port("secret")
237
+ memory = get_port("memory")
238
+ secret_results = await self._load_secrets(
239
+ secret_port=secret_port,
240
+ memory=memory,
241
+ dag_id=context.dag_id,
242
+ )
243
+ results["secrets_loaded"] = secret_results
244
+
245
+ # 3. Custom hooks
246
+ for hook in self.pre_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
+ logger.error(f"Custom hook '{hook_name}' failed: {e}", exc_info=True)
254
+ results[hook_name] = {"error": str(e)}
255
+ raise
256
+
257
+ return results
258
+
259
+ # ========================================================================
260
+ # Post-execution
261
+ # ========================================================================
262
+
263
+ async def post_execute(
264
+ self,
265
+ context: NodeExecutionContext,
266
+ pipeline_name: str,
267
+ pipeline_status: Literal["success", "failed", "cancelled"],
268
+ node_results: dict[str, Any],
269
+ error: BaseException | None = None,
270
+ ) -> dict[str, Any]:
271
+ """Execute all post-DAG lifecycle tasks.
272
+
273
+ Parameters
274
+ ----------
275
+ context : NodeExecutionContext
276
+ Execution context
277
+ pipeline_name : str
278
+ Name of the pipeline
279
+ pipeline_status : Literal["success", "failed", "cancelled"]
280
+ Final pipeline status
281
+ node_results : dict[str, Any]
282
+ Results from all executed nodes
283
+ error : BaseException | None
284
+ Exception if pipeline failed
285
+
286
+ Returns
287
+ -------
288
+ dict[str, Any]
289
+ Results from all post-execution tasks
290
+ """
291
+ from hexdag.core.context import get_observer_manager, get_port, get_ports
292
+
293
+ results: dict[str, Any] = {}
294
+ ports: MappingProxyType[str, Any] | dict[Any, Any] = get_ports() or {}
295
+ observer_manager = get_observer_manager()
296
+
297
+ should_run = (
298
+ (pipeline_status == "success" and self.post_config.run_on_success)
299
+ or (pipeline_status == "failed" and self.post_config.run_on_failure)
300
+ or (pipeline_status == "cancelled" and self.post_config.run_on_cancellation)
301
+ )
302
+
303
+ if not should_run:
304
+ logger.debug(f"Skipping post-DAG hooks for status: {pipeline_status}")
305
+ return {"skipped": True, "reason": f"Not configured for {pipeline_status}"}
306
+
307
+ logger.info(f"Running post-DAG hooks for pipeline '{pipeline_name}' ({pipeline_status})")
308
+
309
+ try:
310
+ # 1. Save checkpoint (if enabled)
311
+ if self.post_config.enable_checkpoint_save and (
312
+ pipeline_status == "success" or self.post_config.checkpoint_on_failure
313
+ ):
314
+ try:
315
+ checkpoint_result = await self._save_checkpoint(
316
+ dict(ports), context, node_results, pipeline_status, observer_manager
317
+ )
318
+ results["checkpoint"] = checkpoint_result
319
+ except Exception as e:
320
+ logger.error(f"Checkpoint save failed: {e}", exc_info=True)
321
+ results["checkpoint"] = {"error": str(e)}
322
+
323
+ # 2. Custom hooks (user-defined)
324
+ for hook in self.post_config.custom_hooks:
325
+ hook_name = hook.__name__
326
+ try:
327
+ logger.debug(f"Running custom post-DAG hook: {hook_name}")
328
+ hook_result = await hook(ports, context, node_results, pipeline_status, error)
329
+ results[hook_name] = hook_result
330
+ except Exception as e:
331
+ logger.error(f"Custom hook '{hook_name}' failed: {e}", exc_info=True)
332
+ results[hook_name] = {"error": str(e)}
333
+
334
+ finally:
335
+ # CRITICAL CLEANUP: Always run these, even if above hooks fail
336
+ # 3. Secret cleanup (security - do this before adapter cleanup)
337
+ if self.post_config.enable_secret_cleanup:
338
+ try:
339
+ memory = get_port("memory")
340
+ secret_cleanup = await self._cleanup_secrets(
341
+ memory=memory, dag_id=context.dag_id
342
+ )
343
+ results["secret_cleanup"] = secret_cleanup
344
+ except Exception as e:
345
+ logger.error(f"Secret cleanup failed: {e}", exc_info=True)
346
+ results["secret_cleanup"] = {"error": str(e)}
347
+
348
+ # 4. Adapter cleanup (close connections - do this last)
349
+ if self.post_config.enable_adapter_cleanup:
350
+ try:
351
+ adapter_cleanup = await self._cleanup_all_adapters(
352
+ ports=dict(ports), observer_manager=observer_manager
353
+ )
354
+ results["adapter_cleanup"] = adapter_cleanup
355
+ except Exception as e:
356
+ logger.error(f"Adapter cleanup failed: {e}", exc_info=True)
357
+ results["adapter_cleanup"] = {"error": str(e)}
358
+
359
+ return results
360
+
361
+ # ========================================================================
362
+ # Health Checks (inlined from HealthCheckManager)
363
+ # ========================================================================
364
+
365
+ async def _check_all_adapters(
366
+ self,
367
+ ports: dict[str, Any],
368
+ observer_manager: ObserverManagerPort | None,
369
+ pipeline_name: str,
370
+ ) -> list[HealthStatus]:
371
+ """Run health checks on all adapters that implement ahealth_check()."""
372
+ health_results = []
373
+
374
+ for port_name, adapter in ports.items():
375
+ if port_name in MANAGER_PORT_NAMES:
376
+ continue
377
+
378
+ if isinstance(adapter, HealthCheckable):
379
+ status = await self._check_single_adapter(port_name, adapter, observer_manager)
380
+ health_results.append(status)
381
+
382
+ return health_results
383
+
384
+ async def _check_single_adapter(
385
+ self,
386
+ port_name: str,
387
+ adapter: Any,
388
+ observer_manager: ObserverManagerPort | None,
389
+ ) -> HealthStatus:
390
+ """Check health of a single adapter."""
391
+ try:
392
+ logger.debug(f"Running health check for {port_name}")
393
+ health_check = adapter.ahealth_check
394
+ status: HealthStatus = await health_check()
395
+ status.port_name = port_name
396
+
397
+ if observer_manager:
398
+ event = HealthCheckCompleted(
399
+ adapter_name=status.adapter_name,
400
+ port_name=port_name,
401
+ status=status,
402
+ )
403
+ await observer_manager.notify(event)
404
+
405
+ self._log_health_result(port_name, status)
406
+ return status
407
+
408
+ except (RuntimeError, ConnectionError, TimeoutError, ValueError) as e:
409
+ logger.error(f"Health check failed for {port_name}: {e}", exc_info=True)
410
+ adapter_name = getattr(adapter, "_hexdag_name", port_name)
411
+ return HealthStatus(
412
+ status="unhealthy",
413
+ adapter_name=adapter_name,
414
+ port_name=port_name,
415
+ error=e,
416
+ )
417
+
418
+ def _log_health_result(self, port_name: str, status: HealthStatus) -> None:
419
+ """Log health check result."""
420
+ if status.status == "healthy":
421
+ latency_info = (
422
+ f" ({status.latency_ms:.{LATENCY_PRECISION}f}ms)" if status.latency_ms else ""
423
+ )
424
+ logger.info(f"✅ {port_name} health check: {status.status}{latency_info}")
425
+ else:
426
+ logger.warning(f"⚠️ {port_name} health check: {status.status} - {status.error}")
427
+
428
+ def _get_unhealthy_adapters(self, health_results: list[HealthStatus]) -> list[HealthStatus]:
429
+ """Filter health results to only unhealthy adapters."""
430
+ return [h for h in health_results if h.status == "unhealthy"]
431
+
432
+ # ========================================================================
433
+ # Secret Management (inlined from SecretManager)
434
+ # ========================================================================
435
+
436
+ async def _load_secrets(
437
+ self,
438
+ secret_port: SecretPort | None,
439
+ memory: Memory | None,
440
+ dag_id: str,
441
+ ) -> dict[str, str]:
442
+ """Load secrets from SecretPort into Memory."""
443
+ if not secret_port:
444
+ logger.debug("No secret port configured, skipping secret injection")
445
+ return {}
446
+
447
+ if not memory:
448
+ logger.warning("Secret port configured but no memory port available")
449
+ return {}
450
+
451
+ try:
452
+ mapping = await secret_port.aload_secrets_to_memory(
453
+ memory=memory,
454
+ prefix=self.pre_config.secret_prefix,
455
+ keys=self.pre_config.secret_keys,
456
+ )
457
+
458
+ memory_keys = list(mapping.values())
459
+ self._loaded_secret_keys[dag_id] = memory_keys
460
+
461
+ logger.info(
462
+ f"Loaded {len(mapping)} secrets into memory with prefix "
463
+ f"'{self.pre_config.secret_prefix}'"
464
+ )
465
+ logger.debug(f"Secret keys loaded: {list(mapping.keys())}")
466
+
467
+ return mapping
468
+
469
+ except (ValueError, KeyError, RuntimeError) as e:
470
+ logger.error(f"Failed to inject secrets: {e}", exc_info=True)
471
+ raise
472
+
473
+ async def _cleanup_secrets(
474
+ self,
475
+ memory: Memory | None,
476
+ dag_id: str,
477
+ ) -> dict[str, Any]:
478
+ """Remove secrets from Memory for security."""
479
+ if not memory:
480
+ logger.debug("No memory port available for secret cleanup")
481
+ return {"cleaned": False, "reason": "No memory port"}
482
+
483
+ secret_keys = self._loaded_secret_keys.get(dag_id, [])
484
+
485
+ if not secret_keys:
486
+ logger.debug("No secrets were loaded for this pipeline")
487
+ return {"cleaned": True, "keys_removed": 0}
488
+
489
+ removed_count = 0
490
+ for secret_key in secret_keys:
491
+ try:
492
+ await memory.aset(secret_key, None)
493
+ removed_count += 1
494
+ logger.debug(f"Removed secret from memory: {secret_key}")
495
+ except (RuntimeError, ValueError, KeyError) as e:
496
+ logger.warning(f"Failed to remove secret '{secret_key}': {e}")
497
+
498
+ if dag_id in self._loaded_secret_keys:
499
+ del self._loaded_secret_keys[dag_id]
500
+
501
+ logger.info(f"Secret cleanup: Removed {removed_count} secret(s) from memory")
502
+ return {"cleaned": True, "keys_removed": removed_count}
503
+
504
+ # ========================================================================
505
+ # Adapter Cleanup (inlined from AdapterLifecycleManager)
506
+ # ========================================================================
507
+
508
+ async def _cleanup_all_adapters(
509
+ self,
510
+ ports: dict[str, Any],
511
+ observer_manager: ObserverManagerPort | None,
512
+ ) -> dict[str, Any]:
513
+ """Close adapter connections and release resources."""
514
+ cleaned_adapters = []
515
+
516
+ for port_name, adapter in ports.items():
517
+ if port_name in MANAGER_PORT_NAMES:
518
+ continue
519
+
520
+ if await self._cleanup_single_adapter(port_name, adapter):
521
+ cleaned_adapters.append(port_name)
522
+
523
+ return {"cleaned_adapters": cleaned_adapters, "count": len(cleaned_adapters)}
524
+
525
+ async def _cleanup_single_adapter(self, port_name: str, adapter: Any) -> bool:
526
+ """Attempt to clean up a single adapter."""
527
+ for method_name in CLEANUP_METHODS:
528
+ if hasattr(adapter, method_name) and callable(getattr(adapter, method_name)):
529
+ cleanup_method = getattr(adapter, method_name)
530
+ try:
531
+ logger.debug(f"Cleaning up adapter '{port_name}' via {method_name}()")
532
+ await cleanup_method()
533
+ logger.info(f"✅ Cleaned up adapter: {port_name}")
534
+ return True
535
+ except (RuntimeError, ValueError, TypeError, ConnectionError, OSError) as e:
536
+ logger.warning(f"Cleanup failed for {port_name}: {e}")
537
+ return False
538
+
539
+ return False
540
+
541
+ # ========================================================================
542
+ # Checkpoint (inlined from PostDagHookManager)
543
+ # ========================================================================
544
+
545
+ async def _save_checkpoint(
546
+ self,
547
+ ports: dict[str, Any],
548
+ context: NodeExecutionContext,
549
+ node_results: dict[str, Any],
550
+ status: str,
551
+ observer_manager: ObserverManagerPort | None,
552
+ ) -> dict[str, Any]:
553
+ """Save final checkpoint state."""
554
+ from hexdag.core.context import get_port
555
+ from hexdag.core.orchestration.components import CheckpointManager
556
+ from hexdag.core.orchestration.models import CheckpointState
557
+
558
+ memory = get_port("memory")
559
+ if not memory:
560
+ logger.debug("No memory port available for checkpoint save")
561
+ return {"skipped": "No memory port available"}
562
+
563
+ checkpoint_mgr = CheckpointManager(storage=memory)
564
+
565
+ from datetime import UTC, datetime
566
+
567
+ state = CheckpointState(
568
+ run_id=context.dag_id,
569
+ dag_id=context.dag_id,
570
+ graph_snapshot={},
571
+ initial_input=None,
572
+ node_results=node_results,
573
+ completed_node_ids=list(node_results.keys()),
574
+ failed_node_ids=[],
575
+ created_at=datetime.now(UTC),
576
+ updated_at=datetime.now(UTC),
577
+ metadata={"pipeline_status": status},
578
+ )
579
+
580
+ await checkpoint_mgr.save(state)
581
+ logger.info(f"Saved checkpoint for run_id: {context.dag_id}")
582
+
583
+ return {"saved": True, "run_id": context.dag_id, "node_count": len(node_results)}