Flowfile 0.3.10__py3-none-any.whl → 0.4.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.
Files changed (111) hide show
  1. flowfile/__init__.py +6 -1
  2. flowfile/api.py +1 -1
  3. flowfile/web/static/assets/{CloudConnectionManager-d7c2c028.js → CloudConnectionManager-d3248f8d.js} +2 -2
  4. flowfile/web/static/assets/{CloudStorageReader-d467329f.js → CloudStorageReader-d65bf041.js} +6 -6
  5. flowfile/web/static/assets/{CloudStorageWriter-071b8b00.js → CloudStorageWriter-e83be3ed.js} +6 -6
  6. flowfile/web/static/assets/ColumnSelector-47996a16.css +10 -0
  7. flowfile/web/static/assets/ColumnSelector-cce661cf.js +83 -0
  8. flowfile/web/static/assets/{ContextMenu-a51e19ea.js → ContextMenu-11a4652a.js} +1 -1
  9. flowfile/web/static/assets/{ContextMenu-2dea5e27.js → ContextMenu-160afb08.js} +1 -1
  10. flowfile/web/static/assets/{ContextMenu-785554c4.js → ContextMenu-cf18d2cc.js} +1 -1
  11. flowfile/web/static/assets/{CrossJoin-cf68ec7a.js → CrossJoin-d395d38c.js} +7 -7
  12. flowfile/web/static/assets/CustomNode-74a37f74.css +32 -0
  13. flowfile/web/static/assets/CustomNode-b812dc0b.js +211 -0
  14. flowfile/web/static/assets/{DatabaseConnectionSettings-435c5dd8.js → DatabaseConnectionSettings-7000bf2c.js} +2 -2
  15. flowfile/web/static/assets/{DatabaseManager-349e33a8.js → DatabaseManager-9662ec5b.js} +2 -2
  16. flowfile/web/static/assets/{DatabaseReader-8075bd28.js → DatabaseReader-4f035d0c.js} +9 -9
  17. flowfile/web/static/assets/{DatabaseWriter-3e2dda89.js → DatabaseWriter-f65dcd54.js} +8 -8
  18. flowfile/web/static/assets/{ExploreData-76ec698c.js → ExploreData-94c43dfc.js} +5 -5
  19. flowfile/web/static/assets/{ExternalSource-609a265c.js → ExternalSource-ac04b3cc.js} +5 -5
  20. flowfile/web/static/assets/{Filter-97cff793.js → Filter-812dcbca.js} +7 -7
  21. flowfile/web/static/assets/{Formula-09de0ec9.js → Formula-71472193.js} +9 -9
  22. flowfile/web/static/assets/{FuzzyMatch-bdf70248.js → FuzzyMatch-b317f631.js} +8 -8
  23. flowfile/web/static/assets/{GraphSolver-0b5a0e05.js → GraphSolver-754a234f.js} +6 -6
  24. flowfile/web/static/assets/{GroupBy-eaddadde.js → GroupBy-6c6f9802.js} +5 -5
  25. flowfile/web/static/assets/{Join-3313371b.js → Join-a1b800be.js} +8 -8
  26. flowfile/web/static/assets/{ManualInput-e8bfc0be.js → ManualInput-a9640276.js} +4 -4
  27. flowfile/web/static/assets/MultiSelect-97213888.js +5 -0
  28. flowfile/web/static/assets/MultiSelect.vue_vue_type_script_setup_true_lang-6ffe088a.js +63 -0
  29. flowfile/web/static/assets/NumericInput-e638088a.js +5 -0
  30. flowfile/web/static/assets/NumericInput.vue_vue_type_script_setup_true_lang-90eb2cba.js +35 -0
  31. flowfile/web/static/assets/{Output-7303bb09.js → Output-76750610.js} +6 -6
  32. flowfile/web/static/assets/{Pivot-3b1c54ef.js → Pivot-7814803f.js} +7 -7
  33. flowfile/web/static/assets/{PivotValidation-3bb36c8f.js → PivotValidation-76dd431a.js} +1 -1
  34. flowfile/web/static/assets/{PivotValidation-eaa819c0.js → PivotValidation-f92137d2.js} +1 -1
  35. flowfile/web/static/assets/{PolarsCode-aa12e25d.js → PolarsCode-889c3008.js} +5 -5
  36. flowfile/web/static/assets/{Read-a2bfc618.js → Read-637b72a7.js} +8 -8
  37. flowfile/web/static/assets/{RecordCount-aa0dc082.js → RecordCount-2b050c41.js} +4 -4
  38. flowfile/web/static/assets/{RecordId-48ee1a3b.js → RecordId-81df7784.js} +6 -6
  39. flowfile/web/static/assets/{SQLQueryComponent-e149dbf2.js → SQLQueryComponent-88dcfe53.js} +1 -1
  40. flowfile/web/static/assets/{Sample-f06cb97a.js → Sample-258ad2a9.js} +4 -4
  41. flowfile/web/static/assets/{SecretManager-37f34886.js → SecretManager-2a2cb7e2.js} +2 -2
  42. flowfile/web/static/assets/{Select-b60e6c47.js → Select-850215fd.js} +7 -7
  43. flowfile/web/static/assets/{SettingsSection-75b6cf4f.js → SettingsSection-0e8d9123.js} +1 -1
  44. flowfile/web/static/assets/{SettingsSection-e57a672e.js → SettingsSection-29b4fa6b.js} +1 -1
  45. flowfile/web/static/assets/{SettingsSection-70e5a7b1.js → SettingsSection-55bae608.js} +1 -1
  46. flowfile/web/static/assets/SingleSelect-bebd408b.js +5 -0
  47. flowfile/web/static/assets/SingleSelect.vue_vue_type_script_setup_true_lang-6093741c.js +62 -0
  48. flowfile/web/static/assets/SliderInput-6a05ab61.js +40 -0
  49. flowfile/web/static/assets/SliderInput-b8fb6a8c.css +4 -0
  50. flowfile/web/static/assets/{Sort-51b1ee4d.js → Sort-10ab48ed.js} +5 -5
  51. flowfile/web/static/assets/TextInput-df9d6259.js +5 -0
  52. flowfile/web/static/assets/TextInput.vue_vue_type_script_setup_true_lang-000e1178.js +32 -0
  53. flowfile/web/static/assets/{TextToRows-26835f8f.js → TextToRows-6c2d93d8.js} +7 -7
  54. flowfile/web/static/assets/ToggleSwitch-0ff7ac52.js +5 -0
  55. flowfile/web/static/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-c6dc3029.js +31 -0
  56. flowfile/web/static/assets/{UnavailableFields-88a4cd0c.js → UnavailableFields-1bab97cb.js} +2 -2
  57. flowfile/web/static/assets/{Union-4d0088eb.js → Union-b563478a.js} +4 -4
  58. flowfile/web/static/assets/{Unique-7d554a62.js → Unique-f90db5db.js} +7 -7
  59. flowfile/web/static/assets/{Unpivot-4668595c.js → Unpivot-bcb0025f.js} +6 -6
  60. flowfile/web/static/assets/{UnpivotValidation-d4f0e0e8.js → UnpivotValidation-c4e73b04.js} +1 -1
  61. flowfile/web/static/assets/{VueGraphicWalker-5324d566.js → VueGraphicWalker-bb8535e2.js} +1 -1
  62. flowfile/web/static/assets/{api-271ed117.js → api-2d6adc4f.js} +1 -1
  63. flowfile/web/static/assets/{api-31e4fea6.js → api-4c8e3822.js} +1 -1
  64. flowfile/web/static/assets/{designer-091bdc3f.css → designer-e3c150ec.css} +41 -41
  65. flowfile/web/static/assets/{designer-bf3d9487.js → designer-f3656d8c.js} +23 -15
  66. flowfile/web/static/assets/{documentation-4d0a1cea.js → documentation-52b241e7.js} +1 -1
  67. flowfile/web/static/assets/{dropDown-025888df.js → dropDown-1bca8a74.js} +1 -1
  68. flowfile/web/static/assets/{fullEditor-1df991ec.js → fullEditor-2985687e.js} +2 -2
  69. flowfile/web/static/assets/{genericNodeSettings-d3b2b2ac.js → genericNodeSettings-0476ba4e.js} +3 -3
  70. flowfile/web/static/assets/{index-d0518598.js → index-246f201c.js} +6 -6
  71. flowfile/web/static/assets/{index-681a3ed0.css → index-50508d4d.css} +8 -0
  72. flowfile/web/static/assets/{outputCsv-d8457527.js → outputCsv-d686eeaf.js} +1 -1
  73. flowfile/web/static/assets/{outputExcel-be89153e.js → outputExcel-8809ea2f.js} +1 -1
  74. flowfile/web/static/assets/{outputParquet-fabb445a.js → outputParquet-53ba645a.js} +1 -1
  75. flowfile/web/static/assets/{readCsv-e8359522.js → readCsv-053bf97b.js} +1 -1
  76. flowfile/web/static/assets/{readExcel-dabaf51b.js → readExcel-ad531eab.js} +3 -3
  77. flowfile/web/static/assets/{readParquet-e0771ef2.js → readParquet-58e899a1.js} +1 -1
  78. flowfile/web/static/assets/{secretApi-ce823eee.js → secretApi-538058f3.js} +1 -1
  79. flowfile/web/static/assets/{selectDynamic-5476546e.js → selectDynamic-b38de2ba.js} +3 -3
  80. flowfile/web/static/assets/user-defined-icon-0ae16c90.png +0 -0
  81. flowfile/web/static/assets/{vue-codemirror.esm-9ed00d50.js → vue-codemirror.esm-db9b8936.js} +33 -3
  82. flowfile/web/static/assets/{vue-content-loader.es-7bca2d9b.js → vue-content-loader.es-b5f3ac30.js} +1 -1
  83. flowfile/web/static/index.html +2 -2
  84. {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/METADATA +3 -2
  85. {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/RECORD +111 -85
  86. {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/WHEEL +1 -1
  87. flowfile_core/configs/node_store/__init__.py +30 -0
  88. flowfile_core/configs/node_store/nodes.py +383 -358
  89. flowfile_core/configs/node_store/user_defined_node_registry.py +193 -0
  90. flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +4 -0
  91. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +19 -34
  92. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +36 -0
  93. flowfile_core/flowfile/flow_graph.py +20 -1
  94. flowfile_core/flowfile/flow_node/flow_node.py +4 -4
  95. flowfile_core/flowfile/manage/open_flowfile.py +9 -1
  96. flowfile_core/flowfile/node_designer/__init__.py +47 -0
  97. flowfile_core/flowfile/node_designer/_type_registry.py +197 -0
  98. flowfile_core/flowfile/node_designer/custom_node.py +371 -0
  99. flowfile_core/flowfile/node_designer/data_types.py +146 -0
  100. flowfile_core/flowfile/node_designer/ui_components.py +277 -0
  101. flowfile_core/flowfile/schema_callbacks.py +1 -1
  102. flowfile_core/main.py +2 -1
  103. flowfile_core/routes/routes.py +16 -20
  104. flowfile_core/routes/user_defined_components.py +55 -0
  105. flowfile_core/schemas/input_schema.py +8 -1
  106. flowfile_core/schemas/schemas.py +6 -3
  107. flowfile_core/utils/validate_setup.py +3 -1
  108. flowfile_frame/flow_frame.py +16 -6
  109. shared/storage_config.py +17 -2
  110. {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/entry_points.txt +0 -0
  111. {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,193 @@
1
+ import sys
2
+ import importlib.util
3
+ import inspect
4
+ from pathlib import Path
5
+ from typing import Dict, Type, List
6
+ import logging
7
+
8
+ from flowfile_core.flowfile.node_designer.custom_node import CustomNodeBase, NodeSettings
9
+ from shared import storage
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def get_all_custom_nodes() -> Dict[str, Type[CustomNodeBase]]:
16
+ """
17
+ Scan the user-defined nodes directory and import all CustomNodeBase subclasses.
18
+
19
+ Returns:
20
+ Dictionary mapping node names to node classes
21
+ """
22
+ custom_nodes = {}
23
+
24
+ # Get the directory path where user-defined nodes are stored
25
+ nodes_directory = storage.user_defined_nodes_icons
26
+
27
+ # Convert to Path object for easier handling
28
+ nodes_path = Path(nodes_directory)
29
+
30
+ if not nodes_path.exists() or not nodes_path.is_dir():
31
+ print(f"Warning: Nodes directory {nodes_path} does not exist or is not a directory")
32
+ return custom_nodes
33
+
34
+ # Scan all Python files in the directory
35
+ for file_path in nodes_path.glob("*.py"):
36
+ # Skip __init__.py and other special files
37
+ if file_path.name.startswith("__"):
38
+ continue
39
+
40
+ try:
41
+ # Load the module dynamically
42
+ module_name = file_path.stem # filename without extension
43
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
44
+
45
+ if spec and spec.loader:
46
+ module = importlib.util.module_from_spec(spec)
47
+
48
+ # Add to sys.modules to handle imports within the module
49
+ sys.modules[module_name] = module
50
+
51
+ # Execute the module
52
+ spec.loader.exec_module(module)
53
+
54
+ # Inspect the module for CustomNodeBase subclasses
55
+ for name, obj in inspect.getmembers(module):
56
+ # Check if it's a class and a subclass of CustomNodeBase
57
+ # but not CustomNodeBase itself
58
+ if (inspect.isclass(obj) and
59
+ issubclass(obj, CustomNodeBase) and
60
+ obj is not CustomNodeBase):
61
+
62
+ # Use the node_name attribute if it exists, otherwise use class name
63
+ node_name = getattr(obj, 'node_name', name)
64
+ custom_nodes[node_name] = obj
65
+ print(f"Loaded custom node: {node_name} from {file_path.name}")
66
+
67
+ except Exception as e:
68
+ print(f"Error loading module from {file_path}: {e}")
69
+ # Continue with other files even if one fails
70
+ continue
71
+
72
+ return custom_nodes
73
+
74
+
75
+ def get_all_custom_nodes_with_validation() -> Dict[str, Type[CustomNodeBase]]:
76
+ """
77
+ Enhanced version that validates the nodes before adding them.
78
+ """
79
+
80
+ custom_nodes = {}
81
+ nodes_path = storage.user_defined_nodes_directory
82
+
83
+ if not nodes_path.exists():
84
+ return custom_nodes
85
+
86
+ for file_path in nodes_path.glob("*.py"):
87
+ if file_path.name.startswith("__"):
88
+ continue
89
+
90
+ try:
91
+ module_name = file_path.stem
92
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
93
+
94
+ if spec and spec.loader:
95
+ module = importlib.util.module_from_spec(spec)
96
+ sys.modules[module_name] = module
97
+ spec.loader.exec_module(module)
98
+
99
+ for name, obj in inspect.getmembers(module):
100
+ if (inspect.isclass(obj) and
101
+ issubclass(obj, CustomNodeBase) and
102
+ obj is not CustomNodeBase):
103
+
104
+ try:
105
+ _obj = obj()
106
+ # Validate that the node has required attributes
107
+ if not hasattr(_obj, 'node_name'):
108
+ logger.error(f"Warning: {name} missing node_name attribute")
109
+ raise ValueError(f"Node {name} must implement a node_name attribute")
110
+
111
+ if not hasattr(_obj, 'settings_schema'):
112
+ logger.error(f"Warning: {name} missing settings_schema attribute")
113
+ raise ValueError(f"Node {name} must implement a settings_schema attribute")
114
+
115
+ if not hasattr(_obj, 'process'):
116
+ logger.error(f"Warning: {name} missing process method")
117
+ raise ValueError(f"Node {name} must implement a process method")
118
+ if not (storage.user_defined_nodes_icons / _obj.node_icon).exists():
119
+ logger.warning(
120
+ f"Warning: Icon file does not exist for node {_obj.node_name} at {_obj.node_icon} "
121
+ "Falling back to default icon."
122
+ )
123
+
124
+ node_name = _obj.to_node_template().item
125
+ custom_nodes[node_name] = obj
126
+ print(f"✓ Loaded: {node_name} from {file_path.name}")
127
+ except Exception as e:
128
+ print(f"Error validating node {name} in {file_path}: {e}")
129
+ continue
130
+ except SyntaxError as e:
131
+ print(f"Syntax error in {file_path}: {e}")
132
+ except ImportError as e:
133
+ print(f"Import error in {file_path}: {e}")
134
+ except Exception as e:
135
+ print(f"Unexpected error loading {file_path}: {e}")
136
+
137
+ return custom_nodes
138
+
139
+
140
+ def get_custom_nodes_lazy() -> List[Type[CustomNodeBase]]:
141
+ """
142
+ Returns a list of custom node classes without instantiating them.
143
+ Useful for registration or catalog purposes.
144
+ """
145
+ nodes = []
146
+ nodes_path = Path(storage.user_defined_nodes_directory)
147
+
148
+ if not nodes_path.exists():
149
+ return nodes
150
+
151
+ for file_path in nodes_path.glob("*.py"):
152
+ if file_path.name.startswith("__"):
153
+ continue
154
+
155
+ try:
156
+ # Create a unique module name to avoid conflicts
157
+ module_name = f"custom_node_{file_path.stem}_{id(file_path)}"
158
+ spec = importlib.util.spec_from_file_location(module_name, file_path)
159
+
160
+ if spec and spec.loader:
161
+ module = importlib.util.module_from_spec(spec)
162
+ spec.loader.exec_module(module)
163
+
164
+ for name, obj in inspect.getmembers(module):
165
+ if (inspect.isclass(obj) and
166
+ issubclass(obj, CustomNodeBase) and
167
+ obj is not CustomNodeBase and
168
+ obj.__module__ == module.__name__): # Only get classes defined in this module
169
+ nodes.append(obj)
170
+
171
+ except Exception as e:
172
+ print(f"Error processing {file_path}: {e}")
173
+ continue
174
+
175
+ return nodes
176
+
177
+
178
+ # Example usage function that matches your original pattern
179
+ def add_custom_node(node_class: Type[CustomNodeBase], registry: Dict[str, Type[CustomNodeBase]]):
180
+ """Add a single custom node to the registry."""
181
+ if hasattr(node_class, 'node_name'):
182
+ registry[node_class.node_name] = node_class
183
+ else:
184
+ registry[node_class.__name__] = node_class
185
+
186
+
187
+ def get_all_nodes_from_standard_location() -> Dict[str, Type[CustomNodeBase]]:
188
+ """
189
+ Main function to get all custom nodes from the standard location.
190
+ This matches your original function signature.
191
+ """
192
+
193
+ return get_all_custom_nodes_with_validation()
@@ -0,0 +1,4 @@
1
+ from typing import Literal
2
+
3
+ DataTypeGroup = Literal['numeric', 'str', 'date']
4
+ ReadableDataTypeGroup = Literal['Numeric', 'String', 'Date', 'Other', 'Boolean', 'Binary', 'Complex']
@@ -1,44 +1,13 @@
1
1
 
2
2
  from dataclasses import dataclass
3
- from typing import Optional, Any, List, Dict, Literal, Iterable
3
+ from typing import Optional, Any, List, Dict, Iterable
4
4
 
5
5
  from flowfile_core.schemas import input_schema
6
6
  from flowfile_core.flowfile.flow_data_engine.flow_file_column.utils import cast_str_to_polars_type
7
7
  from flowfile_core.flowfile.flow_data_engine.flow_file_column.polars_type import PlType
8
+ from flowfile_core.flowfile.flow_data_engine.flow_file_column.interface import ReadableDataTypeGroup, DataTypeGroup
9
+ from flowfile_core.flowfile.flow_data_engine.flow_file_column.type_registry import convert_pl_type_to_string
8
10
  import polars as pl
9
- # TODO: rename flow_file_column to flowfile_column
10
- DataTypeGroup = Literal['numeric', 'str', 'date']
11
-
12
-
13
- def convert_pl_type_to_string(pl_type: pl.DataType, inner: bool = False) -> str:
14
- if isinstance(pl_type, pl.List):
15
- inner_str = convert_pl_type_to_string(pl_type.inner, inner=True)
16
- return f"pl.List({inner_str})"
17
- elif isinstance(pl_type, pl.Array):
18
- inner_str = convert_pl_type_to_string(pl_type.inner, inner=True)
19
- return f"pl.Array({inner_str})"
20
- elif isinstance(pl_type, pl.Decimal):
21
- precision = pl_type.precision if hasattr(pl_type, 'precision') else None
22
- scale = pl_type.scale if hasattr(pl_type, 'scale') else None
23
- if precision is not None and scale is not None:
24
- return f"pl.Decimal({precision}, {scale})"
25
- elif precision is not None:
26
- return f"pl.Decimal({precision})"
27
- else:
28
- return "pl.Decimal()"
29
- elif isinstance(pl_type, pl.Struct):
30
- # Handle Struct with field definitions
31
- fields = []
32
- if hasattr(pl_type, 'fields'):
33
- for field in pl_type.fields:
34
- field_name = field.name
35
- field_type = convert_pl_type_to_string(field.dtype, inner=True)
36
- fields.append(f'pl.Field("{field_name}", {field_type})')
37
- field_str = ", ".join(fields)
38
- return f"pl.Struct([{field_str}])"
39
- else:
40
- # For base types, we want the full pl.TypeName format
41
- return str(pl_type.base_type()) if not inner else f"pl.{pl_type}"
42
11
 
43
12
 
44
13
  @dataclass
@@ -52,6 +21,7 @@ class FlowfileColumn:
52
21
  number_of_empty_values: int
53
22
  number_of_unique_values: int
54
23
  example_values: str
24
+ data_type_group: ReadableDataTypeGroup
55
25
  __sql_type: Optional[Any]
56
26
  __is_unique: Optional[bool]
57
27
  __nullable: Optional[bool]
@@ -75,6 +45,7 @@ class FlowfileColumn:
75
45
  self.__is_unique = None
76
46
  self.__sql_type = None
77
47
  self.__perc_unique = None
48
+ self.data_type_group = self.get_readable_datatype_group()
78
49
 
79
50
  def __repr__(self):
80
51
  """
@@ -220,6 +191,20 @@ class FlowfileColumn:
220
191
  return 'numeric'
221
192
  elif self.data_type in ('datetime', 'date', 'Date', 'Datetime', 'Time'):
222
193
  return 'date'
194
+ else:
195
+ return 'str'
196
+
197
+ def get_readable_datatype_group(self) -> ReadableDataTypeGroup:
198
+ if self.data_type in ('Utf8', 'VARCHAR', 'CHAR', 'NVARCHAR', 'String'):
199
+ return 'String'
200
+ elif self.data_type in ('fixed_decimal', 'decimal', 'float', 'integer', 'boolean', 'double', 'Int16', 'Int32',
201
+ 'Int64', 'Float32', 'Float64', 'Decimal', 'Binary', 'Boolean', 'Uint8', 'Uint16',
202
+ 'Uint32', 'Uint64'):
203
+ return 'Numeric'
204
+ elif self.data_type in ('datetime', 'date', 'Date', 'Datetime', 'Time'):
205
+ return 'Date'
206
+ else:
207
+ return 'Other'
223
208
 
224
209
  def get_polars_type(self) -> PlType:
225
210
  pl_datatype = cast_str_to_polars_type(self.data_type)
@@ -0,0 +1,36 @@
1
+
2
+ from typing import Type, Literal, List, Dict, Union, Tuple
3
+ import polars as pl
4
+ DataTypeGroup = Literal['numeric', 'string', 'datetime', 'boolean', 'binary', 'complex', 'unknown']
5
+
6
+
7
+ def convert_pl_type_to_string(pl_type: pl.DataType, inner: bool = False) -> str:
8
+ if isinstance(pl_type, pl.List):
9
+ inner_str = convert_pl_type_to_string(pl_type.inner, inner=True)
10
+ return f"pl.List({inner_str})"
11
+ elif isinstance(pl_type, pl.Array):
12
+ inner_str = convert_pl_type_to_string(pl_type.inner, inner=True)
13
+ return f"pl.Array({inner_str})"
14
+ elif isinstance(pl_type, pl.Decimal):
15
+ precision = pl_type.precision if hasattr(pl_type, 'precision') else None
16
+ scale = pl_type.scale if hasattr(pl_type, 'scale') else None
17
+ if precision is not None and scale is not None:
18
+ return f"pl.Decimal({precision}, {scale})"
19
+ elif precision is not None:
20
+ return f"pl.Decimal({precision})"
21
+ else:
22
+ return "pl.Decimal()"
23
+ elif isinstance(pl_type, pl.Struct):
24
+ # Handle Struct with field definitions
25
+ fields = []
26
+ if hasattr(pl_type, 'fields'):
27
+ for field in pl_type.fields:
28
+ field_name = field.name
29
+ field_type = convert_pl_type_to_string(field.dtype, inner=True)
30
+ fields.append(f'pl.Field("{field_name}", {field_type})')
31
+ field_str = ", ".join(fields)
32
+ return f"pl.Struct([{field_str}])"
33
+ else:
34
+ # For base types, we want the full pl.TypeName format
35
+ return str(pl_type.base_type()) if not inner else f"pl.{pl_type}"
36
+
@@ -16,7 +16,7 @@ from pyarrow.parquet import ParquetFile
16
16
  from flowfile_core.configs import logger
17
17
  from flowfile_core.configs.flow_logger import FlowLogger
18
18
  from flowfile_core.flowfile.sources.external_sources.factory import data_source_factory
19
- from flowfile_core.flowfile.flow_data_engine.flow_file_column.main import cast_str_to_polars_type, FlowfileColumn
19
+ from flowfile_core.flowfile.flow_data_engine.flow_file_column.main import FlowfileColumn, cast_str_to_polars_type
20
20
 
21
21
  from flowfile_core.flowfile.flow_data_engine.cloud_storage_reader import CloudStorageReader
22
22
  from flowfile_core.utils.arrow_reader import get_read_top_n
@@ -51,6 +51,7 @@ from flowfile_core.flowfile.sources.external_sources.sql_source.sql_source impor
51
51
  from flowfile_core.flowfile.database_connection_manager.db_connections import (get_local_database_connection,
52
52
  get_local_cloud_connection)
53
53
  from flowfile_core.flowfile.util.calculate_layout import calculate_layered_layout
54
+ from flowfile_core.flowfile.node_designer.custom_node import CustomNodeBase
54
55
 
55
56
 
56
57
  def get_xlsx_schema(engine: str, file_path: str, sheet_name: str, start_row: int, start_column: int,
@@ -436,6 +437,24 @@ class FlowGraph:
436
437
  node = self._node_db.get(node_id)
437
438
  if node is not None:
438
439
  return node
440
+
441
+ def add_user_defined_node(self, *,
442
+ custom_node: CustomNodeBase,
443
+ user_defined_node_settings: input_schema.UserDefinedNode
444
+ ):
445
+
446
+ def _func(*fdes: FlowDataEngine) -> FlowDataEngine | None:
447
+ output = custom_node.process(*(fde.data_frame for fde in fdes))
448
+ if isinstance(output, pl.LazyFrame | pl.DataFrame):
449
+ return FlowDataEngine(output)
450
+ return None
451
+
452
+ self.add_node_step(node_id=user_defined_node_settings.node_id,
453
+ function=_func,
454
+ setting_input=user_defined_node_settings,
455
+ input_node_ids=user_defined_node_settings.depending_on_ids,
456
+ node_type=custom_node.item,
457
+ )
439
458
 
440
459
  def add_pivot(self, pivot_settings: input_schema.NodePivot):
441
460
  """Adds a pivot node to the graph.
@@ -8,7 +8,7 @@ from flowfile_core.configs.flow_logger import NodeLogger
8
8
 
9
9
  from flowfile_core.schemas.output_model import TableExample, FileColumn, NodeData
10
10
  from flowfile_core.flowfile.utils import get_hash
11
- from flowfile_core.configs.node_store import nodes as node_interface
11
+ from flowfile_core.configs import node_store
12
12
  from flowfile_core.flowfile.setting_generator import setting_generator, setting_updator
13
13
  from time import sleep
14
14
  from flowfile_core.flowfile.flow_data_engine.subprocess_operations import (
@@ -27,7 +27,7 @@ class FlowNode:
27
27
  """
28
28
  parent_uuid: str
29
29
  node_type: str
30
- node_template: node_interface.NodeTemplate
30
+ node_template: node_store.NodeTemplate
31
31
  node_default: schemas.NodeDefault
32
32
  node_schema: NodeSchemaInformation
33
33
  node_inputs: NodeStepInputs
@@ -251,10 +251,10 @@ class FlowNode:
251
251
  self.results.errors = None
252
252
  self.add_lead_to_in_depend_source()
253
253
  _ = self.hash
254
- self.node_template = node_interface.node_dict.get(self.node_type)
254
+ self.node_template = node_store.node_dict.get(self.node_type)
255
255
  if self.node_template is None:
256
256
  raise Exception(f'Node template {self.node_type} not found')
257
- self.node_default = node_interface.node_defaults.get(self.node_type)
257
+ self.node_default = node_store.node_defaults.get(self.node_type)
258
258
  self.setting_input = setting_input # wait until the end so that the hash is calculated correctly
259
259
 
260
260
  @property
@@ -4,6 +4,7 @@ from flowfile_core.flowfile.manage.compatibility_enhancements import ensure_comp
4
4
  import pickle
5
5
  from flowfile_core.flowfile.flow_graph import FlowGraph
6
6
  from pathlib import Path
7
+ from flowfile_core.configs.node_store import CUSTOM_NODE_STORE
7
8
 
8
9
 
9
10
  def determine_insertion_order(node_storage: schemas.FlowInformation):
@@ -81,7 +82,14 @@ def open_flow(flow_path: Path) -> FlowGraph:
81
82
  new_flow.add_node_promise(node_promise)
82
83
  for node_id in ingestion_order:
83
84
  node_info: schemas.NodeInformation = flow_storage_obj.data[node_id]
84
- getattr(new_flow, 'add_' + node_info.type)(node_info.setting_input)
85
+ if hasattr(node_info.setting_input, "is_user_defined") and node_info.setting_input.is_user_defined:
86
+ if node_info.type not in CUSTOM_NODE_STORE:
87
+ continue
88
+ user_defined_node_class = CUSTOM_NODE_STORE[node_info.type]
89
+ new_flow.add_user_defined_node(custom_node=user_defined_node_class.from_settings(node_info.setting_input.settings),
90
+ user_defined_node_settings=node_info.setting_input)
91
+ else:
92
+ getattr(new_flow, 'add_' + node_info.type)(node_info.setting_input)
85
93
  from_node = new_flow.get_node(node_id)
86
94
  for output_node_id in node_info.outputs:
87
95
  to_node = new_flow.get_node(output_node_id)
@@ -0,0 +1,47 @@
1
+ # flowfile_core/flowfile/node_designer/__init__.py
2
+
3
+ """
4
+ Tools for creating custom Flowfile nodes.
5
+
6
+ This package provides all the necessary components for developers to build their own
7
+ custom nodes, define their UI, and implement their data processing logic.
8
+ """
9
+
10
+ # Import the core base class for creating a new node
11
+ from .custom_node import CustomNodeBase, NodeSettings
12
+
13
+ # Import all UI components so they can be used directly
14
+ from .ui_components import (
15
+ Section,
16
+ TextInput,
17
+ NumericInput,
18
+ ToggleSwitch,
19
+ SingleSelect,
20
+ MultiSelect,
21
+ ColumnSelector,
22
+ IncomingColumns, # Important marker class for dynamic dropdowns
23
+ )
24
+
25
+ # Import the main `Types` object for filtering in ColumnSelector
26
+ from .data_types import Types
27
+
28
+
29
+ # Define the public API of this package
30
+ __all__ = [
31
+ # Core Node Class
32
+ "CustomNodeBase",
33
+
34
+ # UI Components & Layout
35
+ "Section",
36
+ "TextInput",
37
+ "NumericInput",
38
+ "ToggleSwitch",
39
+ "SingleSelect",
40
+ "MultiSelect",
41
+ "NodeSettings",
42
+ "ColumnSelector",
43
+ "IncomingColumns",
44
+
45
+ # Data Type Filtering
46
+ "Types",
47
+ ]
@@ -0,0 +1,197 @@
1
+ # _type_registry.py - Internal type system (not for public use)
2
+ """
3
+ Internal type registry for mapping between different type representations.
4
+ This module should not be imported directly by users.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Type, List, Dict, Set, Any, Union
9
+ import polars as pl
10
+
11
+ # Import public types
12
+ from flowfile_core.flowfile.node_designer.data_types import TypeGroup, DataType
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class TypeMapping:
17
+ """Internal mapping between type representations."""
18
+ data_type: DataType
19
+ polars_type: Type[pl.DataType]
20
+ type_group: TypeGroup
21
+ aliases: tuple[str, ...] = ()
22
+
23
+
24
+ class TypeRegistry:
25
+ """
26
+ Internal registry for type conversions and lookups.
27
+ This class is not part of the public API.
28
+ """
29
+
30
+ def __init__(self):
31
+ self._mappings: List[TypeMapping] = [
32
+ # Numeric types
33
+ TypeMapping(DataType.Int8, pl.Int8, TypeGroup.Numeric, ("i8",)),
34
+ TypeMapping(DataType.Int16, pl.Int16, TypeGroup.Numeric, ("i16",)),
35
+ TypeMapping(DataType.Int32, pl.Int32, TypeGroup.Numeric, ("i32", "int32")),
36
+ TypeMapping(DataType.Int64, pl.Int64, TypeGroup.Numeric,
37
+ ("i64", "int64", "int", "integer", "bigint")),
38
+ TypeMapping(DataType.UInt8, pl.UInt8, TypeGroup.Numeric, ("u8",)),
39
+ TypeMapping(DataType.UInt16, pl.UInt16, TypeGroup.Numeric, ("u16",)),
40
+ TypeMapping(DataType.UInt32, pl.UInt32, TypeGroup.Numeric, ("u32", "uint32")),
41
+ TypeMapping(DataType.UInt64, pl.UInt64, TypeGroup.Numeric, ("u64", "uint64")),
42
+ TypeMapping(DataType.Float32, pl.Float32, TypeGroup.Numeric, ("f32", "float32")),
43
+ TypeMapping(DataType.Float64, pl.Float64, TypeGroup.Numeric,
44
+ ("f64", "float64", "float", "double")),
45
+ TypeMapping(DataType.Decimal, pl.Decimal, TypeGroup.Numeric,
46
+ ("decimal", "numeric", "dec")),
47
+
48
+ # String types
49
+ TypeMapping(DataType.String, pl.String, TypeGroup.String,
50
+ ("str", "string", "utf8", "varchar", "text")),
51
+ TypeMapping(DataType.Categorical, pl.Categorical, TypeGroup.String,
52
+ ("cat", "categorical", "enum", "factor")),
53
+
54
+ # Date types
55
+ TypeMapping(DataType.Date, pl.Date, TypeGroup.Date, ("date",)),
56
+ TypeMapping(DataType.Datetime, pl.Datetime, TypeGroup.Date,
57
+ ("datetime", "timestamp")),
58
+ TypeMapping(DataType.Time, pl.Time, TypeGroup.Date, ("time",)),
59
+ TypeMapping(DataType.Duration, pl.Duration, TypeGroup.Date,
60
+ ("duration", "timedelta")),
61
+
62
+ # Other types
63
+ TypeMapping(DataType.Boolean, pl.Boolean, TypeGroup.Boolean,
64
+ ("bool", "boolean")),
65
+ TypeMapping(DataType.Binary, pl.Binary, TypeGroup.Binary,
66
+ ("binary", "bytes", "bytea")),
67
+ TypeMapping(DataType.List, pl.List, TypeGroup.Complex, ("list", "array")),
68
+ TypeMapping(DataType.Struct, pl.Struct, TypeGroup.Complex, ("struct", "object")),
69
+ TypeMapping(DataType.Array, pl.Array, TypeGroup.Complex, ("fixed_array",)),
70
+ ]
71
+
72
+ self._build_indices()
73
+
74
+ def _build_indices(self):
75
+ """Build lookup indices for fast access."""
76
+ self._by_data_type: Dict[DataType, TypeMapping] = {}
77
+ self._by_polars_type: Dict[Type[pl.DataType], TypeMapping] = {}
78
+ self._by_alias: Dict[str, TypeMapping] = {}
79
+ self._by_group: Dict[TypeGroup, List[TypeMapping]] = {g: [] for g in TypeGroup}
80
+
81
+ for mapping in self._mappings:
82
+ self._by_data_type[mapping.data_type] = mapping
83
+ self._by_polars_type[mapping.polars_type] = mapping
84
+
85
+ if mapping.type_group != TypeGroup.All:
86
+ self._by_group[mapping.type_group].append(mapping)
87
+
88
+ # Register all aliases (case-insensitive)
89
+ for alias in mapping.aliases:
90
+ self._by_alias[alias.lower()] = mapping
91
+
92
+ # Register enum names as aliases
93
+ self._by_alias[mapping.data_type.value.lower()] = mapping
94
+ self._by_alias[mapping.polars_type.__name__.lower()] = mapping
95
+
96
+ # Register "pl.TypeName" format
97
+ self._by_alias[f"pl.{mapping.polars_type.__name__}".lower()] = mapping
98
+
99
+ def normalize(self, type_spec: Any) -> Set[DataType]:
100
+ """
101
+ Normalize any type specification to a set of DataType enums.
102
+ This is the main internal API for type resolution.
103
+ """
104
+ # Handle special case: All types
105
+ if type_spec == TypeGroup.All or type_spec == "ALL":
106
+ return set(self._by_data_type.keys())
107
+
108
+ # Handle TypeGroup
109
+ if isinstance(type_spec, TypeGroup):
110
+ return {m.data_type for m in self._by_group.get(type_spec, [])}
111
+
112
+ # Handle DataType
113
+ if isinstance(type_spec, DataType):
114
+ return {type_spec}
115
+
116
+ # Handle Polars type class
117
+ if isinstance(type_spec, type) and issubclass(type_spec, pl.DataType):
118
+ mapping = self._by_polars_type.get(type_spec)
119
+ if mapping:
120
+ return {mapping.data_type}
121
+
122
+ # Handle Polars type instance
123
+ if isinstance(type_spec, pl.DataType):
124
+ base_type = type_spec.base_type() if hasattr(type_spec, 'base_type') else type(type_spec)
125
+ mapping = self._by_polars_type.get(base_type)
126
+ if mapping:
127
+ return {mapping.data_type}
128
+
129
+ # Handle string aliases
130
+ if isinstance(type_spec, str):
131
+ type_spec_lower = type_spec.lower()
132
+
133
+ # Try TypeGroup name
134
+ try:
135
+ group = TypeGroup(type_spec)
136
+ return {m.data_type for m in self._by_group.get(group, [])}
137
+ except (ValueError, KeyError):
138
+ pass
139
+
140
+ # Try DataType name
141
+ try:
142
+ dt = DataType(type_spec)
143
+ return {dt}
144
+ except (ValueError, KeyError):
145
+ pass
146
+
147
+ # Check aliases
148
+ mapping = self._by_alias.get(type_spec_lower)
149
+ if mapping:
150
+ return {mapping.data_type}
151
+
152
+ # Default to empty set if unrecognized
153
+ return set()
154
+
155
+ def normalize_list(self, type_specs: List[Any]) -> Set[DataType]:
156
+ """Normalize a list of type specifications."""
157
+ result = set()
158
+ for spec in type_specs:
159
+ result.update(self.normalize(spec))
160
+ return result
161
+
162
+ def get_polars_types(self, data_types: Set[DataType]) -> Set[Type[pl.DataType]]:
163
+ """Convert a set of DataType enums to Polars types."""
164
+ result = set()
165
+ for dt in data_types:
166
+ mapping = self._by_data_type.get(dt)
167
+ if mapping:
168
+ result.add(mapping.polars_type)
169
+ return result
170
+
171
+ def get_polars_type(self, data_type: DataType) -> Type[pl.DataType]:
172
+ """Get the Polars type for a single DataType."""
173
+ mapping = self._by_data_type.get(data_type)
174
+ return mapping.polars_type if mapping else pl.String # Default fallback
175
+
176
+
177
+ # Singleton instance
178
+ _registry = TypeRegistry()
179
+
180
+
181
+ # Internal API functions (not for public use)
182
+ def normalize_type_spec(type_spec: Any) -> Set[DataType]:
183
+ """Internal function to normalize type specifications."""
184
+ if isinstance(type_spec, list):
185
+ return _registry.normalize_list(type_spec)
186
+ return _registry.normalize(type_spec)
187
+
188
+
189
+ def get_polars_types(data_types: Set[DataType]) -> Set[Type[pl.DataType]]:
190
+ """Internal function to get Polars types."""
191
+ return _registry.get_polars_types(data_types)
192
+
193
+
194
+ def check_column_type(column_dtype: pl.DataType, accepted_types: Set[DataType]) -> bool:
195
+ """Check if a column's dtype matches the accepted types."""
196
+ normalized = _registry.normalize(column_dtype)
197
+ return bool(normalized & accepted_types)