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,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
+ ...