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,479 @@
1
+ """TOML configuration loader for HexDAG."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import tomllib # Python 3.11+
8
+ from functools import lru_cache
9
+ from pathlib import Path
10
+ from typing import Any, Literal, cast
11
+
12
+ from hexdag.core.config.models import HexDAGConfig, LoggingConfig, ManifestEntry
13
+ from hexdag.core.logging import get_logger
14
+
15
+ TOML_IMPORT_MESSAGE = (
16
+ "TOML support requires 'tomli' for Python < 3.11. Install with: pip install tomli"
17
+ )
18
+
19
+
20
+ # Type alias for configuration data that can be recursively substituted
21
+ ConfigData = str | dict[str, "ConfigData"] | list["ConfigData"] | int | float | bool | None
22
+
23
+ # Constants for boolean environment variable parsing
24
+ _TRUTHY_VALUES = frozenset({"true", "1", "yes", "on", "enabled"})
25
+ _FALSY_VALUES = frozenset({"false", "0", "no", "off", "disabled"})
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ def _parse_bool_env(value: str) -> bool:
31
+ """Parse boolean from environment variable value.
32
+
33
+ Parameters
34
+ ----------
35
+ value : str
36
+ Environment variable value
37
+
38
+ Returns
39
+ -------
40
+ bool
41
+ Parsed boolean value
42
+
43
+ Raises
44
+ ------
45
+ ValueError
46
+ If value is not a recognized boolean string
47
+ """
48
+ normalized = value.lower().strip()
49
+ if normalized in _TRUTHY_VALUES:
50
+ return True
51
+ if normalized in _FALSY_VALUES:
52
+ return False
53
+ expected = _TRUTHY_VALUES | _FALSY_VALUES
54
+ raise ValueError(f"Invalid boolean value: {value!r}. Expected one of: {expected}")
55
+
56
+
57
+ @lru_cache(maxsize=32)
58
+ def _load_and_parse_cached(path_str: str) -> HexDAGConfig:
59
+ """Cached configuration loader."""
60
+ loader = ConfigLoader()
61
+ return loader._load_and_parse(Path(path_str))
62
+
63
+
64
+ class ConfigLoader:
65
+ """Loads and processes HexDAG configuration from TOML files."""
66
+
67
+ ENV_VAR_PATTERN = re.compile(r"\$\{([^}]+)\}")
68
+
69
+ def __init__(self) -> None:
70
+ """Initialize the config loader."""
71
+
72
+ if tomllib is None:
73
+ raise ImportError(TOML_IMPORT_MESSAGE)
74
+
75
+ def load_from_toml(self, path: str | Path | None = None) -> HexDAGConfig:
76
+ """Load configuration from TOML file.
77
+
78
+ Parameters
79
+ ----------
80
+ path : str | Path | None
81
+ Path to TOML file. If None, searches for pyproject.toml or hexdag.toml
82
+
83
+ Returns
84
+ -------
85
+ HexDAGConfig
86
+ Parsed configuration with environment variables substituted
87
+
88
+ """
89
+ # Find config file
90
+ config_path = self._find_config_file(path)
91
+
92
+ # Use cached loader with file path
93
+ return _load_and_parse_cached(str(config_path.absolute()))
94
+
95
+ def _load_and_parse(self, config_path: Path) -> HexDAGConfig:
96
+ """Load and parse configuration file."""
97
+ logger.info("Loading configuration from {path}", path=config_path)
98
+
99
+ # Load TOML
100
+ with config_path.open("rb") as f:
101
+ data = tomllib.load(f)
102
+
103
+ if config_path.name == "pyproject.toml":
104
+ # Look for [tool.hexdag] section
105
+ hexdag_data = data.get("tool", {}).get("hexdag", {})
106
+ if not hexdag_data:
107
+ logger.warning("No [tool.hexdag] section found in pyproject.toml, using defaults")
108
+ return get_default_config()
109
+ else:
110
+ # Direct hexdag.toml file - check if it has [tool.hexdag] or is flat
111
+ if "tool" in data and "hexdag" in data.get("tool", {}):
112
+ # TOML file uses [tool.hexdag] format (like pyproject.toml)
113
+ hexdag_data = data["tool"]["hexdag"]
114
+ else:
115
+ # Flat format (top-level keys)
116
+ hexdag_data = data
117
+
118
+ # Process environment variable substitution
119
+ hexdag_data = self._substitute_env_vars(hexdag_data)
120
+
121
+ # Parse configuration sections
122
+ return self._parse_config(hexdag_data)
123
+
124
+ def _find_config_file(self, path: str | Path | None) -> Path:
125
+ """Find configuration file.
126
+
127
+ Parameters
128
+ ----------
129
+ path : str | Path | None
130
+ Explicit path or None to search
131
+
132
+ Returns
133
+ -------
134
+ Path
135
+ Path to configuration file
136
+
137
+ Raises
138
+ ------
139
+ FileNotFoundError
140
+ If no configuration file is found
141
+ """
142
+ if path:
143
+ config_path = Path(path)
144
+ if not config_path.exists():
145
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
146
+ return config_path
147
+
148
+ # Check environment variable first
149
+ if env_path := os.getenv("HEXDAG_CONFIG_PATH"):
150
+ config_path = Path(env_path)
151
+ if config_path.exists():
152
+ logger.debug(f"Using config from HEXDAG_CONFIG_PATH: {config_path}")
153
+ return config_path
154
+ logger.warning(f"HEXDAG_CONFIG_PATH set but file not found: {config_path}")
155
+
156
+ # Search for config files in order of preference
157
+ search_paths = [
158
+ Path("hexdag.toml"),
159
+ Path("pyproject.toml"),
160
+ Path(".hexdag.toml"),
161
+ ]
162
+
163
+ for search_path in search_paths:
164
+ if search_path.exists():
165
+ return search_path
166
+
167
+ # Also check parent directories for pyproject.toml
168
+ current = Path.cwd()
169
+ while current != current.parent:
170
+ pyproject = current / "pyproject.toml"
171
+ if pyproject.exists():
172
+ with pyproject.open("rb") as f:
173
+ data = tomllib.load(f)
174
+ if "tool" in data and "hexdag" in data["tool"]:
175
+ return pyproject
176
+ current = current.parent
177
+
178
+ raise FileNotFoundError(
179
+ "No configuration file found. Searched for: hexdag.toml, pyproject.toml, .hexdag.toml"
180
+ )
181
+
182
+ def _substitute_env_vars(self, data: Any) -> Any:
183
+ """Recursively substitute environment variables in configuration.
184
+
185
+ Parameters
186
+ ----------
187
+ data : Any
188
+ Configuration data (dict, list, str, etc.)
189
+
190
+ Returns
191
+ -------
192
+ Any
193
+ Data with environment variables substituted
194
+ """
195
+ if isinstance(data, str):
196
+ # Replace ${VAR} with environment variable value
197
+ def replacer(match: re.Match[str]) -> str:
198
+ var_name = match.group(1)
199
+ value = os.environ.get(var_name)
200
+ if value is None:
201
+ # Only log at debug level to avoid cluttering CLI output
202
+ logger.debug(
203
+ f"Environment variable ${{{var_name}}} not found, keeping placeholder"
204
+ )
205
+ return match.group(0) # Keep original placeholder
206
+
207
+ return value
208
+
209
+ return self.ENV_VAR_PATTERN.sub(replacer, data)
210
+
211
+ if isinstance(data, dict):
212
+ return {key: self._substitute_env_vars(value) for key, value in data.items()}
213
+
214
+ if isinstance(data, list):
215
+ return [self._substitute_env_vars(item) for item in data]
216
+
217
+ return data
218
+
219
+ def _parse_config(self, data: dict[str, Any]) -> HexDAGConfig:
220
+ """Parse configuration data into HexDAGConfig.
221
+
222
+ Parameters
223
+ ----------
224
+ data : dict[str, Any]
225
+ Raw configuration data from TOML
226
+
227
+ Returns
228
+ -------
229
+ HexDAGConfig
230
+ Parsed configuration object
231
+ """
232
+ config = HexDAGConfig()
233
+
234
+ # Parse modules list
235
+ if "modules" in data:
236
+ config.modules = data["modules"]
237
+ logger.debug("Loaded {count} modules", count=len(config.modules))
238
+
239
+ # Parse plugins list
240
+ if "plugins" in data:
241
+ config.plugins = data["plugins"]
242
+ logger.debug("Loaded {count} plugins", count=len(config.plugins))
243
+
244
+ # Parse dev mode
245
+ config.dev_mode = data.get("dev_mode", False)
246
+
247
+ # Parse logging configuration with environment variable overrides
248
+ config.logging = self._parse_logging_config(data.get("logging", {}))
249
+
250
+ # Parse settings section
251
+ if "settings" in data:
252
+ config.settings = data["settings"]
253
+ logger.debug("Loaded {count} settings", count=len(config.settings))
254
+
255
+ return config
256
+
257
+ def _parse_logging_config(self, logging_data: dict[str, Any]) -> LoggingConfig:
258
+ """Parse logging configuration with environment variable overrides.
259
+
260
+ Environment variables take precedence over TOML configuration:
261
+ - HEXDAG_LOG_LEVEL: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
262
+ - HEXDAG_LOG_FORMAT: Output format (console, json, structured, rich, dual)
263
+ - HEXDAG_LOG_FILE: Optional file path for log output
264
+ - HEXDAG_LOG_COLOR: Use color output (true/false)
265
+ - HEXDAG_LOG_TIMESTAMP: Include timestamp (true/false)
266
+ - HEXDAG_LOG_RICH: Use Rich library for enhanced output (true/false)
267
+ - HEXDAG_LOG_DUAL_SINK: Enable dual-sink mode (true/false)
268
+ - HEXDAG_LOG_STDLIB_BRIDGE: Enable stdlib logging bridge (true/false)
269
+ - HEXDAG_LOG_BACKTRACE: Enable backtrace in logs (true/false)
270
+ - HEXDAG_LOG_DIAGNOSE: Enable diagnose mode (true/false)
271
+
272
+ Parameters
273
+ ----------
274
+ logging_data : dict[str, Any]
275
+ Logging section from TOML config
276
+
277
+ Returns
278
+ -------
279
+ LoggingConfig
280
+ Parsed logging configuration with env overrides applied
281
+ """
282
+ # Start with TOML config values
283
+ level = logging_data.get("level", "INFO")
284
+ format_type = logging_data.get("format", "structured")
285
+ output_file = logging_data.get("output_file")
286
+ use_color = logging_data.get("use_color", True)
287
+ include_timestamp = logging_data.get("include_timestamp", True)
288
+ use_rich = logging_data.get("use_rich", False)
289
+ dual_sink = logging_data.get("dual_sink", False)
290
+ enable_stdlib_bridge = logging_data.get("enable_stdlib_bridge", False)
291
+ backtrace = logging_data.get("backtrace", True)
292
+ diagnose = logging_data.get("diagnose", True)
293
+
294
+ # Apply environment variable overrides
295
+ if env_level := os.getenv("HEXDAG_LOG_LEVEL"):
296
+ level = env_level.upper()
297
+ logger.debug(f"Overriding log level from env: {level}")
298
+
299
+ if env_format := os.getenv("HEXDAG_LOG_FORMAT"):
300
+ format_type = env_format.lower()
301
+ logger.debug(f"Overriding log format from env: {format_type}")
302
+
303
+ if env_file := os.getenv("HEXDAG_LOG_FILE"):
304
+ output_file = env_file
305
+ logger.debug(f"Overriding log file from env: {output_file}")
306
+
307
+ if env_color := os.getenv("HEXDAG_LOG_COLOR"):
308
+ try:
309
+ use_color = _parse_bool_env(env_color)
310
+ logger.debug(f"Overriding log color from env: {use_color}")
311
+ except ValueError as e:
312
+ logger.warning(f"Invalid HEXDAG_LOG_COLOR value: {e}")
313
+
314
+ if env_timestamp := os.getenv("HEXDAG_LOG_TIMESTAMP"):
315
+ try:
316
+ include_timestamp = _parse_bool_env(env_timestamp)
317
+ logger.debug(f"Overriding log timestamp from env: {include_timestamp}")
318
+ except ValueError as e:
319
+ logger.warning(f"Invalid HEXDAG_LOG_TIMESTAMP value: {e}")
320
+
321
+ if env_rich := os.getenv("HEXDAG_LOG_RICH"):
322
+ try:
323
+ use_rich = _parse_bool_env(env_rich)
324
+ logger.debug(f"Overriding use_rich from env: {use_rich}")
325
+ except ValueError as e:
326
+ logger.warning(f"Invalid HEXDAG_LOG_RICH value: {e}")
327
+
328
+ if env_dual_sink := os.getenv("HEXDAG_LOG_DUAL_SINK"):
329
+ try:
330
+ dual_sink = _parse_bool_env(env_dual_sink)
331
+ logger.debug(f"Overriding dual_sink from env: {dual_sink}")
332
+ except ValueError as e:
333
+ logger.warning(f"Invalid HEXDAG_LOG_DUAL_SINK value: {e}")
334
+
335
+ if env_stdlib_bridge := os.getenv("HEXDAG_LOG_STDLIB_BRIDGE"):
336
+ try:
337
+ enable_stdlib_bridge = _parse_bool_env(env_stdlib_bridge)
338
+ logger.debug(f"Overriding enable_stdlib_bridge from env: {enable_stdlib_bridge}")
339
+ except ValueError as e:
340
+ logger.warning(f"Invalid HEXDAG_LOG_STDLIB_BRIDGE value: {e}")
341
+
342
+ if env_backtrace := os.getenv("HEXDAG_LOG_BACKTRACE"):
343
+ try:
344
+ backtrace = _parse_bool_env(env_backtrace)
345
+ logger.debug(f"Overriding backtrace from env: {backtrace}")
346
+ except ValueError as e:
347
+ logger.warning(f"Invalid HEXDAG_LOG_BACKTRACE value: {e}")
348
+
349
+ if env_diagnose := os.getenv("HEXDAG_LOG_DIAGNOSE"):
350
+ try:
351
+ diagnose = _parse_bool_env(env_diagnose)
352
+ logger.debug(f"Overriding diagnose from env: {diagnose}")
353
+ except ValueError as e:
354
+ logger.warning(f"Invalid HEXDAG_LOG_DIAGNOSE value: {e}")
355
+
356
+ # Cast to proper Literal types for type safety
357
+ # These will be validated by Pydantic at runtime
358
+ return LoggingConfig(
359
+ level=cast("Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']", level),
360
+ format=cast("Literal['console', 'json', 'structured', 'dual', 'rich']", format_type),
361
+ output_file=output_file,
362
+ use_color=use_color,
363
+ include_timestamp=include_timestamp,
364
+ use_rich=use_rich,
365
+ dual_sink=dual_sink,
366
+ enable_stdlib_bridge=enable_stdlib_bridge,
367
+ backtrace=backtrace,
368
+ diagnose=diagnose,
369
+ )
370
+
371
+
372
+ @lru_cache(maxsize=32)
373
+ def _cached_load_config(path_str: str | None) -> HexDAGConfig:
374
+ """Internal cached configuration loader.
375
+
376
+ Parameters
377
+ ----------
378
+ path_str : str | None
379
+ String representation of path for caching
380
+ Returns
381
+ -------
382
+ HexDAGConfig
383
+ Loaded configuration
384
+ """
385
+ try:
386
+ loader = ConfigLoader()
387
+ if path_str and path_str.startswith("__auto__"):
388
+ return loader.load_from_toml(None)
389
+ return loader.load_from_toml(Path(path_str) if path_str else None)
390
+ except FileNotFoundError:
391
+ logger.info("No configuration file found, using defaults")
392
+ return get_default_config()
393
+
394
+
395
+ def load_config(path: str | Path | None = None) -> HexDAGConfig:
396
+ """Load configuration from TOML file or return defaults.
397
+
398
+ Parameters
399
+ ----------
400
+ path : str | Path | None
401
+ Path to configuration file or None to search
402
+
403
+ Returns
404
+ -------
405
+ HexDAGConfig
406
+ Loaded configuration or defaults if no file found
407
+ """
408
+
409
+ try:
410
+ loader = ConfigLoader()
411
+ return loader.load_from_toml(path)
412
+ except FileNotFoundError:
413
+ logger.info("No configuration file found, using defaults")
414
+ return get_default_config()
415
+
416
+
417
+ def clear_config_cache() -> None:
418
+ """Clear all configuration caches.
419
+
420
+ Useful for testing or when configuration files have been modified
421
+ and you need to force a reload.
422
+ """
423
+ _cached_load_config.cache_clear()
424
+ _load_and_parse_cached.cache_clear()
425
+
426
+
427
+ def get_default_config() -> HexDAGConfig:
428
+ """Get default configuration.
429
+
430
+ Returns
431
+ -------
432
+ HexDAGConfig
433
+ Default configuration with core ports and builtin components
434
+ """
435
+ return HexDAGConfig(
436
+ modules=[
437
+ "hexdag.core.ports",
438
+ "hexdag.builtin.nodes",
439
+ "hexdag.builtin.adapters.mock",
440
+ "hexdag.builtin.adapters.local",
441
+ "hexdag.builtin.tools.builtin_tools",
442
+ ],
443
+ settings={
444
+ "log_level": "INFO",
445
+ "enable_metrics": True,
446
+ },
447
+ )
448
+
449
+
450
+ def config_to_manifest_entries(config: HexDAGConfig) -> list[ManifestEntry]:
451
+ """Convert configuration to manifest entries.
452
+
453
+ Parameters
454
+ ----------
455
+ config : HexDAGConfig
456
+ Configuration object
457
+
458
+ Returns
459
+ -------
460
+ list[ManifestEntry]
461
+ List of manifest entries for registry bootstrap
462
+ """
463
+ # Core and builtin modules go to 'core' namespace, others to 'user'
464
+ module_entries = [
465
+ ManifestEntry(
466
+ namespace=(
467
+ "core"
468
+ if (module.startswith("hexdag.core") or module.startswith("hexdag.builtin"))
469
+ else "user"
470
+ ),
471
+ module=module,
472
+ )
473
+ for module in config.modules
474
+ ]
475
+
476
+ # Plugin modules go to 'plugin' namespace
477
+ plugin_entries = [ManifestEntry(namespace="plugin", module=plugin) for plugin in config.plugins]
478
+
479
+ return module_entries + plugin_entries
@@ -0,0 +1,150 @@
1
+ """Configuration data models for HexDAG."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Literal
7
+
8
+ from hexdag.core.exceptions import ValidationError
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class LoggingConfig:
13
+ """Logging configuration for HexDAG.
14
+
15
+ Attributes
16
+ ----------
17
+ level : str, default="INFO"
18
+ Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
19
+ format : str, default="structured"
20
+ Output format (console, json, structured, dual, rich)
21
+ output_file : str | None, default=None
22
+ Optional file path to write logs to
23
+ use_color : bool, default=True
24
+ Use ANSI color codes (auto-disabled for non-TTY)
25
+ include_timestamp : bool, default=True
26
+ Include timestamp in log output
27
+ use_rich : bool, default=False
28
+ Use Rich library for enhanced console output with better formatting
29
+ dual_sink : bool, default=False
30
+ Enable dual-sink mode: pretty console (Rich) + structured JSON to stdout
31
+ enable_stdlib_bridge : bool, default=False
32
+ Enable interception of stdlib logging for third-party libraries
33
+ backtrace : bool, default=True
34
+ Enable backtrace for debugging (disable in production for security)
35
+ diagnose : bool, default=True
36
+ Enable diagnose mode with variable values (disable in production for security)
37
+
38
+ Examples
39
+ --------
40
+ TOML configuration:
41
+
42
+ ```toml
43
+ [tool.hexdag.logging]
44
+ level = "DEBUG"
45
+ format = "rich"
46
+ use_rich = true
47
+ dual_sink = false
48
+ ```
49
+
50
+ Dual-sink configuration (dev mode):
51
+
52
+ ```toml
53
+ [tool.hexdag.logging]
54
+ level = "INFO"
55
+ dual_sink = true
56
+ use_rich = true
57
+ ```
58
+
59
+ Environment variable overrides:
60
+
61
+ ```bash
62
+ export HEXDAG_LOG_LEVEL=DEBUG
63
+ export HEXDAG_LOG_FORMAT=rich
64
+ export HEXDAG_LOG_FILE=/var/log/hexdag/app.log
65
+ export HEXDAG_LOG_DUAL_SINK=true
66
+ export HEXDAG_LOG_RICH=true
67
+ ```
68
+ """
69
+
70
+ level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
71
+ format: Literal["console", "json", "structured", "dual", "rich"] = "structured"
72
+ output_file: str | None = None
73
+ use_color: bool = True
74
+ include_timestamp: bool = True
75
+ use_rich: bool = False
76
+ dual_sink: bool = False
77
+ enable_stdlib_bridge: bool = False
78
+ backtrace: bool = True
79
+ diagnose: bool = True
80
+
81
+
82
+ @dataclass(frozen=True, slots=True)
83
+ class ManifestEntry:
84
+ """Single entry defining a module to load."""
85
+
86
+ namespace: str
87
+ module: str
88
+
89
+ def __post_init__(self) -> None:
90
+ """Validate manifest entry.
91
+
92
+ Raises
93
+ ------
94
+ ValidationError
95
+ If namespace or module is empty or namespace contains ':'
96
+ """
97
+ if not self.namespace:
98
+ raise ValidationError("namespace", "cannot be empty")
99
+ if not self.module:
100
+ raise ValidationError("module", "cannot be empty")
101
+ if ":" in self.namespace:
102
+ raise ValidationError("namespace", "cannot contain ':'", self.namespace)
103
+
104
+
105
+ @dataclass(slots=True)
106
+ class HexDAGConfig:
107
+ """Complete HexDAG configuration.
108
+
109
+ Attributes
110
+ ----------
111
+ modules : list[str]
112
+ List of module paths to load
113
+ plugins : list[str]
114
+ List of plugin names to load
115
+ dev_mode : bool
116
+ Enable development mode features
117
+ logging : LoggingConfig
118
+ Logging configuration
119
+ settings : dict[str, Any]
120
+ Additional custom settings
121
+
122
+ Examples
123
+ --------
124
+ TOML configuration in pyproject.toml:
125
+
126
+ ```toml
127
+ [tool.hexdag]
128
+ modules = ["myapp.adapters", "myapp.nodes"]
129
+ plugins = ["hexdag-openai", "hexdag-postgres"]
130
+ dev_mode = true
131
+
132
+ [tool.hexdag.logging]
133
+ level = "DEBUG"
134
+ format = "structured"
135
+ use_color = true
136
+ ```
137
+ """
138
+
139
+ # Core configuration
140
+ modules: list[str] = field(default_factory=list)
141
+ plugins: list[str] = field(default_factory=list)
142
+
143
+ # Development settings
144
+ dev_mode: bool = False
145
+
146
+ # Logging configuration
147
+ logging: LoggingConfig = field(default_factory=LoggingConfig)
148
+
149
+ # Additional settings
150
+ settings: dict[str, Any] = field(default_factory=dict)