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,111 @@
|
|
|
1
|
+
"""Observer models and data classes.
|
|
2
|
+
|
|
3
|
+
This module provides typed data structures for observer implementations,
|
|
4
|
+
following the framework's "Pydantic validation everywhere" principle.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AlertType(StrEnum):
|
|
13
|
+
"""Types of alerts that can be triggered."""
|
|
14
|
+
|
|
15
|
+
SLOW_NODE = "SLOW_NODE"
|
|
16
|
+
NODE_FAILURE = "NODE_FAILURE"
|
|
17
|
+
RESOURCE_EXHAUSTED = "RESOURCE_EXHAUSTED"
|
|
18
|
+
QUALITY_ISSUE = "QUALITY_ISSUE"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AlertSeverity(StrEnum):
|
|
22
|
+
"""Alert severity levels."""
|
|
23
|
+
|
|
24
|
+
INFO = "info"
|
|
25
|
+
WARNING = "warning"
|
|
26
|
+
ERROR = "error"
|
|
27
|
+
CRITICAL = "critical"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class Alert:
|
|
32
|
+
"""Typed alert data structure.
|
|
33
|
+
|
|
34
|
+
Replaces untyped dict[str, Any] with proper validation and type safety.
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
type : AlertType
|
|
39
|
+
Type of alert
|
|
40
|
+
node : str
|
|
41
|
+
Node that triggered the alert
|
|
42
|
+
message : str
|
|
43
|
+
Human-readable alert message
|
|
44
|
+
timestamp : float
|
|
45
|
+
Unix timestamp when alert was triggered
|
|
46
|
+
severity : AlertSeverity
|
|
47
|
+
Alert severity level
|
|
48
|
+
duration_ms : float, optional
|
|
49
|
+
Duration in milliseconds (for slow node alerts)
|
|
50
|
+
threshold_ms : float, optional
|
|
51
|
+
Threshold that was exceeded (for slow node alerts)
|
|
52
|
+
error : str, optional
|
|
53
|
+
Error message (for failure alerts)
|
|
54
|
+
metadata : dict[str, Any]
|
|
55
|
+
Additional alert metadata
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
type: AlertType
|
|
59
|
+
node: str
|
|
60
|
+
message: str
|
|
61
|
+
timestamp: float
|
|
62
|
+
severity: AlertSeverity = AlertSeverity.WARNING
|
|
63
|
+
duration_ms: float | None = None
|
|
64
|
+
threshold_ms: float | None = None
|
|
65
|
+
error: str | None = None
|
|
66
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class NodeMetrics:
|
|
71
|
+
"""Consolidated metrics for a single node.
|
|
72
|
+
|
|
73
|
+
Uses single dataclass instead of multiple parallel dict structures.
|
|
74
|
+
Follows the HandlerEntry pattern from README.md.
|
|
75
|
+
|
|
76
|
+
Attributes
|
|
77
|
+
----------
|
|
78
|
+
timings : list[float]
|
|
79
|
+
List of execution times in milliseconds
|
|
80
|
+
executions : int
|
|
81
|
+
Total number of executions
|
|
82
|
+
failures : int
|
|
83
|
+
Total number of failures
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
timings: list[float] = field(default_factory=list)
|
|
87
|
+
executions: int = 0
|
|
88
|
+
failures: int = 0
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def average_ms(self) -> float:
|
|
92
|
+
"""Calculate average execution time."""
|
|
93
|
+
return sum(self.timings) / len(self.timings) if self.timings else 0.0
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def min_ms(self) -> float:
|
|
97
|
+
"""Get minimum execution time."""
|
|
98
|
+
return min(self.timings) if self.timings else 0.0
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def max_ms(self) -> float:
|
|
102
|
+
"""Get maximum execution time."""
|
|
103
|
+
return max(self.timings) if self.timings else 0.0
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def success_rate(self) -> float:
|
|
107
|
+
"""Calculate success rate as percentage."""
|
|
108
|
+
if self.executions == 0:
|
|
109
|
+
return 0.0
|
|
110
|
+
successes = self.executions - self.failures
|
|
111
|
+
return (successes / self.executions) * 100.0
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Event taxonomy: registry metadata, envelope building, validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import dataclass, fields
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from enum import Enum, StrEnum
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, ValidationError, field_validator
|
|
14
|
+
|
|
15
|
+
from .events import EVENT_REGISTRY, Event, EventSpec
|
|
16
|
+
|
|
17
|
+
# Canonical namespace/action definitions
|
|
18
|
+
EVENT_TYPE_RE = re.compile(r"^[a-z]+:[a-z]+$")
|
|
19
|
+
|
|
20
|
+
APPROVED_ACTIONS: dict[str, set[str]] = {
|
|
21
|
+
"pipeline": {"started", "completed", "failed"},
|
|
22
|
+
"dag": {"started", "completed", "failed"},
|
|
23
|
+
"wave": {"started", "completed"},
|
|
24
|
+
"node": {"started", "completed", "failed", "skipped"},
|
|
25
|
+
"policy": {"decision"},
|
|
26
|
+
"observer": {"timeout", "error"},
|
|
27
|
+
"registry": {"resolved", "missing"},
|
|
28
|
+
"tool": {"called", "completed"},
|
|
29
|
+
"llm": {"prompt", "response"},
|
|
30
|
+
"port": set(),
|
|
31
|
+
"memory": set(),
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Severity(StrEnum):
|
|
36
|
+
"""Severity levels used in the event envelope."""
|
|
37
|
+
|
|
38
|
+
info = "info"
|
|
39
|
+
warn = "warn"
|
|
40
|
+
error = "error"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True)
|
|
44
|
+
class EventContext:
|
|
45
|
+
"""Execution context supplied when building an envelope."""
|
|
46
|
+
|
|
47
|
+
pipeline: str
|
|
48
|
+
pipeline_run_id: str
|
|
49
|
+
tenant: str | None = None
|
|
50
|
+
project: str | None = None
|
|
51
|
+
environment: str | None = None
|
|
52
|
+
correlation_id: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def generate_event_id(preferred: str | None = None) -> str:
|
|
56
|
+
"""Generate a stable string identifier for an event."""
|
|
57
|
+
if preferred:
|
|
58
|
+
return str(preferred)
|
|
59
|
+
|
|
60
|
+
func = getattr(uuid, "uuid7", None)
|
|
61
|
+
if func is not None:
|
|
62
|
+
return str(func())
|
|
63
|
+
return str(uuid.uuid4())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _now_rfc3339_ms() -> str:
|
|
67
|
+
"""Return current UTC timestamp with millisecond precision."""
|
|
68
|
+
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _coerce(value: Any) -> Any:
|
|
72
|
+
"""Convert common non-JSON types into serializable representations."""
|
|
73
|
+
if isinstance(value, Exception):
|
|
74
|
+
return str(value)
|
|
75
|
+
if isinstance(value, Enum):
|
|
76
|
+
return value.value
|
|
77
|
+
if isinstance(value, (bytes, bytearray)):
|
|
78
|
+
return value.decode("utf-8", errors="replace")
|
|
79
|
+
if hasattr(value, "isoformat"):
|
|
80
|
+
try:
|
|
81
|
+
return value.isoformat()
|
|
82
|
+
except Exception: # pragma: no cover - fallback path
|
|
83
|
+
return str(value)
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _ensure_json_serializable(payload: Any) -> None:
|
|
88
|
+
"""Verify payload can be encoded as JSON using stdlib encoder.
|
|
89
|
+
|
|
90
|
+
Raises
|
|
91
|
+
------
|
|
92
|
+
TypeError
|
|
93
|
+
If ``payload`` cannot be JSON-encoded using the standard library ``json`` module.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
json.dumps(payload)
|
|
97
|
+
except TypeError as exc: # pragma: no cover - surfaced as ValueError via caller
|
|
98
|
+
raise TypeError(f"attrs not JSON-serializable: {exc}") from exc
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class EventEnvelope(BaseModel):
|
|
102
|
+
"""Pydantic model describing the canonical event envelope."""
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(extra="forbid", use_enum_values=True)
|
|
105
|
+
|
|
106
|
+
event_type: str
|
|
107
|
+
event_id: str
|
|
108
|
+
timestamp: str
|
|
109
|
+
pipeline: str
|
|
110
|
+
pipeline_run_id: str
|
|
111
|
+
severity: Severity
|
|
112
|
+
attrs: dict[str, Any]
|
|
113
|
+
node: str | None = None
|
|
114
|
+
wave: int | None = None
|
|
115
|
+
tenant: str | None = None
|
|
116
|
+
project: str | None = None
|
|
117
|
+
environment: str | None = None
|
|
118
|
+
correlation_id: str | None = None
|
|
119
|
+
|
|
120
|
+
@field_validator("event_type")
|
|
121
|
+
@classmethod
|
|
122
|
+
def _validate_event_type_field(cls, value: str) -> str:
|
|
123
|
+
validate_event_type(value)
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
@field_validator("timestamp")
|
|
127
|
+
@classmethod
|
|
128
|
+
def _validate_timestamp_field(cls, value: str) -> str:
|
|
129
|
+
_validate_timestamp(value)
|
|
130
|
+
return value
|
|
131
|
+
|
|
132
|
+
@field_validator("attrs")
|
|
133
|
+
@classmethod
|
|
134
|
+
def _validate_attrs_field(cls, value: dict[str, Any]) -> dict[str, Any]:
|
|
135
|
+
_ensure_json_serializable(value)
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _infer_severity(event_type: str) -> Severity:
|
|
140
|
+
"""Derive severity from event action."""
|
|
141
|
+
_, action = event_type.split(":", 1)
|
|
142
|
+
if action in {"failed", "error"}:
|
|
143
|
+
return Severity.error
|
|
144
|
+
return Severity.info
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _resolve_attr_fields(event: Event, spec: EventSpec) -> tuple[str, ...]:
|
|
148
|
+
if spec.attr_fields is not None:
|
|
149
|
+
return spec.attr_fields
|
|
150
|
+
mapped = set(spec.envelope_fields.values())
|
|
151
|
+
attr_fields: list[str] = []
|
|
152
|
+
for field in fields(event):
|
|
153
|
+
if not field.init:
|
|
154
|
+
continue
|
|
155
|
+
if field.name in mapped:
|
|
156
|
+
continue
|
|
157
|
+
attr_fields.append(field.name)
|
|
158
|
+
return tuple(attr_fields)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def build_envelope(event: Event, context: EventContext) -> dict[str, Any]:
|
|
162
|
+
"""Convert an internal event object into a canonical envelope dict.
|
|
163
|
+
|
|
164
|
+
Raises
|
|
165
|
+
------
|
|
166
|
+
KeyError
|
|
167
|
+
If the event class is not registered in ``EVENT_REGISTRY``.
|
|
168
|
+
AttributeError
|
|
169
|
+
If the event instance is missing an attribute required by the
|
|
170
|
+
registry mapping.
|
|
171
|
+
ValueError
|
|
172
|
+
If the generated payload fails envelope validation.
|
|
173
|
+
"""
|
|
174
|
+
class_name = type(event).__name__
|
|
175
|
+
try:
|
|
176
|
+
spec = EVENT_REGISTRY[class_name]
|
|
177
|
+
except KeyError as exc:
|
|
178
|
+
raise KeyError(f"unmapped event class: {class_name}") from exc
|
|
179
|
+
|
|
180
|
+
payload: dict[str, Any] = {
|
|
181
|
+
"event_type": spec.event_type,
|
|
182
|
+
"event_id": generate_event_id(getattr(event, "event_id", None)),
|
|
183
|
+
"timestamp": _now_rfc3339_ms(),
|
|
184
|
+
"pipeline": context.pipeline,
|
|
185
|
+
"pipeline_run_id": context.pipeline_run_id,
|
|
186
|
+
"severity": _infer_severity(spec.event_type).value,
|
|
187
|
+
"attrs": {},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Optional context fields when provided
|
|
191
|
+
for field in ("tenant", "project", "environment", "correlation_id"):
|
|
192
|
+
value = getattr(context, field)
|
|
193
|
+
if value:
|
|
194
|
+
payload[field] = value
|
|
195
|
+
|
|
196
|
+
# Populate mapped top-level fields from the event data
|
|
197
|
+
for target, attr_name in spec.envelope_fields.items():
|
|
198
|
+
if not hasattr(event, attr_name):
|
|
199
|
+
raise AttributeError(
|
|
200
|
+
f"Event '{class_name}' missing attribute '{attr_name}' required for '{target}'"
|
|
201
|
+
)
|
|
202
|
+
value = getattr(event, attr_name)
|
|
203
|
+
payload[target] = _coerce(value)
|
|
204
|
+
|
|
205
|
+
# Build attrs from declared event fields
|
|
206
|
+
for attr_name in _resolve_attr_fields(event, spec):
|
|
207
|
+
if not hasattr(event, attr_name):
|
|
208
|
+
continue
|
|
209
|
+
payload["attrs"][attr_name] = _coerce(getattr(event, attr_name))
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
envelope_model = EventEnvelope.model_validate(payload)
|
|
213
|
+
except ValidationError as exc: # pragma: no cover - handled via ValueError for callers
|
|
214
|
+
raise ValueError(str(exc)) from exc
|
|
215
|
+
|
|
216
|
+
return envelope_model.model_dump(exclude_none=True)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def validate_event_type(event_type: str) -> None:
|
|
220
|
+
"""Ensure event_type follows namespace:action pattern and is approved.
|
|
221
|
+
|
|
222
|
+
Raises
|
|
223
|
+
------
|
|
224
|
+
ValueError
|
|
225
|
+
If the provided event type does not match the canonical pattern or
|
|
226
|
+
is not part of the approved namespace/action sets.
|
|
227
|
+
"""
|
|
228
|
+
if not EVENT_TYPE_RE.match(event_type):
|
|
229
|
+
raise ValueError(f"event_type not matching ^[a-z]+:[a-z]+$: {event_type}")
|
|
230
|
+
namespace, action = event_type.split(":", 1)
|
|
231
|
+
actions = APPROVED_ACTIONS.get(namespace)
|
|
232
|
+
if actions is None or (actions and action not in actions):
|
|
233
|
+
raise ValueError(f"event_type not in approved sets: {event_type}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _validate_timestamp(ts: str) -> None:
|
|
237
|
+
try:
|
|
238
|
+
datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
239
|
+
except ValueError as exc:
|
|
240
|
+
raise ValueError("timestamp must be ISO 8601 compatible") from exc
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
REQUIRED_FIELDS = (
|
|
244
|
+
"event_type",
|
|
245
|
+
"event_id",
|
|
246
|
+
"timestamp",
|
|
247
|
+
"pipeline",
|
|
248
|
+
"pipeline_run_id",
|
|
249
|
+
"severity",
|
|
250
|
+
"attrs",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def validate_envelope(envelope: dict[str, Any]) -> None:
|
|
255
|
+
"""Validate a complete event envelope against required fields and rules.
|
|
256
|
+
|
|
257
|
+
Raises
|
|
258
|
+
------
|
|
259
|
+
ValueError
|
|
260
|
+
If mandatory fields are missing or the payload fails schema validation.
|
|
261
|
+
"""
|
|
262
|
+
for field_name in REQUIRED_FIELDS:
|
|
263
|
+
if field_name not in envelope:
|
|
264
|
+
raise ValueError(f"missing field: {field_name}")
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
EventEnvelope.model_validate(envelope)
|
|
268
|
+
except ValidationError as exc:
|
|
269
|
+
raise ValueError(str(exc)) from exc
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Context objects, protocols, and constants for hook execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from hexdag.core.orchestration.models import NodeExecutionContext
|
|
11
|
+
from hexdag.core.ports.observer_manager import ObserverManagerPort
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Constants for pipeline status (replacing string literals)
|
|
15
|
+
class PipelineStatus(StrEnum):
|
|
16
|
+
"""Pipeline execution status enumeration.
|
|
17
|
+
|
|
18
|
+
This replaces string literals for pipeline status with a proper type.
|
|
19
|
+
|
|
20
|
+
Attributes
|
|
21
|
+
----------
|
|
22
|
+
SUCCESS : str
|
|
23
|
+
Pipeline completed successfully
|
|
24
|
+
FAILED : str
|
|
25
|
+
Pipeline failed with an error
|
|
26
|
+
CANCELLED : str
|
|
27
|
+
Pipeline was cancelled (e.g., timeout)
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
SUCCESS = "success"
|
|
31
|
+
FAILED = "failed"
|
|
32
|
+
CANCELLED = "cancelled"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True)
|
|
36
|
+
class PreHookContext:
|
|
37
|
+
"""Context for pre-DAG hook execution.
|
|
38
|
+
|
|
39
|
+
Bundles all parameters needed for pre-DAG hooks into a single object,
|
|
40
|
+
reducing parameter lists and improving maintainability.
|
|
41
|
+
|
|
42
|
+
Attributes
|
|
43
|
+
----------
|
|
44
|
+
ports : dict[str, Any]
|
|
45
|
+
All available ports for the pipeline
|
|
46
|
+
context : NodeExecutionContext
|
|
47
|
+
Execution context for this pipeline run
|
|
48
|
+
observer_manager : ObserverManagerPort | None
|
|
49
|
+
Optional observer for event emission
|
|
50
|
+
pipeline_name : str
|
|
51
|
+
Name of the pipeline being executed
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
Example usage::
|
|
56
|
+
|
|
57
|
+
ctx = PreHookContext(
|
|
58
|
+
ports={"llm": openai, "database": postgres},
|
|
59
|
+
context=execution_context,
|
|
60
|
+
observer_manager=observer,
|
|
61
|
+
pipeline_name="my_pipeline"
|
|
62
|
+
)
|
|
63
|
+
results = await manager.execute_hooks(ctx)
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
ports: dict[str, Any]
|
|
67
|
+
context: NodeExecutionContext
|
|
68
|
+
observer_manager: ObserverManagerPort | None
|
|
69
|
+
pipeline_name: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(slots=True)
|
|
73
|
+
class PostHookContext:
|
|
74
|
+
"""Context for post-DAG hook execution.
|
|
75
|
+
|
|
76
|
+
Bundles all parameters needed for post-DAG hooks into a single object,
|
|
77
|
+
reducing parameter lists and improving maintainability.
|
|
78
|
+
|
|
79
|
+
Attributes
|
|
80
|
+
----------
|
|
81
|
+
ports : dict[str, Any]
|
|
82
|
+
All available ports
|
|
83
|
+
context : NodeExecutionContext
|
|
84
|
+
Execution context
|
|
85
|
+
observer_manager : ObserverManagerPort | None
|
|
86
|
+
Optional observer manager
|
|
87
|
+
pipeline_name : str
|
|
88
|
+
Pipeline name
|
|
89
|
+
pipeline_status : PipelineStatus
|
|
90
|
+
Final pipeline status (enum)
|
|
91
|
+
node_results : dict[str, Any]
|
|
92
|
+
Results from all executed nodes
|
|
93
|
+
error : Exception | None
|
|
94
|
+
Exception if pipeline failed
|
|
95
|
+
|
|
96
|
+
Examples
|
|
97
|
+
--------
|
|
98
|
+
Example usage::
|
|
99
|
+
|
|
100
|
+
ctx = PostHookContext(
|
|
101
|
+
ports=ports,
|
|
102
|
+
context=execution_context,
|
|
103
|
+
observer_manager=observer,
|
|
104
|
+
pipeline_name="my_pipeline",
|
|
105
|
+
pipeline_status=PipelineStatus.SUCCESS,
|
|
106
|
+
node_results={"node1": "result1"},
|
|
107
|
+
error=None
|
|
108
|
+
)
|
|
109
|
+
results = await manager.execute_hooks(ctx)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
ports: dict[str, Any]
|
|
113
|
+
context: NodeExecutionContext
|
|
114
|
+
observer_manager: ObserverManagerPort | None
|
|
115
|
+
pipeline_name: str
|
|
116
|
+
pipeline_status: PipelineStatus
|
|
117
|
+
node_results: dict[str, Any]
|
|
118
|
+
error: Exception | None = None
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def status_str(self) -> str:
|
|
122
|
+
"""Get status as string for backward compatibility.
|
|
123
|
+
|
|
124
|
+
Returns
|
|
125
|
+
-------
|
|
126
|
+
str
|
|
127
|
+
Status value ("success", "failed", or "cancelled")
|
|
128
|
+
"""
|
|
129
|
+
return self.pipeline_status.value
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class PreHookManagerProtocol(Protocol):
|
|
133
|
+
"""Protocol for pre-DAG hook managers.
|
|
134
|
+
|
|
135
|
+
This protocol defines the interface that all pre-DAG hook managers must implement,
|
|
136
|
+
enabling better testing, dependency injection, and extensibility.
|
|
137
|
+
|
|
138
|
+
Examples
|
|
139
|
+
--------
|
|
140
|
+
>>> class CustomPreHookManager:
|
|
141
|
+
... async def execute_hooks(
|
|
142
|
+
... self,
|
|
143
|
+
... ports: dict[str, Any],
|
|
144
|
+
... context: NodeExecutionContext,
|
|
145
|
+
... observer_manager: ObserverManagerPort | None,
|
|
146
|
+
... pipeline_name: str,
|
|
147
|
+
... ) -> dict[str, Any]:
|
|
148
|
+
... # Custom implementation
|
|
149
|
+
... return {}
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
async def execute_hooks(
|
|
153
|
+
self,
|
|
154
|
+
ports: dict[str, Any],
|
|
155
|
+
context: NodeExecutionContext,
|
|
156
|
+
observer_manager: ObserverManagerPort | None,
|
|
157
|
+
pipeline_name: str,
|
|
158
|
+
) -> dict[str, Any]:
|
|
159
|
+
"""Execute all pre-DAG hooks.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
ports : dict[str, Any]
|
|
164
|
+
All available ports for the pipeline
|
|
165
|
+
context : NodeExecutionContext
|
|
166
|
+
Execution context for this pipeline run
|
|
167
|
+
observer_manager : ObserverManagerPort | None
|
|
168
|
+
Optional observer for event emission
|
|
169
|
+
pipeline_name : str
|
|
170
|
+
Name of the pipeline being executed
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
dict[str, Any]
|
|
175
|
+
Results from all hook executions
|
|
176
|
+
"""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PostHookManagerProtocol(Protocol):
|
|
181
|
+
"""Protocol for post-DAG hook managers.
|
|
182
|
+
|
|
183
|
+
This protocol defines the interface that all post-DAG hook managers must implement,
|
|
184
|
+
enabling better testing, dependency injection, and extensibility.
|
|
185
|
+
|
|
186
|
+
Examples
|
|
187
|
+
--------
|
|
188
|
+
>>> class CustomPostHookManager:
|
|
189
|
+
... async def execute_hooks(
|
|
190
|
+
... self,
|
|
191
|
+
... ports: dict[str, Any],
|
|
192
|
+
... context: NodeExecutionContext,
|
|
193
|
+
... observer_manager: ObserverManagerPort | None,
|
|
194
|
+
... pipeline_name: str,
|
|
195
|
+
... pipeline_status: str,
|
|
196
|
+
... node_results: dict[str, Any],
|
|
197
|
+
... error: Exception | None = None,
|
|
198
|
+
... ) -> dict[str, Any]:
|
|
199
|
+
... # Custom implementation
|
|
200
|
+
... return {}
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
async def execute_hooks(
|
|
204
|
+
self,
|
|
205
|
+
ports: dict[str, Any],
|
|
206
|
+
context: NodeExecutionContext,
|
|
207
|
+
observer_manager: ObserverManagerPort | None,
|
|
208
|
+
pipeline_name: str,
|
|
209
|
+
pipeline_status: str,
|
|
210
|
+
node_results: dict[str, Any],
|
|
211
|
+
error: Exception | None = None,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
"""Execute all post-DAG hooks.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
ports : dict[str, Any]
|
|
218
|
+
All available ports
|
|
219
|
+
context : NodeExecutionContext
|
|
220
|
+
Execution context
|
|
221
|
+
observer_manager : ObserverManagerPort | None
|
|
222
|
+
Optional observer manager
|
|
223
|
+
pipeline_name : str
|
|
224
|
+
Pipeline name
|
|
225
|
+
pipeline_status : str
|
|
226
|
+
Final pipeline status
|
|
227
|
+
node_results : dict[str, Any]
|
|
228
|
+
Results from all executed nodes
|
|
229
|
+
error : Exception | None
|
|
230
|
+
Exception if pipeline failed
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
dict[str, Any]
|
|
235
|
+
Results from all hook executions
|
|
236
|
+
"""
|
|
237
|
+
...
|