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,518 @@
1
+ """Automatic input mapping using Pydantic models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, cast
6
+
7
+ from pydantic import BaseModel, Field, create_model, model_validator
8
+
9
+ from hexdag.core.exceptions import ResourceNotFoundError, ValidationError
10
+ from hexdag.core.protocols import DictConvertible, is_dict_convertible, is_schema_type
11
+
12
+
13
+ class FieldMappingRegistry:
14
+ """Registry for common field mappings - empty by default, no magic."""
15
+
16
+ def __init__(self) -> None:
17
+ """Initialize empty registry - users must define their mappings."""
18
+ self.mappings: dict[str, dict[str, str]] = {}
19
+
20
+ def register(self, name: str, mapping: dict[str, str]) -> None:
21
+ """Register a reusable field mapping.
22
+
23
+ Args
24
+ ----
25
+ name: Name for the mapping pattern
26
+ mapping: dict of {target_field: "source.path"}
27
+
28
+ Raises
29
+ ------
30
+ ValidationError
31
+ If the mapping name is empty
32
+ """
33
+ if not name:
34
+ raise ValidationError("name", "cannot be empty")
35
+ if not mapping:
36
+ raise ValidationError("mapping", "cannot be empty")
37
+ self.mappings[name] = mapping
38
+
39
+ def get(self, name_or_mapping: str | dict[str, str]) -> dict[str, str]:
40
+ """Get mapping by name or return inline mapping.
41
+
42
+ Args
43
+ ----
44
+ name_or_mapping: Either a string name or inline mapping dict
45
+
46
+ Returns
47
+ -------
48
+ dict[str, str]
49
+ The resolved mapping dictionary
50
+
51
+ Raises
52
+ ------
53
+ ResourceNotFoundError
54
+ If the mapping name is not found in registry
55
+ """
56
+ if isinstance(name_or_mapping, str):
57
+ if name_or_mapping not in self.mappings:
58
+ available = list(self.mappings.keys()) if self.mappings else []
59
+ raise ResourceNotFoundError("field mapping", name_or_mapping, available)
60
+ return self.mappings[name_or_mapping]
61
+ return name_or_mapping
62
+
63
+ def clear(self) -> None:
64
+ """Clear all registered mappings."""
65
+ self.mappings.clear()
66
+
67
+
68
+ class FieldExtractor:
69
+ """Handles extraction of values from nested data structures."""
70
+
71
+ @staticmethod
72
+ def extract(data: dict[Any, Any] | DictConvertible, path: str) -> Any:
73
+ """Extract value from nested data structure using dot notation path.
74
+
75
+ Args
76
+ ----
77
+ data: The data structure to extract from
78
+ path: Dot-separated path to the value (e.g., "user.profile.name")
79
+
80
+ Returns
81
+ -------
82
+ The extracted value or None if not found
83
+
84
+ """
85
+ if not path:
86
+ return data
87
+
88
+ parts = path.split(".")
89
+ current: Any = data
90
+
91
+ for part in parts:
92
+ current = FieldExtractor._extract_single_level(current, part)
93
+ if current is None:
94
+ break
95
+
96
+ return current
97
+
98
+ @staticmethod
99
+ def _extract_single_level(data: Any, key: str) -> Any:
100
+ """Extract a single level from the data.
101
+
102
+ Args
103
+ ----
104
+ data: Current data object
105
+ key: The key/attribute to extract
106
+
107
+ Returns
108
+ -------
109
+ The value at the key or None if not found
110
+
111
+ """
112
+ if data is None:
113
+ return None
114
+
115
+ if isinstance(data, dict):
116
+ return data.get(key)
117
+
118
+ if is_dict_convertible(data):
119
+ return getattr(data, key, None)
120
+
121
+ # Try generic attribute access for other objects
122
+ try:
123
+ return getattr(data, key, None)
124
+ except (AttributeError, TypeError):
125
+ return None
126
+
127
+
128
+ class TypeInferrer:
129
+ """Handles type inference from Pydantic models."""
130
+
131
+ @staticmethod
132
+ def infer_from_path(model: type[BaseModel], field_path: list[str]) -> type[Any]:
133
+ """Infer field type from a Pydantic model and field path.
134
+
135
+ Args
136
+ ----
137
+ model: The Pydantic model class
138
+ field_path: list of field names forming the path
139
+
140
+ Returns
141
+ -------
142
+ The inferred type or Any if inference fails
143
+
144
+ """
145
+ if not field_path:
146
+ return model
147
+
148
+ field_name = field_path[0]
149
+ field_type = TypeInferrer._get_field_type(model, field_name)
150
+
151
+ if field_type is None:
152
+ return cast("type[Any]", Any)
153
+
154
+ # Recurse for nested paths
155
+ if len(field_path) > 1 and TypeInferrer._is_base_model(field_type):
156
+ return TypeInferrer.infer_from_path(field_type, field_path[1:])
157
+
158
+ return field_type
159
+
160
+ @staticmethod
161
+ def _get_field_type(model: type[BaseModel], field_name: str) -> type[Any] | None:
162
+ """Get the type of a specific field from a model.
163
+
164
+ Args
165
+ ----
166
+ model: The Pydantic model class
167
+ field_name: Name of the field
168
+
169
+ Returns
170
+ -------
171
+ The field type or None if not found
172
+
173
+ """
174
+ try:
175
+ # Pydantic v2 approach - use protocol check
176
+ if is_schema_type(model):
177
+ model_fields = getattr(model, "model_fields", {})
178
+ if field_name in model_fields:
179
+ annotation: type[Any] = model_fields[field_name].annotation
180
+ return annotation
181
+ except (AttributeError, TypeError, KeyError):
182
+ # Field not found or error accessing it
183
+ pass
184
+
185
+ return None
186
+
187
+ @staticmethod
188
+ def _is_base_model(field_type: Any) -> bool:
189
+ """Check if a type is a BaseModel subclass.
190
+
191
+ Args
192
+ ----
193
+ field_type: The type to check
194
+
195
+ Returns
196
+ -------
197
+ True if the type is a BaseModel subclass
198
+
199
+ """
200
+ return is_schema_type(field_type)
201
+
202
+
203
+ class ModelFactory:
204
+ """Factory for creating mapped Pydantic models."""
205
+
206
+ @staticmethod
207
+ def create_mapped_model(
208
+ name: str,
209
+ mapping: dict[str, str],
210
+ dependency_models: dict[str, type[BaseModel]] | None = None,
211
+ ) -> type[BaseModel]:
212
+ """Create a Pydantic model with automatic field mapping.
213
+
214
+ Args
215
+ ----
216
+ name: Name for the generated model
217
+ mapping: Field mapping {target_field: "source.field.path"}
218
+ dependency_models: Optional dict of {dep_name: OutputModel} for type inference
219
+
220
+ Returns
221
+ -------
222
+ Pydantic model class with automatic field extraction
223
+
224
+ """
225
+ field_definitions = ModelFactory._build_field_definitions(mapping, dependency_models)
226
+
227
+ validator = ModelFactory._create_validator(mapping)
228
+
229
+ model: type[BaseModel] = create_model(
230
+ name,
231
+ __validators__={"extract_mapped_fields": validator},
232
+ **field_definitions,
233
+ )
234
+
235
+ model._field_mapping = mapping # type: ignore[attr-defined]
236
+
237
+ return model
238
+
239
+ @staticmethod
240
+ def _build_field_definitions(
241
+ mapping: dict[str, str], dependency_models: dict[str, type[BaseModel]] | None
242
+ ) -> dict[str, Any]:
243
+ """Build field definitions with type inference.
244
+
245
+ Args
246
+ ----
247
+ mapping: Field mapping dictionary
248
+ dependency_models: Optional dependency models for type inference
249
+
250
+ Returns
251
+ -------
252
+ dictionary of field definitions for create_model
253
+
254
+ """
255
+ definitions: dict[str, Any] = {}
256
+
257
+ for target_field, source_path in mapping.items():
258
+ field_type = ModelFactory._infer_field_type(source_path, dependency_models)
259
+ # Make fields optional by default since mapping might not provide them
260
+ definitions[target_field] = (field_type | None, Field(default=None))
261
+
262
+ return definitions
263
+
264
+ @staticmethod
265
+ def _infer_field_type(
266
+ source_path: str, dependency_models: dict[str, type[BaseModel]] | None
267
+ ) -> type[Any]:
268
+ """Infer type for a field from source path.
269
+
270
+ Args
271
+ ----
272
+ source_path: The source path string
273
+ dependency_models: Optional dependency models
274
+
275
+ Returns
276
+ -------
277
+ The inferred type or Any
278
+
279
+ """
280
+ if not dependency_models or "." not in source_path:
281
+ return cast("type[Any]", Any)
282
+
283
+ parts = source_path.split(".")
284
+ dep_name = parts[0]
285
+
286
+ if dep_name not in dependency_models:
287
+ return cast("type[Any]", Any)
288
+
289
+ return TypeInferrer.infer_from_path(dependency_models[dep_name], parts[1:])
290
+
291
+ @staticmethod
292
+ def _create_validator(mapping: dict[str, str]) -> Any:
293
+ """Create the field extraction validator.
294
+
295
+ Args
296
+ ----
297
+ mapping: Field mapping dictionary
298
+
299
+ Returns
300
+ -------
301
+ A Pydantic validator function
302
+
303
+ """
304
+
305
+ def extract_mapped_fields(data: Any) -> dict[str, Any]:
306
+ """Extract fields from nested structure based on mapping.
307
+
308
+ This validator handles two scenarios:
309
+ 1. Data pre-processed by ExecutionCoordinator._apply_input_mapping
310
+ - Target fields already exist in data with resolved values
311
+ - Just pass through the pre-resolved data
312
+ 2. Raw dependency data that needs extraction
313
+ - Use FieldExtractor to extract values from nested structures
314
+ """
315
+ if not isinstance(data, dict):
316
+ return {}
317
+
318
+ result: dict[str, Any] = {}
319
+ target_fields = set(mapping.keys())
320
+
321
+ # Check if data was pre-processed by ExecutionCoordinator
322
+ # Pre-processed data has the target field names as keys (not source paths)
323
+ data_keys = set(data.keys())
324
+ if target_fields <= data_keys:
325
+ # Data already has all target fields - it was pre-processed
326
+ # Just extract the target fields directly
327
+ for target_field in target_fields:
328
+ result[target_field] = data.get(target_field)
329
+ return result
330
+
331
+ # Data needs extraction using the mapping paths
332
+ for target_field, source_path in mapping.items():
333
+ # Skip $input paths - these should have been resolved by ExecutionCoordinator
334
+ # If we get here with $input paths, the data wasn't pre-processed
335
+ if source_path.startswith("$input"):
336
+ # Try to find the target field directly in data
337
+ if target_field in data:
338
+ result[target_field] = data[target_field]
339
+ continue
340
+
341
+ value = FieldExtractor.extract(data, source_path)
342
+ if value is not None:
343
+ result[target_field] = value
344
+
345
+ return result
346
+
347
+ return model_validator(mode="before")(extract_mapped_fields)
348
+
349
+
350
+ class MappedInput:
351
+ """Factory for creating auto-mapped Pydantic input models.
352
+
353
+ This class provides a simple API for creating Pydantic models
354
+ that automatically map fields from nested input structures.
355
+
356
+ Example
357
+ -------
358
+ ConsumerInput = MappedInput.create_model(
359
+ "ConsumerInput",
360
+ {
361
+ "content": "processor.text",
362
+ "language": "processor.metadata.lang",
363
+ "status": "validator.status"
364
+ }
365
+ )
366
+
367
+ """
368
+
369
+ @staticmethod
370
+ def create_model(
371
+ name: str,
372
+ mapping: dict[str, str],
373
+ dependency_models: dict[str, type[BaseModel]] | None = None,
374
+ ) -> type[BaseModel]:
375
+ """Create a Pydantic model with automatic field mapping.
376
+
377
+ Args:
378
+ ----
379
+ name: Name for the generated model
380
+ mapping: Field mapping {target_field: "source.field.path"}
381
+ dependency_models: Optional dict of {dep_name: OutputModel} for type inference
382
+
383
+ Returns
384
+ -------
385
+ Pydantic model class with automatic field extraction
386
+
387
+ Example
388
+ -------
389
+ ConsumerInput = MappedInput.create_model(
390
+ "ConsumerInput",
391
+ {
392
+ "content": "processor.text",
393
+ "language": "processor.metadata.lang",
394
+ "status": "validator.status"
395
+ }
396
+ )
397
+
398
+ """
399
+ return ModelFactory.create_mapped_model(name, mapping, dependency_models)
400
+
401
+ # Maintain backward compatibility
402
+ _extract_value = staticmethod(FieldExtractor.extract)
403
+ _infer_field_type = staticmethod(TypeInferrer.infer_from_path)
404
+
405
+
406
+ class AutoMappedInput(BaseModel):
407
+ """Base class for models with automatic field mapping.
408
+
409
+ Users can subclass this to create models with field mapping.
410
+
411
+ Example
412
+ -------
413
+ class ConsumerInput(AutoMappedInput):
414
+ content: str
415
+ language: str
416
+ status: str
417
+
418
+ _field_mapping = {
419
+ "content": "processor.text",
420
+ "language": "processor.metadata.lang",
421
+ "status": "validator.status"
422
+ }
423
+
424
+ """
425
+
426
+ @model_validator(mode="before")
427
+ @classmethod
428
+ def apply_field_mapping(cls: type[AutoMappedInput], data: Any) -> dict[str, Any]:
429
+ """Automatically apply field mapping before validation.
430
+
431
+ Args
432
+ ----
433
+ cls: The class being instantiated
434
+ data: Input data to be mapped
435
+
436
+ Returns
437
+ -------
438
+ Mapped data dictionary
439
+
440
+ """
441
+ field_mapping = cls._get_field_mapping()
442
+
443
+ if not field_mapping:
444
+ return cls._normalize_to_dict(data)
445
+
446
+ if not isinstance(data, dict):
447
+ return {}
448
+
449
+ result: dict[str, Any] = {}
450
+ for target_field, source_path in field_mapping.items():
451
+ value = FieldExtractor.extract(data, source_path)
452
+ if value is not None:
453
+ result[target_field] = value
454
+
455
+ return result
456
+
457
+ @classmethod
458
+ def _get_field_mapping(cls) -> dict[str, str]:
459
+ """Get the field mapping from the class.
460
+
461
+ Returns
462
+ -------
463
+ The field mapping dictionary or empty dict
464
+
465
+ """
466
+ if not hasattr(cls, "_field_mapping"):
467
+ return {}
468
+
469
+ mapping_attr = getattr(cls, "_field_mapping", None)
470
+
471
+ if mapping_attr is None:
472
+ return {}
473
+
474
+ # Direct dict assignment on the class
475
+ if isinstance(mapping_attr, dict):
476
+ return mapping_attr
477
+
478
+ # Pydantic private attr with default
479
+ if hasattr(mapping_attr, "default"):
480
+ default_value = getattr(mapping_attr, "default", {})
481
+ if isinstance(default_value, dict):
482
+ return default_value
483
+ if hasattr(default_value, "items"):
484
+ try:
485
+ return dict(default_value.items())
486
+ except (TypeError, ValueError):
487
+ pass
488
+ return {}
489
+
490
+ # Try to use it directly if it's dict-like
491
+ if hasattr(mapping_attr, "items"):
492
+ try:
493
+ # Call items() method to get the key-value pairs
494
+ return dict(mapping_attr.items())
495
+ except (TypeError, ValueError, AttributeError):
496
+ pass
497
+
498
+ return {}
499
+
500
+ @staticmethod
501
+ def _normalize_to_dict(data: Any) -> dict[str, Any]:
502
+ """Normalize data to a dictionary.
503
+
504
+ Args
505
+ ----
506
+ data: Input data
507
+
508
+ Returns
509
+ -------
510
+ dictionary representation of the data
511
+
512
+ """
513
+ if isinstance(data, dict):
514
+ return data
515
+ if is_dict_convertible(data):
516
+ result: dict[str, Any] = data.model_dump()
517
+ return result
518
+ return {}