hexdag 0.5.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hexdag/__init__.py +116 -0
- hexdag/__main__.py +30 -0
- hexdag/adapters/executors/__init__.py +5 -0
- hexdag/adapters/executors/local_executor.py +316 -0
- hexdag/builtin/__init__.py +6 -0
- hexdag/builtin/adapters/__init__.py +51 -0
- hexdag/builtin/adapters/anthropic/__init__.py +5 -0
- hexdag/builtin/adapters/anthropic/anthropic_adapter.py +151 -0
- hexdag/builtin/adapters/database/__init__.py +6 -0
- hexdag/builtin/adapters/database/csv/csv_adapter.py +249 -0
- hexdag/builtin/adapters/database/pgvector/__init__.py +5 -0
- hexdag/builtin/adapters/database/pgvector/pgvector_adapter.py +478 -0
- hexdag/builtin/adapters/database/sqlalchemy/sqlalchemy_adapter.py +252 -0
- hexdag/builtin/adapters/database/sqlite/__init__.py +5 -0
- hexdag/builtin/adapters/database/sqlite/sqlite_adapter.py +410 -0
- hexdag/builtin/adapters/local/README.md +59 -0
- hexdag/builtin/adapters/local/__init__.py +7 -0
- hexdag/builtin/adapters/local/local_observer_manager.py +696 -0
- hexdag/builtin/adapters/memory/__init__.py +47 -0
- hexdag/builtin/adapters/memory/file_memory_adapter.py +297 -0
- hexdag/builtin/adapters/memory/in_memory_memory.py +216 -0
- hexdag/builtin/adapters/memory/schemas.py +57 -0
- hexdag/builtin/adapters/memory/session_memory.py +178 -0
- hexdag/builtin/adapters/memory/sqlite_memory_adapter.py +215 -0
- hexdag/builtin/adapters/memory/state_memory.py +280 -0
- hexdag/builtin/adapters/mock/README.md +89 -0
- hexdag/builtin/adapters/mock/__init__.py +15 -0
- hexdag/builtin/adapters/mock/hexdag.toml +50 -0
- hexdag/builtin/adapters/mock/mock_database.py +225 -0
- hexdag/builtin/adapters/mock/mock_embedding.py +223 -0
- hexdag/builtin/adapters/mock/mock_llm.py +177 -0
- hexdag/builtin/adapters/mock/mock_tool_adapter.py +192 -0
- hexdag/builtin/adapters/mock/mock_tool_router.py +232 -0
- hexdag/builtin/adapters/openai/__init__.py +5 -0
- hexdag/builtin/adapters/openai/openai_adapter.py +634 -0
- hexdag/builtin/adapters/secret/__init__.py +7 -0
- hexdag/builtin/adapters/secret/local_secret_adapter.py +248 -0
- hexdag/builtin/adapters/unified_tool_router.py +280 -0
- hexdag/builtin/macros/__init__.py +17 -0
- hexdag/builtin/macros/conversation_agent.py +390 -0
- hexdag/builtin/macros/llm_macro.py +151 -0
- hexdag/builtin/macros/reasoning_agent.py +423 -0
- hexdag/builtin/macros/tool_macro.py +380 -0
- hexdag/builtin/nodes/__init__.py +38 -0
- hexdag/builtin/nodes/_discovery.py +123 -0
- hexdag/builtin/nodes/agent_node.py +696 -0
- hexdag/builtin/nodes/base_node_factory.py +242 -0
- hexdag/builtin/nodes/composite_node.py +926 -0
- hexdag/builtin/nodes/data_node.py +201 -0
- hexdag/builtin/nodes/expression_node.py +487 -0
- hexdag/builtin/nodes/function_node.py +454 -0
- hexdag/builtin/nodes/llm_node.py +491 -0
- hexdag/builtin/nodes/loop_node.py +920 -0
- hexdag/builtin/nodes/mapped_input.py +518 -0
- hexdag/builtin/nodes/port_call_node.py +269 -0
- hexdag/builtin/nodes/tool_call_node.py +195 -0
- hexdag/builtin/nodes/tool_utils.py +390 -0
- hexdag/builtin/prompts/__init__.py +68 -0
- hexdag/builtin/prompts/base.py +422 -0
- hexdag/builtin/prompts/chat_prompts.py +303 -0
- hexdag/builtin/prompts/error_correction_prompts.py +320 -0
- hexdag/builtin/prompts/tool_prompts.py +160 -0
- hexdag/builtin/tools/builtin_tools.py +84 -0
- hexdag/builtin/tools/database_tools.py +164 -0
- hexdag/cli/__init__.py +17 -0
- hexdag/cli/__main__.py +7 -0
- hexdag/cli/commands/__init__.py +27 -0
- hexdag/cli/commands/build_cmd.py +812 -0
- hexdag/cli/commands/create_cmd.py +208 -0
- hexdag/cli/commands/docs_cmd.py +293 -0
- hexdag/cli/commands/generate_types_cmd.py +252 -0
- hexdag/cli/commands/init_cmd.py +188 -0
- hexdag/cli/commands/pipeline_cmd.py +494 -0
- hexdag/cli/commands/plugin_dev_cmd.py +529 -0
- hexdag/cli/commands/plugins_cmd.py +441 -0
- hexdag/cli/commands/studio_cmd.py +101 -0
- hexdag/cli/commands/validate_cmd.py +221 -0
- hexdag/cli/main.py +84 -0
- hexdag/core/__init__.py +83 -0
- hexdag/core/config/__init__.py +20 -0
- hexdag/core/config/loader.py +479 -0
- hexdag/core/config/models.py +150 -0
- hexdag/core/configurable.py +294 -0
- hexdag/core/context/__init__.py +37 -0
- hexdag/core/context/execution_context.py +378 -0
- hexdag/core/docs/__init__.py +26 -0
- hexdag/core/docs/extractors.py +678 -0
- hexdag/core/docs/generators.py +890 -0
- hexdag/core/docs/models.py +120 -0
- hexdag/core/domain/__init__.py +10 -0
- hexdag/core/domain/dag.py +1225 -0
- hexdag/core/exceptions.py +234 -0
- hexdag/core/expression_parser.py +569 -0
- hexdag/core/logging.py +449 -0
- hexdag/core/models/__init__.py +17 -0
- hexdag/core/models/base.py +138 -0
- hexdag/core/orchestration/__init__.py +46 -0
- hexdag/core/orchestration/body_executor.py +481 -0
- hexdag/core/orchestration/components/__init__.py +97 -0
- hexdag/core/orchestration/components/adapter_lifecycle_manager.py +113 -0
- hexdag/core/orchestration/components/checkpoint_manager.py +134 -0
- hexdag/core/orchestration/components/execution_coordinator.py +360 -0
- hexdag/core/orchestration/components/health_check_manager.py +176 -0
- hexdag/core/orchestration/components/input_mapper.py +143 -0
- hexdag/core/orchestration/components/lifecycle_manager.py +583 -0
- hexdag/core/orchestration/components/node_executor.py +377 -0
- hexdag/core/orchestration/components/secret_manager.py +202 -0
- hexdag/core/orchestration/components/wave_executor.py +158 -0
- hexdag/core/orchestration/constants.py +17 -0
- hexdag/core/orchestration/events/README.md +312 -0
- hexdag/core/orchestration/events/__init__.py +104 -0
- hexdag/core/orchestration/events/batching.py +330 -0
- hexdag/core/orchestration/events/decorators.py +139 -0
- hexdag/core/orchestration/events/events.py +573 -0
- hexdag/core/orchestration/events/observers/__init__.py +30 -0
- hexdag/core/orchestration/events/observers/core_observers.py +690 -0
- hexdag/core/orchestration/events/observers/models.py +111 -0
- hexdag/core/orchestration/events/taxonomy.py +269 -0
- hexdag/core/orchestration/hook_context.py +237 -0
- hexdag/core/orchestration/hooks.py +437 -0
- hexdag/core/orchestration/models.py +418 -0
- hexdag/core/orchestration/orchestrator.py +910 -0
- hexdag/core/orchestration/orchestrator_factory.py +275 -0
- hexdag/core/orchestration/port_wrappers.py +327 -0
- hexdag/core/orchestration/prompt/__init__.py +32 -0
- hexdag/core/orchestration/prompt/template.py +332 -0
- hexdag/core/pipeline_builder/__init__.py +21 -0
- hexdag/core/pipeline_builder/component_instantiator.py +386 -0
- hexdag/core/pipeline_builder/include_tag.py +265 -0
- hexdag/core/pipeline_builder/pipeline_config.py +133 -0
- hexdag/core/pipeline_builder/py_tag.py +223 -0
- hexdag/core/pipeline_builder/tag_discovery.py +268 -0
- hexdag/core/pipeline_builder/yaml_builder.py +1196 -0
- hexdag/core/pipeline_builder/yaml_validator.py +569 -0
- hexdag/core/ports/__init__.py +65 -0
- hexdag/core/ports/api_call.py +133 -0
- hexdag/core/ports/database.py +489 -0
- hexdag/core/ports/embedding.py +215 -0
- hexdag/core/ports/executor.py +237 -0
- hexdag/core/ports/file_storage.py +117 -0
- hexdag/core/ports/healthcheck.py +87 -0
- hexdag/core/ports/llm.py +551 -0
- hexdag/core/ports/memory.py +70 -0
- hexdag/core/ports/observer_manager.py +130 -0
- hexdag/core/ports/secret.py +145 -0
- hexdag/core/ports/tool_router.py +94 -0
- hexdag/core/ports_builder.py +623 -0
- hexdag/core/protocols.py +273 -0
- hexdag/core/resolver.py +304 -0
- hexdag/core/schema/__init__.py +9 -0
- hexdag/core/schema/generator.py +742 -0
- hexdag/core/secrets.py +242 -0
- hexdag/core/types.py +413 -0
- hexdag/core/utils/async_warnings.py +206 -0
- hexdag/core/utils/schema_conversion.py +78 -0
- hexdag/core/utils/sql_validation.py +86 -0
- hexdag/core/validation/secure_json.py +148 -0
- hexdag/core/yaml_macro.py +517 -0
- hexdag/mcp_server.py +3120 -0
- hexdag/studio/__init__.py +10 -0
- hexdag/studio/build_ui.py +92 -0
- hexdag/studio/server/__init__.py +1 -0
- hexdag/studio/server/main.py +100 -0
- hexdag/studio/server/routes/__init__.py +9 -0
- hexdag/studio/server/routes/execute.py +208 -0
- hexdag/studio/server/routes/export.py +558 -0
- hexdag/studio/server/routes/files.py +207 -0
- hexdag/studio/server/routes/plugins.py +419 -0
- hexdag/studio/server/routes/validate.py +220 -0
- hexdag/studio/ui/index.html +13 -0
- hexdag/studio/ui/package-lock.json +2992 -0
- hexdag/studio/ui/package.json +31 -0
- hexdag/studio/ui/postcss.config.js +6 -0
- hexdag/studio/ui/public/hexdag.svg +5 -0
- hexdag/studio/ui/src/App.tsx +251 -0
- hexdag/studio/ui/src/components/Canvas.tsx +408 -0
- hexdag/studio/ui/src/components/ContextMenu.tsx +187 -0
- hexdag/studio/ui/src/components/FileBrowser.tsx +123 -0
- hexdag/studio/ui/src/components/Header.tsx +181 -0
- hexdag/studio/ui/src/components/HexdagNode.tsx +193 -0
- hexdag/studio/ui/src/components/NodeInspector.tsx +512 -0
- hexdag/studio/ui/src/components/NodePalette.tsx +262 -0
- hexdag/studio/ui/src/components/NodePortsSection.tsx +403 -0
- hexdag/studio/ui/src/components/PluginManager.tsx +347 -0
- hexdag/studio/ui/src/components/PortsEditor.tsx +481 -0
- hexdag/studio/ui/src/components/PythonEditor.tsx +195 -0
- hexdag/studio/ui/src/components/ValidationPanel.tsx +105 -0
- hexdag/studio/ui/src/components/YamlEditor.tsx +196 -0
- hexdag/studio/ui/src/components/index.ts +8 -0
- hexdag/studio/ui/src/index.css +92 -0
- hexdag/studio/ui/src/main.tsx +10 -0
- hexdag/studio/ui/src/types/index.ts +123 -0
- hexdag/studio/ui/src/vite-env.d.ts +1 -0
- hexdag/studio/ui/tailwind.config.js +29 -0
- hexdag/studio/ui/tsconfig.json +37 -0
- hexdag/studio/ui/tsconfig.node.json +13 -0
- hexdag/studio/ui/vite.config.ts +35 -0
- hexdag/visualization/__init__.py +69 -0
- hexdag/visualization/dag_visualizer.py +1020 -0
- hexdag-0.5.0.dev1.dist-info/METADATA +369 -0
- hexdag-0.5.0.dev1.dist-info/RECORD +261 -0
- hexdag-0.5.0.dev1.dist-info/WHEEL +4 -0
- hexdag-0.5.0.dev1.dist-info/entry_points.txt +4 -0
- hexdag-0.5.0.dev1.dist-info/licenses/LICENSE +190 -0
- hexdag_plugins/.gitignore +43 -0
- hexdag_plugins/README.md +73 -0
- hexdag_plugins/__init__.py +1 -0
- hexdag_plugins/azure/LICENSE +21 -0
- hexdag_plugins/azure/README.md +414 -0
- hexdag_plugins/azure/__init__.py +21 -0
- hexdag_plugins/azure/azure_blob_adapter.py +450 -0
- hexdag_plugins/azure/azure_cosmos_adapter.py +383 -0
- hexdag_plugins/azure/azure_keyvault_adapter.py +314 -0
- hexdag_plugins/azure/azure_openai_adapter.py +415 -0
- hexdag_plugins/azure/pyproject.toml +107 -0
- hexdag_plugins/azure/tests/__init__.py +1 -0
- hexdag_plugins/azure/tests/test_azure_blob_adapter.py +350 -0
- hexdag_plugins/azure/tests/test_azure_cosmos_adapter.py +323 -0
- hexdag_plugins/azure/tests/test_azure_keyvault_adapter.py +330 -0
- hexdag_plugins/azure/tests/test_azure_openai_adapter.py +329 -0
- hexdag_plugins/hexdag_etl/README.md +168 -0
- hexdag_plugins/hexdag_etl/__init__.py +53 -0
- hexdag_plugins/hexdag_etl/examples/01_simple_pandas_transform.py +270 -0
- hexdag_plugins/hexdag_etl/examples/02_simple_pandas_only.py +149 -0
- hexdag_plugins/hexdag_etl/examples/03_file_io_pipeline.py +109 -0
- hexdag_plugins/hexdag_etl/examples/test_pandas_transform.py +84 -0
- hexdag_plugins/hexdag_etl/hexdag.toml +25 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/__init__.py +48 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/__init__.py +13 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/api_extract.py +230 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/base_node_factory.py +181 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/file_io.py +415 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/outlook.py +492 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/pandas_transform.py +563 -0
- hexdag_plugins/hexdag_etl/hexdag_etl/nodes/sql_extract_load.py +112 -0
- hexdag_plugins/hexdag_etl/pyproject.toml +82 -0
- hexdag_plugins/hexdag_etl/test_transform.py +54 -0
- hexdag_plugins/hexdag_etl/tests/test_plugin_integration.py +62 -0
- hexdag_plugins/mysql_adapter/LICENSE +21 -0
- hexdag_plugins/mysql_adapter/README.md +224 -0
- hexdag_plugins/mysql_adapter/__init__.py +6 -0
- hexdag_plugins/mysql_adapter/mysql_adapter.py +408 -0
- hexdag_plugins/mysql_adapter/pyproject.toml +93 -0
- hexdag_plugins/mysql_adapter/tests/test_mysql_adapter.py +259 -0
- hexdag_plugins/storage/README.md +184 -0
- hexdag_plugins/storage/__init__.py +19 -0
- hexdag_plugins/storage/file/__init__.py +5 -0
- hexdag_plugins/storage/file/local.py +325 -0
- hexdag_plugins/storage/ports/__init__.py +5 -0
- hexdag_plugins/storage/ports/vector_store.py +236 -0
- hexdag_plugins/storage/sql/__init__.py +7 -0
- hexdag_plugins/storage/sql/base.py +187 -0
- hexdag_plugins/storage/sql/mysql.py +27 -0
- hexdag_plugins/storage/sql/postgresql.py +27 -0
- hexdag_plugins/storage/tests/__init__.py +1 -0
- hexdag_plugins/storage/tests/test_local_file_storage.py +161 -0
- hexdag_plugins/storage/tests/test_sql_adapters.py +212 -0
- hexdag_plugins/storage/vector/__init__.py +7 -0
- hexdag_plugins/storage/vector/chromadb.py +223 -0
- hexdag_plugins/storage/vector/in_memory.py +285 -0
- hexdag_plugins/storage/vector/pgvector.py +502 -0
|
@@ -0,0 +1,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 {}
|