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,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)}
|