Flowfile 0.3.9__py3-none-any.whl → 0.5.1__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.
- flowfile/__init__.py +8 -1
- flowfile/api.py +1 -3
- flowfile/web/static/assets/{CloudConnectionManager-c97c25f8.js → CloudConnectionManager-0dfba9f2.js} +2 -2
- flowfile/web/static/assets/{CloudStorageReader-f1ff509e.js → CloudStorageReader-d5b1b6c9.js} +11 -78
- flowfile/web/static/assets/{CloudStorageWriter-034f8b78.js → CloudStorageWriter-00d87aad.js} +12 -79
- flowfile/web/static/assets/{CloudStorageWriter-49c9a4b2.css → CloudStorageWriter-b0ee067f.css} +24 -24
- flowfile/web/static/assets/ColumnSelector-4685e75d.js +83 -0
- flowfile/web/static/assets/ColumnSelector-47996a16.css +10 -0
- flowfile/web/static/assets/ContextMenu-23e909da.js +41 -0
- flowfile/web/static/assets/{SettingsSection-9c836ecc.css → ContextMenu-4c74eef1.css} +0 -21
- flowfile/web/static/assets/ContextMenu-63cfa99b.css +26 -0
- flowfile/web/static/assets/ContextMenu-70ae0c79.js +41 -0
- flowfile/web/static/assets/ContextMenu-c13f91d0.css +26 -0
- flowfile/web/static/assets/ContextMenu-f149cf7c.js +41 -0
- flowfile/web/static/assets/{CrossJoin-41efa4cb.css → CrossJoin-1119d18e.css} +18 -18
- flowfile/web/static/assets/{CrossJoin-9e156ebe.js → CrossJoin-702a3edd.js} +14 -84
- flowfile/web/static/assets/CustomNode-74a37f74.css +32 -0
- flowfile/web/static/assets/CustomNode-b1519993.js +211 -0
- flowfile/web/static/assets/{DatabaseConnectionSettings-d5c625b3.js → DatabaseConnectionSettings-6f3e4ea5.js} +3 -3
- flowfile/web/static/assets/{DatabaseManager-265adc5e.js → DatabaseManager-cf5ef661.js} +2 -2
- flowfile/web/static/assets/{DatabaseReader-f50c6558.css → DatabaseReader-ae61773c.css} +0 -27
- flowfile/web/static/assets/{DatabaseReader-0b10551e.js → DatabaseReader-d38c7295.js} +14 -114
- flowfile/web/static/assets/{DatabaseWriter-c17c6916.js → DatabaseWriter-b04ef46a.js} +13 -74
- flowfile/web/static/assets/{ExploreData-5bdae813.css → ExploreData-2d0cf4db.css} +8 -14
- flowfile/web/static/assets/ExploreData-5fa10ed8.js +192 -0
- flowfile/web/static/assets/{ExternalSource-3a66556c.js → ExternalSource-d39af878.js} +8 -79
- flowfile/web/static/assets/{Filter-91ad87e7.js → Filter-9b6d08db.js} +12 -85
- flowfile/web/static/assets/{Filter-a9d08ba1.css → Filter-f62091b3.css} +3 -3
- flowfile/web/static/assets/{Formula-3c395ab1.js → Formula-6b04fb1d.js} +20 -87
- flowfile/web/static/assets/{Formula-29f19d21.css → Formula-bb96803d.css} +4 -4
- flowfile/web/static/assets/{FuzzyMatch-6857de82.css → FuzzyMatch-1010f966.css} +42 -42
- flowfile/web/static/assets/{FuzzyMatch-2df0d230.js → FuzzyMatch-999521f4.js} +16 -87
- flowfile/web/static/assets/{GraphSolver-d285877f.js → GraphSolver-17dd2198.js} +13 -159
- flowfile/web/static/assets/GraphSolver-f0cb7bfb.css +22 -0
- flowfile/web/static/assets/{GroupBy-0bd1cc6b.js → GroupBy-6b039e18.js} +12 -75
- flowfile/web/static/assets/{Unique-b5615727.css → GroupBy-b9505323.css} +8 -8
- flowfile/web/static/assets/{Join-5a78a203.js → Join-24d0f113.js} +15 -85
- flowfile/web/static/assets/{Join-f45eff22.css → Join-fd79b451.css} +20 -20
- flowfile/web/static/assets/{ManualInput-a71b52c6.css → ManualInput-3246a08d.css} +20 -20
- flowfile/web/static/assets/{ManualInput-93aef9d6.js → ManualInput-34639209.js} +11 -82
- flowfile/web/static/assets/MultiSelect-0e8724a3.js +5 -0
- flowfile/web/static/assets/MultiSelect.vue_vue_type_script_setup_true_lang-b0e538c2.js +63 -0
- flowfile/web/static/assets/NumericInput-3d63a470.js +5 -0
- flowfile/web/static/assets/NumericInput.vue_vue_type_script_setup_true_lang-e0edeccc.js +35 -0
- flowfile/web/static/assets/Output-283fe388.css +37 -0
- flowfile/web/static/assets/{Output-411ecaee.js → Output-edea9802.js} +62 -273
- flowfile/web/static/assets/{Pivot-89db4b04.js → Pivot-61d19301.js} +14 -138
- flowfile/web/static/assets/Pivot-cf333e3d.css +22 -0
- flowfile/web/static/assets/PivotValidation-891ddfb0.css +13 -0
- flowfile/web/static/assets/PivotValidation-c46cd420.css +13 -0
- flowfile/web/static/assets/PivotValidation-de9f43fe.js +61 -0
- flowfile/web/static/assets/PivotValidation-f97fec5b.js +61 -0
- flowfile/web/static/assets/{PolarsCode-a9f974f8.js → PolarsCode-bc3c9984.js} +13 -80
- flowfile/web/static/assets/Read-64a3f259.js +218 -0
- flowfile/web/static/assets/Read-e808b239.css +62 -0
- flowfile/web/static/assets/RecordCount-3d5039be.js +53 -0
- flowfile/web/static/assets/{RecordId-55ae7d36.js → RecordId-597510e0.js} +8 -80
- flowfile/web/static/assets/SQLQueryComponent-36cef432.css +27 -0
- flowfile/web/static/assets/SQLQueryComponent-df51adbe.js +38 -0
- flowfile/web/static/assets/{Sample-b4a18476.js → Sample-4be0a507.js} +8 -77
- flowfile/web/static/assets/{SecretManager-b066d13a.js → SecretManager-4839be57.js} +2 -2
- flowfile/web/static/assets/{Select-727688dc.js → Select-9b72f201.js} +11 -85
- flowfile/web/static/assets/SettingsSection-2e4d03c4.css +21 -0
- flowfile/web/static/assets/SettingsSection-5c696bee.css +20 -0
- flowfile/web/static/assets/SettingsSection-71e6b7e3.css +21 -0
- flowfile/web/static/assets/SettingsSection-7ded385d.js +45 -0
- flowfile/web/static/assets/{SettingsSection-695ac487.js → SettingsSection-e1e9c953.js} +2 -40
- flowfile/web/static/assets/SettingsSection-f0f75a42.js +53 -0
- flowfile/web/static/assets/SingleSelect-6c777aac.js +5 -0
- flowfile/web/static/assets/SingleSelect.vue_vue_type_script_setup_true_lang-33e3ff9b.js +62 -0
- flowfile/web/static/assets/SliderInput-7cb93e62.js +40 -0
- flowfile/web/static/assets/SliderInput-b8fb6a8c.css +4 -0
- flowfile/web/static/assets/{GroupBy-ab1ea74b.css → Sort-3643d625.css} +8 -8
- flowfile/web/static/assets/{Sort-be3339a8.js → Sort-6cbde21a.js} +12 -97
- flowfile/web/static/assets/TextInput-d9a40c11.js +5 -0
- flowfile/web/static/assets/TextInput.vue_vue_type_script_setup_true_lang-5896c375.js +32 -0
- flowfile/web/static/assets/{TextToRows-c92d1ec2.css → TextToRows-5d2c1190.css} +9 -9
- flowfile/web/static/assets/{TextToRows-7b8998da.js → TextToRows-c4fcbf4d.js} +14 -83
- flowfile/web/static/assets/ToggleSwitch-4ef91d19.js +5 -0
- flowfile/web/static/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-38478c20.js +31 -0
- flowfile/web/static/assets/{UnavailableFields-8b0cb48e.js → UnavailableFields-a03f512c.js} +2 -2
- flowfile/web/static/assets/{Union-8d9ac7f9.css → Union-af6c3d9b.css} +6 -6
- flowfile/web/static/assets/Union-bfe9b996.js +77 -0
- flowfile/web/static/assets/{Unique-af5a80b4.js → Unique-5d023a27.js} +23 -104
- flowfile/web/static/assets/{Sort-7ccfa0fe.css → Unique-f9fb0809.css} +8 -8
- flowfile/web/static/assets/Unpivot-1e422df3.css +30 -0
- flowfile/web/static/assets/{Unpivot-5195d411.js → Unpivot-91cc5354.js} +12 -166
- flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +13 -0
- flowfile/web/static/assets/UnpivotValidation-7ee2de44.js +51 -0
- flowfile/web/static/assets/{ExploreData-18a4fe52.js → VueGraphicWalker-e51b9924.js} +4 -264
- flowfile/web/static/assets/VueGraphicWalker-ed5ab88b.css +6 -0
- flowfile/web/static/assets/{api-cb00cce6.js → api-c1bad5ca.js} +1 -1
- flowfile/web/static/assets/{api-023d1733.js → api-cf1221f0.js} +1 -1
- flowfile/web/static/assets/{designer-2197d782.css → designer-8da3ba3a.css} +859 -201
- flowfile/web/static/assets/{designer-6c322d8e.js → designer-9633482a.js} +2297 -733
- flowfile/web/static/assets/{documentation-4d1fafe1.js → documentation-ca400224.js} +1 -1
- flowfile/web/static/assets/{dropDown-0b46dd77.js → dropDown-614b998d.js} +1 -1
- flowfile/web/static/assets/{fullEditor-ec4e4f95.js → fullEditor-f7971590.js} +2 -2
- flowfile/web/static/assets/{genericNodeSettings-def5879b.js → genericNodeSettings-4fe5f36b.js} +3 -3
- flowfile/web/static/assets/{index-681a3ed0.css → index-50508d4d.css} +8 -0
- flowfile/web/static/assets/{index-683fc198.js → index-5429bbf8.js} +208 -31
- flowfile/web/static/assets/nodeInput-5d0d6b79.js +41 -0
- flowfile/web/static/assets/outputCsv-076b85ab.js +86 -0
- flowfile/web/static/assets/{Output-48f81019.css → outputCsv-9cc59e0b.css} +0 -143
- flowfile/web/static/assets/outputExcel-0fd17dbe.js +56 -0
- flowfile/web/static/assets/outputExcel-b41305c0.css +102 -0
- flowfile/web/static/assets/outputParquet-b61e0847.js +31 -0
- flowfile/web/static/assets/outputParquet-cf8cf3f2.css +4 -0
- flowfile/web/static/assets/readCsv-a8bb8b61.js +179 -0
- flowfile/web/static/assets/readCsv-c767cb37.css +52 -0
- flowfile/web/static/assets/readExcel-67b4aee0.js +201 -0
- flowfile/web/static/assets/readExcel-806d2826.css +64 -0
- flowfile/web/static/assets/readParquet-48c81530.css +19 -0
- flowfile/web/static/assets/readParquet-92ce1dbc.js +23 -0
- flowfile/web/static/assets/{secretApi-baceb6f9.js → secretApi-68435402.js} +1 -1
- flowfile/web/static/assets/{selectDynamic-de91449a.js → selectDynamic-92e25ee3.js} +7 -7
- flowfile/web/static/assets/{selectDynamic-b062bc9b.css → selectDynamic-aa913ff4.css} +16 -16
- flowfile/web/static/assets/user-defined-icon-0ae16c90.png +0 -0
- flowfile/web/static/assets/{vue-codemirror.esm-dc5e3348.js → vue-codemirror.esm-41b0e0d7.js} +65 -36
- flowfile/web/static/assets/{vue-content-loader.es-ba94b82f.js → vue-content-loader.es-2c8e608f.js} +1 -1
- flowfile/web/static/index.html +2 -2
- {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/METADATA +5 -3
- {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/RECORD +191 -121
- {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/WHEEL +1 -1
- {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/entry_points.txt +1 -0
- flowfile_core/__init__.py +3 -0
- flowfile_core/configs/flow_logger.py +5 -13
- flowfile_core/configs/node_store/__init__.py +30 -0
- flowfile_core/configs/node_store/nodes.py +383 -99
- flowfile_core/configs/node_store/user_defined_node_registry.py +193 -0
- flowfile_core/configs/settings.py +2 -1
- flowfile_core/database/connection.py +5 -21
- flowfile_core/fileExplorer/funcs.py +239 -121
- flowfile_core/flowfile/analytics/analytics_processor.py +1 -0
- flowfile_core/flowfile/code_generator/code_generator.py +62 -64
- flowfile_core/flowfile/flow_data_engine/create/funcs.py +73 -56
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +77 -86
- flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +4 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +19 -34
- flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +36 -0
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +23 -23
- flowfile_core/flowfile/flow_data_engine/join/utils.py +1 -1
- flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +9 -4
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +212 -86
- flowfile_core/flowfile/flow_data_engine/utils.py +2 -0
- flowfile_core/flowfile/flow_graph.py +240 -54
- flowfile_core/flowfile/flow_node/flow_node.py +48 -13
- flowfile_core/flowfile/flow_node/models.py +2 -1
- flowfile_core/flowfile/handler.py +24 -5
- flowfile_core/flowfile/manage/compatibility_enhancements.py +404 -41
- flowfile_core/flowfile/manage/io_flowfile.py +394 -0
- flowfile_core/flowfile/node_designer/__init__.py +47 -0
- flowfile_core/flowfile/node_designer/_type_registry.py +197 -0
- flowfile_core/flowfile/node_designer/custom_node.py +371 -0
- flowfile_core/flowfile/node_designer/ui_components.py +277 -0
- flowfile_core/flowfile/schema_callbacks.py +17 -10
- flowfile_core/flowfile/setting_generator/settings.py +15 -10
- flowfile_core/main.py +5 -1
- flowfile_core/routes/routes.py +73 -30
- flowfile_core/routes/user_defined_components.py +55 -0
- flowfile_core/schemas/cloud_storage_schemas.py +0 -2
- flowfile_core/schemas/input_schema.py +228 -65
- flowfile_core/schemas/output_model.py +5 -2
- flowfile_core/schemas/schemas.py +153 -35
- flowfile_core/schemas/transform_schema.py +1083 -412
- flowfile_core/schemas/yaml_types.py +103 -0
- flowfile_core/types.py +156 -0
- flowfile_core/utils/validate_setup.py +3 -1
- flowfile_frame/__init__.py +3 -1
- flowfile_frame/flow_frame.py +31 -24
- flowfile_frame/flow_frame_methods.py +12 -9
- flowfile_worker/__init__.py +9 -35
- flowfile_worker/create/__init__.py +3 -21
- flowfile_worker/create/funcs.py +68 -56
- flowfile_worker/create/models.py +130 -62
- flowfile_worker/main.py +5 -2
- flowfile_worker/routes.py +52 -13
- shared/__init__.py +15 -0
- shared/storage_config.py +258 -0
- tools/migrate/README.md +56 -0
- tools/migrate/__init__.py +12 -0
- tools/migrate/__main__.py +131 -0
- tools/migrate/legacy_schemas.py +621 -0
- tools/migrate/migrate.py +598 -0
- tools/migrate/tests/__init__.py +0 -0
- tools/migrate/tests/conftest.py +23 -0
- tools/migrate/tests/test_migrate.py +627 -0
- tools/migrate/tests/test_migration_e2e.py +1010 -0
- tools/migrate/tests/test_node_migrations.py +813 -0
- flowfile/web/static/assets/GraphSolver-17fd26db.css +0 -68
- flowfile/web/static/assets/Pivot-f415e85f.css +0 -35
- flowfile/web/static/assets/Read-80dc1675.css +0 -197
- flowfile/web/static/assets/Read-c3b1929c.js +0 -701
- flowfile/web/static/assets/RecordCount-4e95f98e.js +0 -122
- flowfile/web/static/assets/Union-89fd73dc.js +0 -146
- flowfile/web/static/assets/Unpivot-246e9bbd.css +0 -77
- flowfile/web/static/assets/nodeTitle-a16db7c3.js +0 -227
- flowfile/web/static/assets/nodeTitle-f4b12bcb.css +0 -134
- flowfile_core/flowfile/manage/open_flowfile.py +0 -135
- {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info/licenses}/LICENSE +0 -0
- /flowfile_core/flowfile/manage/manage_flowfile.py → /tools/__init__.py +0 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Fixed custom_node.py with proper type hints
|
|
2
|
+
|
|
3
|
+
import polars as pl
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from typing import Any, Dict, Optional, TypeVar
|
|
6
|
+
from flowfile_core.flowfile.node_designer.ui_components import FlowfileInComponent, IncomingColumns, Section
|
|
7
|
+
from flowfile_core.schemas.schemas import NodeTemplate, NodeTypeLiteral, TransformTypeLiteral
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def to_frontend_schema(model_instance: BaseModel) -> dict:
|
|
11
|
+
"""
|
|
12
|
+
Recursively converts a Pydantic model instance into a JSON-serializable
|
|
13
|
+
dictionary suitable for the frontend.
|
|
14
|
+
|
|
15
|
+
This function handles special marker classes like `IncomingColumns` and
|
|
16
|
+
nested `Section` and `FlowfileInComponent` instances.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
model_instance: The Pydantic model instance to convert.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
A dictionary representation of the model.
|
|
23
|
+
"""
|
|
24
|
+
result = {}
|
|
25
|
+
extra_fields = getattr(model_instance, '__pydantic_extra__', {})
|
|
26
|
+
model_fields = {k: getattr(model_instance, k) for k in model_instance.model_fields.keys()}
|
|
27
|
+
for key, value in (extra_fields|model_fields).items():
|
|
28
|
+
result[key] = _convert_value(value)
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _convert_value(value: Any) -> Any:
|
|
33
|
+
"""
|
|
34
|
+
Helper function to convert any value to a frontend-ready format.
|
|
35
|
+
"""
|
|
36
|
+
if isinstance(value, Section):
|
|
37
|
+
section_data = value.model_dump(
|
|
38
|
+
include={'title', 'description', 'hidden'},
|
|
39
|
+
exclude_none=True
|
|
40
|
+
)
|
|
41
|
+
section_data["component_type"] = "Section"
|
|
42
|
+
section_data["components"] = {
|
|
43
|
+
key: _convert_value(comp)
|
|
44
|
+
for key, comp in value.get_components().items()
|
|
45
|
+
}
|
|
46
|
+
return section_data
|
|
47
|
+
|
|
48
|
+
elif isinstance(value, FlowfileInComponent):
|
|
49
|
+
component_dict = value.model_dump(exclude_none=True)
|
|
50
|
+
if 'options' in component_dict:
|
|
51
|
+
if component_dict['options'] is IncomingColumns or (
|
|
52
|
+
isinstance(component_dict['options'], type) and
|
|
53
|
+
issubclass(component_dict['options'], IncomingColumns)
|
|
54
|
+
):
|
|
55
|
+
component_dict['options'] = {"__type__": "IncomingColumns"}
|
|
56
|
+
return component_dict
|
|
57
|
+
elif isinstance(value, BaseModel):
|
|
58
|
+
return to_frontend_schema(value)
|
|
59
|
+
elif isinstance(value, list):
|
|
60
|
+
return [_convert_value(item) for item in value]
|
|
61
|
+
elif isinstance(value, dict):
|
|
62
|
+
return {k: _convert_value(v) for k, v in value.items()}
|
|
63
|
+
elif isinstance(value, tuple):
|
|
64
|
+
return tuple(_convert_value(item) for item in value)
|
|
65
|
+
else:
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Type variable for the Section factory
|
|
70
|
+
T = TypeVar('T', bound=Section)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_section(**components: FlowfileInComponent) -> Section:
|
|
74
|
+
"""
|
|
75
|
+
Factory function to create a Section with proper type hints.
|
|
76
|
+
|
|
77
|
+
This is a convenience function that makes it easier to create `Section`
|
|
78
|
+
objects with autocomplete and type checking in modern editors.
|
|
79
|
+
|
|
80
|
+
Usage:
|
|
81
|
+
advanced_config_section = create_section(
|
|
82
|
+
case_sensitive=case_sensitive_toggle
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
**components: Keyword arguments where each key is the component name
|
|
87
|
+
and the value is a `FlowfileInComponent` instance.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A new `Section` instance containing the provided components.
|
|
91
|
+
"""
|
|
92
|
+
return Section(**components)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class NodeSettings(BaseModel):
|
|
96
|
+
"""
|
|
97
|
+
The top-level container for all sections in a node's UI.
|
|
98
|
+
|
|
99
|
+
This class holds all the `Section` objects that make up the settings panel
|
|
100
|
+
for a custom node.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
class MyNodeSettings(NodeSettings):
|
|
104
|
+
main_config = main_config_section
|
|
105
|
+
advanced_options = advanced_config_section
|
|
106
|
+
"""
|
|
107
|
+
class Config:
|
|
108
|
+
extra = 'allow'
|
|
109
|
+
arbitrary_types_allowed = True
|
|
110
|
+
|
|
111
|
+
def __init__(self, **sections):
|
|
112
|
+
"""
|
|
113
|
+
Initialize NodeSettings with sections as keyword arguments.
|
|
114
|
+
"""
|
|
115
|
+
super().__init__(**sections)
|
|
116
|
+
|
|
117
|
+
def populate_values(self, values: Dict[str, Any]) -> 'NodeSettings':
|
|
118
|
+
"""
|
|
119
|
+
Populates the settings with values received from the frontend.
|
|
120
|
+
|
|
121
|
+
This method is used internally to update the node's state based on
|
|
122
|
+
user input in the UI.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
values: A dictionary of values from the frontend, where keys are
|
|
126
|
+
section names and values are dictionaries of component
|
|
127
|
+
values.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The `NodeSettings` instance with updated component values.
|
|
131
|
+
"""
|
|
132
|
+
# Handle both extra fields and defined fields
|
|
133
|
+
all_sections = {}
|
|
134
|
+
|
|
135
|
+
# Get extra fields
|
|
136
|
+
extra_fields = getattr(self, '__pydantic_extra__', {})
|
|
137
|
+
all_sections.update(extra_fields)
|
|
138
|
+
|
|
139
|
+
# Get defined fields that are Sections
|
|
140
|
+
for field_name in self.model_fields:
|
|
141
|
+
field_value = getattr(self, field_name, None)
|
|
142
|
+
if isinstance(field_value, Section):
|
|
143
|
+
all_sections[field_name] = field_value
|
|
144
|
+
|
|
145
|
+
for section_name, section in all_sections.items():
|
|
146
|
+
if section_name in values:
|
|
147
|
+
section_values = values[section_name]
|
|
148
|
+
for component_name, component in section.get_components().items():
|
|
149
|
+
if component_name in section_values:
|
|
150
|
+
component.set_value(section_values[component_name])
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def create_node_settings(**sections: Section) -> NodeSettings:
|
|
155
|
+
"""
|
|
156
|
+
Factory function to create NodeSettings with proper type hints.
|
|
157
|
+
|
|
158
|
+
This is a convenience function for creating `NodeSettings` instances.
|
|
159
|
+
|
|
160
|
+
Usage:
|
|
161
|
+
FilterNodeSchema = create_node_settings(
|
|
162
|
+
main_config=main_config_section,
|
|
163
|
+
advanced_options=advanced_config_section
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
**sections: Keyword arguments where each key is the section name
|
|
168
|
+
and the value is a `Section` instance.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
A new `NodeSettings` instance containing the provided sections.
|
|
172
|
+
"""
|
|
173
|
+
return NodeSettings(**sections)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class SectionBuilder:
|
|
177
|
+
"""
|
|
178
|
+
A builder pattern for creating `Section` objects with proper type hints.
|
|
179
|
+
|
|
180
|
+
This provides a more fluent and readable way to construct complex sections,
|
|
181
|
+
especially when the number of components is large.
|
|
182
|
+
|
|
183
|
+
Usage:
|
|
184
|
+
builder = SectionBuilder(title="Advanced Settings")
|
|
185
|
+
builder.add_component("timeout", NumericInput(label="Timeout (s)"))
|
|
186
|
+
builder.add_component("retries", NumericInput(label="Number of Retries"))
|
|
187
|
+
advanced_section = builder.build()
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, title: Optional[str] = None, description: Optional[str] = None, hidden: bool = False):
|
|
191
|
+
self._section = Section(title=title, description=description, hidden=hidden)
|
|
192
|
+
|
|
193
|
+
def add_component(self, name: str, component: FlowfileInComponent) -> 'SectionBuilder':
|
|
194
|
+
"""Add a component to the section."""
|
|
195
|
+
setattr(self._section, name, component)
|
|
196
|
+
extra = getattr(self._section, '__pydantic_extra__', {})
|
|
197
|
+
extra[name] = component
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def build(self) -> Section:
|
|
201
|
+
"""Build and return the Section."""
|
|
202
|
+
return self._section
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class NodeSettingsBuilder:
|
|
206
|
+
"""
|
|
207
|
+
A builder pattern for creating `NodeSettings` objects.
|
|
208
|
+
|
|
209
|
+
Provides a fluent interface for constructing the entire settings schema
|
|
210
|
+
for a custom node.
|
|
211
|
+
|
|
212
|
+
Usage:
|
|
213
|
+
settings_builder = NodeSettingsBuilder()
|
|
214
|
+
settings_builder.add_section("main", main_section)
|
|
215
|
+
settings_builder.add_section("advanced", advanced_section)
|
|
216
|
+
my_node_settings = settings_builder.build()
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
def __init__(self):
|
|
220
|
+
self._settings = NodeSettings()
|
|
221
|
+
|
|
222
|
+
def add_section(self, name: str, section: Section) -> 'NodeSettingsBuilder':
|
|
223
|
+
"""Add a section to the node settings."""
|
|
224
|
+
setattr(self._settings, name, section)
|
|
225
|
+
extra = getattr(self._settings, '__pydantic_extra__', {})
|
|
226
|
+
extra[name] = section
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
def build(self) -> NodeSettings:
|
|
230
|
+
"""Build and return the NodeSettings."""
|
|
231
|
+
return self._settings
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class CustomNodeBase(BaseModel):
|
|
235
|
+
"""
|
|
236
|
+
The base class for creating a custom node in Flowfile.
|
|
237
|
+
|
|
238
|
+
To create a new node, you should inherit from this class and define its
|
|
239
|
+
attributes and the `process` method.
|
|
240
|
+
"""
|
|
241
|
+
# Core node properties
|
|
242
|
+
node_name: str
|
|
243
|
+
node_category: str = "Custom"
|
|
244
|
+
node_icon: str = "user-defined-icon.png"
|
|
245
|
+
settings_schema: Optional[NodeSettings] = None
|
|
246
|
+
|
|
247
|
+
# I/O configuration
|
|
248
|
+
number_of_inputs: int = 1
|
|
249
|
+
number_of_outputs: int = 1
|
|
250
|
+
|
|
251
|
+
# Display properties in the UI
|
|
252
|
+
node_group: Optional[str] = "custom"
|
|
253
|
+
title: Optional[str] = "Custom Node"
|
|
254
|
+
intro: Optional[str] = "A custom node for data processing"
|
|
255
|
+
|
|
256
|
+
# Behavior properties
|
|
257
|
+
node_type: NodeTypeLiteral = "process"
|
|
258
|
+
transform_type: TransformTypeLiteral = "wide"
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def item(self):
|
|
262
|
+
"""A unique identifier for the node, derived from its name."""
|
|
263
|
+
return self.node_name.replace(" ", "_").lower()
|
|
264
|
+
|
|
265
|
+
class Config:
|
|
266
|
+
arbitrary_types_allowed = True
|
|
267
|
+
|
|
268
|
+
def __init__(self, **data):
|
|
269
|
+
"""
|
|
270
|
+
Initialize the node, optionally populating settings from initial values.
|
|
271
|
+
"""
|
|
272
|
+
initial_values = data.pop('initial_values', None)
|
|
273
|
+
super().__init__(**data)
|
|
274
|
+
if self.settings_schema and initial_values:
|
|
275
|
+
self.settings_schema.populate_values(initial_values)
|
|
276
|
+
|
|
277
|
+
def get_frontend_schema(self) -> dict:
|
|
278
|
+
"""
|
|
279
|
+
Get the frontend-ready schema with current values.
|
|
280
|
+
|
|
281
|
+
This method is called by the backend to send the node's UI definition
|
|
282
|
+
and current state to the frontend.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
A dictionary representing the node's schema and values.
|
|
286
|
+
"""
|
|
287
|
+
schema = {
|
|
288
|
+
"node_name": self.node_name,
|
|
289
|
+
"node_category": self.node_category,
|
|
290
|
+
"node_icon": self.node_icon,
|
|
291
|
+
"number_of_inputs": self.number_of_inputs,
|
|
292
|
+
"number_of_outputs": self.number_of_outputs,
|
|
293
|
+
"node_group": self.node_group,
|
|
294
|
+
"title": self.title,
|
|
295
|
+
"intro": self.intro,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if self.settings_schema:
|
|
299
|
+
schema["settings_schema"] = to_frontend_schema(self.settings_schema)
|
|
300
|
+
else:
|
|
301
|
+
schema["settings_schema"] = {}
|
|
302
|
+
|
|
303
|
+
return schema
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def from_frontend_schema(cls, schema: dict) -> 'CustomNodeBase':
|
|
307
|
+
"""
|
|
308
|
+
Create a node instance from a frontend schema.
|
|
309
|
+
|
|
310
|
+
This is used when loading a node from a saved flow.
|
|
311
|
+
"""
|
|
312
|
+
settings_values = schema.pop('settings_schema', {})
|
|
313
|
+
node = cls(**schema)
|
|
314
|
+
if settings_values and node.settings_schema:
|
|
315
|
+
node.settings_schema.populate_values(settings_values)
|
|
316
|
+
return node
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def from_settings(cls, settings_values: dict) -> 'CustomNodeBase':
|
|
320
|
+
"""
|
|
321
|
+
Create a node instance with just its settings values.
|
|
322
|
+
|
|
323
|
+
Useful for creating a configured node instance programmatically.
|
|
324
|
+
"""
|
|
325
|
+
node = cls()
|
|
326
|
+
if settings_values and node.settings_schema:
|
|
327
|
+
node.settings_schema.populate_values(settings_values)
|
|
328
|
+
return node
|
|
329
|
+
|
|
330
|
+
def update_settings(self, values: Dict[str, Any]) -> 'CustomNodeBase':
|
|
331
|
+
"""
|
|
332
|
+
Update the settings with new values from the frontend.
|
|
333
|
+
"""
|
|
334
|
+
if self.settings_schema:
|
|
335
|
+
self.settings_schema.populate_values(values)
|
|
336
|
+
return self
|
|
337
|
+
|
|
338
|
+
def process(self, *inputs: pl.DataFrame) -> pl.DataFrame:
|
|
339
|
+
"""
|
|
340
|
+
The main data processing logic for the node.
|
|
341
|
+
|
|
342
|
+
This method must be implemented by all subclasses. It receives one or
|
|
343
|
+
more Polars DataFrames as input and should return a single DataFrame
|
|
344
|
+
as output.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
*inputs: A variable number of Polars DataFrames, corresponding to
|
|
348
|
+
the inputs connected to the node.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
A Polars DataFrame containing the processed data.
|
|
352
|
+
"""
|
|
353
|
+
raise NotImplementedError
|
|
354
|
+
|
|
355
|
+
def to_node_template(self) -> NodeTemplate:
|
|
356
|
+
"""
|
|
357
|
+
Convert the node to a `NodeTemplate` for storage or transmission.
|
|
358
|
+
"""
|
|
359
|
+
return NodeTemplate(
|
|
360
|
+
name=self.node_name,
|
|
361
|
+
item=self.item,
|
|
362
|
+
input=self.number_of_inputs,
|
|
363
|
+
output=self.number_of_outputs,
|
|
364
|
+
image=self.node_icon,
|
|
365
|
+
node_group=self.node_group,
|
|
366
|
+
drawer_title=self.title,
|
|
367
|
+
drawer_intro=self.intro,
|
|
368
|
+
node_type=self.node_type,
|
|
369
|
+
transform_type=self.transform_type,
|
|
370
|
+
custom_node=True
|
|
371
|
+
)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# ui_components.py - Updated ColumnSelector
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Any, Literal, Union, Type, Tuple, Dict
|
|
4
|
+
|
|
5
|
+
from pydantic import Field, BaseModel, computed_field
|
|
6
|
+
|
|
7
|
+
from flowfile_core.flowfile.node_designer._type_registry import normalize_type_spec
|
|
8
|
+
# Public API import
|
|
9
|
+
from flowfile_core.types import DataType, TypeSpec
|
|
10
|
+
|
|
11
|
+
InputType = Literal["text", "number", "secret", "array", "date", "boolean"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_input_to_data_types(
|
|
15
|
+
v: Any
|
|
16
|
+
) -> Union[Literal["ALL"], List[DataType]]:
|
|
17
|
+
"""
|
|
18
|
+
Normalizes a wide variety of inputs to either 'ALL' or a sorted list of DataType enums.
|
|
19
|
+
This function is used as a Pydantic BeforeValidator.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
v: The input value to normalize. Can be a string, a list of strings,
|
|
23
|
+
a DataType, a TypeGroup, or a list of those.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Either the string "ALL" or a sorted list of unique DataType enums.
|
|
27
|
+
"""
|
|
28
|
+
if v == "ALL":
|
|
29
|
+
return "ALL"
|
|
30
|
+
if isinstance(v, list) and all(isinstance(item, DataType) for item in v):
|
|
31
|
+
return v
|
|
32
|
+
|
|
33
|
+
normalized_set = normalize_type_spec(v)
|
|
34
|
+
|
|
35
|
+
if normalized_set == set(DataType):
|
|
36
|
+
return "ALL"
|
|
37
|
+
|
|
38
|
+
return sorted(list(normalized_set), key=lambda x: x.value)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FlowfileInComponent(BaseModel):
|
|
42
|
+
"""
|
|
43
|
+
Base class for all UI components in the node settings panel.
|
|
44
|
+
|
|
45
|
+
This class provides the common attributes and methods that all UI components share.
|
|
46
|
+
It's not meant to be used directly, but rather to be inherited by specific
|
|
47
|
+
component classes.
|
|
48
|
+
"""
|
|
49
|
+
component_type: str = Field(..., description="Type of the UI component")
|
|
50
|
+
value: Any = None
|
|
51
|
+
label: Optional[str] = None
|
|
52
|
+
input_type: InputType
|
|
53
|
+
|
|
54
|
+
def set_value(self, value: Any):
|
|
55
|
+
"""
|
|
56
|
+
Sets the value of the component, received from the frontend.
|
|
57
|
+
|
|
58
|
+
This method is used internally by the framework to populate the component's
|
|
59
|
+
value when a user interacts with the UI.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
value: The new value for the component.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The component instance with the updated value.
|
|
66
|
+
"""
|
|
67
|
+
self.value = value
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class IncomingColumns:
|
|
72
|
+
"""
|
|
73
|
+
A marker class used in `SingleSelect` and `MultiSelect` components.
|
|
74
|
+
|
|
75
|
+
When `options` is set to this class, the component will be dynamically
|
|
76
|
+
populated with the column names from the node's input dataframe.
|
|
77
|
+
This allows users to select from the available columns at runtime.
|
|
78
|
+
|
|
79
|
+
Example:
|
|
80
|
+
class MyNodeSettings(NodeSettings):
|
|
81
|
+
column_to_process = SingleSelect(
|
|
82
|
+
label="Select a column",
|
|
83
|
+
options=IncomingColumns
|
|
84
|
+
)
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ColumnSelector(FlowfileInComponent):
|
|
90
|
+
"""
|
|
91
|
+
A UI component that allows users to select one or more columns from the
|
|
92
|
+
input dataframe, with an optional filter based on column data types.
|
|
93
|
+
|
|
94
|
+
This is particularly useful when a node operation should only be applied
|
|
95
|
+
to columns of a specific type (e.g., numeric, string, date).
|
|
96
|
+
"""
|
|
97
|
+
component_type: Literal["ColumnSelector"] = "ColumnSelector"
|
|
98
|
+
required: bool = False
|
|
99
|
+
multiple: bool = False
|
|
100
|
+
input_type: InputType = "text"
|
|
101
|
+
|
|
102
|
+
# Normalized output: either "ALL" or list of DataType enums
|
|
103
|
+
data_type_filter_input: TypeSpec = Field(
|
|
104
|
+
default="ALL",
|
|
105
|
+
alias="data_types",
|
|
106
|
+
repr=False,
|
|
107
|
+
exclude=True
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
class Config:
|
|
111
|
+
arbitrary_types_allowed = True
|
|
112
|
+
|
|
113
|
+
@computed_field
|
|
114
|
+
@property
|
|
115
|
+
def data_types_filter(self) -> Union[Literal["ALL"], List[DataType]]:
|
|
116
|
+
"""
|
|
117
|
+
A computed field that normalizes the `data_type_filter_input` into a
|
|
118
|
+
standardized format for the frontend.
|
|
119
|
+
"""
|
|
120
|
+
return normalize_input_to_data_types(self.data_type_filter_input)
|
|
121
|
+
|
|
122
|
+
def model_dump(self, **kwargs) -> dict:
|
|
123
|
+
"""
|
|
124
|
+
Overrides the default `model_dump` to ensure `data_types` is in the
|
|
125
|
+
correct format for the frontend.
|
|
126
|
+
"""
|
|
127
|
+
data = super().model_dump(**kwargs)
|
|
128
|
+
if 'data_types_filter' in data and data['data_types_filter'] != "ALL":
|
|
129
|
+
data['data_types'] = sorted([dt.value for dt in data['data_types_filter']])
|
|
130
|
+
return data
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TextInput(FlowfileInComponent):
|
|
134
|
+
"""A standard text input field for capturing string values."""
|
|
135
|
+
component_type: Literal["TextInput"] = "TextInput"
|
|
136
|
+
default: Optional[str] = ""
|
|
137
|
+
placeholder: Optional[str] = ""
|
|
138
|
+
input_type: InputType = "text"
|
|
139
|
+
|
|
140
|
+
def __init__(self, **data):
|
|
141
|
+
super().__init__(**data)
|
|
142
|
+
if self.value is None and self.default is not None:
|
|
143
|
+
self.value = self.default
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class NumericInput(FlowfileInComponent):
|
|
147
|
+
"""A numeric input field with optional minimum and maximum value validation."""
|
|
148
|
+
component_type: Literal["NumericInput"] = "NumericInput"
|
|
149
|
+
default: Optional[float] = None
|
|
150
|
+
min_value: Optional[float] = None
|
|
151
|
+
max_value: Optional[float] = None
|
|
152
|
+
input_type: InputType = "number"
|
|
153
|
+
|
|
154
|
+
def __init__(self, **data):
|
|
155
|
+
super().__init__(**data)
|
|
156
|
+
if self.value is None and self.default is not None:
|
|
157
|
+
self.value = self.default
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ToggleSwitch(FlowfileInComponent):
|
|
161
|
+
"""A boolean toggle switch, typically used for enabling or disabling a feature."""
|
|
162
|
+
component_type: Literal["ToggleSwitch"] = "ToggleSwitch"
|
|
163
|
+
default: bool = False
|
|
164
|
+
description: Optional[str] = None
|
|
165
|
+
input_type: InputType = "boolean"
|
|
166
|
+
|
|
167
|
+
def __init__(self, **data):
|
|
168
|
+
super().__init__(**data)
|
|
169
|
+
if self.value is None:
|
|
170
|
+
self.value = self.default
|
|
171
|
+
|
|
172
|
+
def __bool__(self):
|
|
173
|
+
"""Allows the component instance to be evaluated as a boolean."""
|
|
174
|
+
return bool(self.value)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class SingleSelect(FlowfileInComponent):
|
|
178
|
+
"""
|
|
179
|
+
A dropdown menu for selecting a single option from a list.
|
|
180
|
+
|
|
181
|
+
The options can be a static list of strings or tuples, or they can be
|
|
182
|
+
dynamically populated from the input dataframe's columns by using the
|
|
183
|
+
`IncomingColumns` marker.
|
|
184
|
+
"""
|
|
185
|
+
component_type: Literal["SingleSelect"] = "SingleSelect"
|
|
186
|
+
options: Union[List[Union[str, Tuple[str, Any]]], Type[IncomingColumns]]
|
|
187
|
+
default: Optional[Any] = None
|
|
188
|
+
input_type: InputType = "text"
|
|
189
|
+
|
|
190
|
+
def __init__(self, **data):
|
|
191
|
+
super().__init__(**data)
|
|
192
|
+
if self.value is None and self.default is not None:
|
|
193
|
+
self.value = self.default
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class MultiSelect(FlowfileInComponent):
|
|
197
|
+
"""
|
|
198
|
+
A multi-select dropdown for choosing multiple options from a list.
|
|
199
|
+
|
|
200
|
+
Like `SingleSelect`, the options can be static or dynamically populated
|
|
201
|
+
from the input columns using the `IncomingColumns` marker.
|
|
202
|
+
"""
|
|
203
|
+
component_type: Literal["MultiSelect"] = "MultiSelect"
|
|
204
|
+
options: Union[List[Union[str, Tuple[str, Any]]], Type[IncomingColumns]]
|
|
205
|
+
default: List[Any] = Field(default_factory=list)
|
|
206
|
+
input_type: InputType = "array"
|
|
207
|
+
|
|
208
|
+
def __init__(self, **data):
|
|
209
|
+
super().__init__(**data)
|
|
210
|
+
if self.value is None:
|
|
211
|
+
self.value = self.default if self.default else []
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Section(BaseModel):
|
|
215
|
+
"""
|
|
216
|
+
A container for grouping related UI components in the node settings panel.
|
|
217
|
+
|
|
218
|
+
Sections help organize the UI by grouping components under a common title
|
|
219
|
+
and description. Components can be added as keyword arguments during
|
|
220
|
+
initialization or afterward.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
main_section = Section(
|
|
224
|
+
title="Main Settings",
|
|
225
|
+
description="Configure the primary behavior of the node.",
|
|
226
|
+
my_text_input=TextInput(label="Enter a value")
|
|
227
|
+
)
|
|
228
|
+
"""
|
|
229
|
+
title: Optional[str] = None
|
|
230
|
+
description: Optional[str] = None
|
|
231
|
+
hidden: bool = False
|
|
232
|
+
|
|
233
|
+
class Config:
|
|
234
|
+
extra = 'allow'
|
|
235
|
+
arbitrary_types_allowed = True
|
|
236
|
+
|
|
237
|
+
def __init__(self, **data):
|
|
238
|
+
"""
|
|
239
|
+
Initialize a Section with components as keyword arguments.
|
|
240
|
+
"""
|
|
241
|
+
super().__init__(**data)
|
|
242
|
+
|
|
243
|
+
def __call__(self, **kwargs) -> 'Section':
|
|
244
|
+
"""
|
|
245
|
+
Allows adding components to the section after initialization.
|
|
246
|
+
|
|
247
|
+
This makes it possible to build up a section dynamically.
|
|
248
|
+
"""
|
|
249
|
+
for key, value in kwargs.items():
|
|
250
|
+
setattr(self, key, value)
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def get_components(self) -> Dict[str, FlowfileInComponent]:
|
|
254
|
+
"""
|
|
255
|
+
Get all FlowfileInComponent instances from the section.
|
|
256
|
+
|
|
257
|
+
This method collects all the UI components that have been added to the
|
|
258
|
+
section, whether as defined fields or as extra fields.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
A dictionary mapping component names to their instances.
|
|
262
|
+
"""
|
|
263
|
+
components = {}
|
|
264
|
+
|
|
265
|
+
# Get from extra fields
|
|
266
|
+
for key, value in getattr(self, '__pydantic_extra__', {}).items():
|
|
267
|
+
if isinstance(value, FlowfileInComponent):
|
|
268
|
+
components[key] = value
|
|
269
|
+
|
|
270
|
+
# Get from defined fields (excluding metadata)
|
|
271
|
+
for field_name in self.model_fields:
|
|
272
|
+
if field_name not in {'title', 'description', 'hidden'}:
|
|
273
|
+
value = getattr(self, field_name, None)
|
|
274
|
+
if isinstance(value, FlowfileInComponent):
|
|
275
|
+
components[field_name] = value
|
|
276
|
+
|
|
277
|
+
return components
|