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.
Files changed (201) hide show
  1. flowfile/__init__.py +8 -1
  2. flowfile/api.py +1 -3
  3. flowfile/web/static/assets/{CloudConnectionManager-c97c25f8.js → CloudConnectionManager-0dfba9f2.js} +2 -2
  4. flowfile/web/static/assets/{CloudStorageReader-f1ff509e.js → CloudStorageReader-d5b1b6c9.js} +11 -78
  5. flowfile/web/static/assets/{CloudStorageWriter-034f8b78.js → CloudStorageWriter-00d87aad.js} +12 -79
  6. flowfile/web/static/assets/{CloudStorageWriter-49c9a4b2.css → CloudStorageWriter-b0ee067f.css} +24 -24
  7. flowfile/web/static/assets/ColumnSelector-4685e75d.js +83 -0
  8. flowfile/web/static/assets/ColumnSelector-47996a16.css +10 -0
  9. flowfile/web/static/assets/ContextMenu-23e909da.js +41 -0
  10. flowfile/web/static/assets/{SettingsSection-9c836ecc.css → ContextMenu-4c74eef1.css} +0 -21
  11. flowfile/web/static/assets/ContextMenu-63cfa99b.css +26 -0
  12. flowfile/web/static/assets/ContextMenu-70ae0c79.js +41 -0
  13. flowfile/web/static/assets/ContextMenu-c13f91d0.css +26 -0
  14. flowfile/web/static/assets/ContextMenu-f149cf7c.js +41 -0
  15. flowfile/web/static/assets/{CrossJoin-41efa4cb.css → CrossJoin-1119d18e.css} +18 -18
  16. flowfile/web/static/assets/{CrossJoin-9e156ebe.js → CrossJoin-702a3edd.js} +14 -84
  17. flowfile/web/static/assets/CustomNode-74a37f74.css +32 -0
  18. flowfile/web/static/assets/CustomNode-b1519993.js +211 -0
  19. flowfile/web/static/assets/{DatabaseConnectionSettings-d5c625b3.js → DatabaseConnectionSettings-6f3e4ea5.js} +3 -3
  20. flowfile/web/static/assets/{DatabaseManager-265adc5e.js → DatabaseManager-cf5ef661.js} +2 -2
  21. flowfile/web/static/assets/{DatabaseReader-f50c6558.css → DatabaseReader-ae61773c.css} +0 -27
  22. flowfile/web/static/assets/{DatabaseReader-0b10551e.js → DatabaseReader-d38c7295.js} +14 -114
  23. flowfile/web/static/assets/{DatabaseWriter-c17c6916.js → DatabaseWriter-b04ef46a.js} +13 -74
  24. flowfile/web/static/assets/{ExploreData-5bdae813.css → ExploreData-2d0cf4db.css} +8 -14
  25. flowfile/web/static/assets/ExploreData-5fa10ed8.js +192 -0
  26. flowfile/web/static/assets/{ExternalSource-3a66556c.js → ExternalSource-d39af878.js} +8 -79
  27. flowfile/web/static/assets/{Filter-91ad87e7.js → Filter-9b6d08db.js} +12 -85
  28. flowfile/web/static/assets/{Filter-a9d08ba1.css → Filter-f62091b3.css} +3 -3
  29. flowfile/web/static/assets/{Formula-3c395ab1.js → Formula-6b04fb1d.js} +20 -87
  30. flowfile/web/static/assets/{Formula-29f19d21.css → Formula-bb96803d.css} +4 -4
  31. flowfile/web/static/assets/{FuzzyMatch-6857de82.css → FuzzyMatch-1010f966.css} +42 -42
  32. flowfile/web/static/assets/{FuzzyMatch-2df0d230.js → FuzzyMatch-999521f4.js} +16 -87
  33. flowfile/web/static/assets/{GraphSolver-d285877f.js → GraphSolver-17dd2198.js} +13 -159
  34. flowfile/web/static/assets/GraphSolver-f0cb7bfb.css +22 -0
  35. flowfile/web/static/assets/{GroupBy-0bd1cc6b.js → GroupBy-6b039e18.js} +12 -75
  36. flowfile/web/static/assets/{Unique-b5615727.css → GroupBy-b9505323.css} +8 -8
  37. flowfile/web/static/assets/{Join-5a78a203.js → Join-24d0f113.js} +15 -85
  38. flowfile/web/static/assets/{Join-f45eff22.css → Join-fd79b451.css} +20 -20
  39. flowfile/web/static/assets/{ManualInput-a71b52c6.css → ManualInput-3246a08d.css} +20 -20
  40. flowfile/web/static/assets/{ManualInput-93aef9d6.js → ManualInput-34639209.js} +11 -82
  41. flowfile/web/static/assets/MultiSelect-0e8724a3.js +5 -0
  42. flowfile/web/static/assets/MultiSelect.vue_vue_type_script_setup_true_lang-b0e538c2.js +63 -0
  43. flowfile/web/static/assets/NumericInput-3d63a470.js +5 -0
  44. flowfile/web/static/assets/NumericInput.vue_vue_type_script_setup_true_lang-e0edeccc.js +35 -0
  45. flowfile/web/static/assets/Output-283fe388.css +37 -0
  46. flowfile/web/static/assets/{Output-411ecaee.js → Output-edea9802.js} +62 -273
  47. flowfile/web/static/assets/{Pivot-89db4b04.js → Pivot-61d19301.js} +14 -138
  48. flowfile/web/static/assets/Pivot-cf333e3d.css +22 -0
  49. flowfile/web/static/assets/PivotValidation-891ddfb0.css +13 -0
  50. flowfile/web/static/assets/PivotValidation-c46cd420.css +13 -0
  51. flowfile/web/static/assets/PivotValidation-de9f43fe.js +61 -0
  52. flowfile/web/static/assets/PivotValidation-f97fec5b.js +61 -0
  53. flowfile/web/static/assets/{PolarsCode-a9f974f8.js → PolarsCode-bc3c9984.js} +13 -80
  54. flowfile/web/static/assets/Read-64a3f259.js +218 -0
  55. flowfile/web/static/assets/Read-e808b239.css +62 -0
  56. flowfile/web/static/assets/RecordCount-3d5039be.js +53 -0
  57. flowfile/web/static/assets/{RecordId-55ae7d36.js → RecordId-597510e0.js} +8 -80
  58. flowfile/web/static/assets/SQLQueryComponent-36cef432.css +27 -0
  59. flowfile/web/static/assets/SQLQueryComponent-df51adbe.js +38 -0
  60. flowfile/web/static/assets/{Sample-b4a18476.js → Sample-4be0a507.js} +8 -77
  61. flowfile/web/static/assets/{SecretManager-b066d13a.js → SecretManager-4839be57.js} +2 -2
  62. flowfile/web/static/assets/{Select-727688dc.js → Select-9b72f201.js} +11 -85
  63. flowfile/web/static/assets/SettingsSection-2e4d03c4.css +21 -0
  64. flowfile/web/static/assets/SettingsSection-5c696bee.css +20 -0
  65. flowfile/web/static/assets/SettingsSection-71e6b7e3.css +21 -0
  66. flowfile/web/static/assets/SettingsSection-7ded385d.js +45 -0
  67. flowfile/web/static/assets/{SettingsSection-695ac487.js → SettingsSection-e1e9c953.js} +2 -40
  68. flowfile/web/static/assets/SettingsSection-f0f75a42.js +53 -0
  69. flowfile/web/static/assets/SingleSelect-6c777aac.js +5 -0
  70. flowfile/web/static/assets/SingleSelect.vue_vue_type_script_setup_true_lang-33e3ff9b.js +62 -0
  71. flowfile/web/static/assets/SliderInput-7cb93e62.js +40 -0
  72. flowfile/web/static/assets/SliderInput-b8fb6a8c.css +4 -0
  73. flowfile/web/static/assets/{GroupBy-ab1ea74b.css → Sort-3643d625.css} +8 -8
  74. flowfile/web/static/assets/{Sort-be3339a8.js → Sort-6cbde21a.js} +12 -97
  75. flowfile/web/static/assets/TextInput-d9a40c11.js +5 -0
  76. flowfile/web/static/assets/TextInput.vue_vue_type_script_setup_true_lang-5896c375.js +32 -0
  77. flowfile/web/static/assets/{TextToRows-c92d1ec2.css → TextToRows-5d2c1190.css} +9 -9
  78. flowfile/web/static/assets/{TextToRows-7b8998da.js → TextToRows-c4fcbf4d.js} +14 -83
  79. flowfile/web/static/assets/ToggleSwitch-4ef91d19.js +5 -0
  80. flowfile/web/static/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-38478c20.js +31 -0
  81. flowfile/web/static/assets/{UnavailableFields-8b0cb48e.js → UnavailableFields-a03f512c.js} +2 -2
  82. flowfile/web/static/assets/{Union-8d9ac7f9.css → Union-af6c3d9b.css} +6 -6
  83. flowfile/web/static/assets/Union-bfe9b996.js +77 -0
  84. flowfile/web/static/assets/{Unique-af5a80b4.js → Unique-5d023a27.js} +23 -104
  85. flowfile/web/static/assets/{Sort-7ccfa0fe.css → Unique-f9fb0809.css} +8 -8
  86. flowfile/web/static/assets/Unpivot-1e422df3.css +30 -0
  87. flowfile/web/static/assets/{Unpivot-5195d411.js → Unpivot-91cc5354.js} +12 -166
  88. flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +13 -0
  89. flowfile/web/static/assets/UnpivotValidation-7ee2de44.js +51 -0
  90. flowfile/web/static/assets/{ExploreData-18a4fe52.js → VueGraphicWalker-e51b9924.js} +4 -264
  91. flowfile/web/static/assets/VueGraphicWalker-ed5ab88b.css +6 -0
  92. flowfile/web/static/assets/{api-cb00cce6.js → api-c1bad5ca.js} +1 -1
  93. flowfile/web/static/assets/{api-023d1733.js → api-cf1221f0.js} +1 -1
  94. flowfile/web/static/assets/{designer-2197d782.css → designer-8da3ba3a.css} +859 -201
  95. flowfile/web/static/assets/{designer-6c322d8e.js → designer-9633482a.js} +2297 -733
  96. flowfile/web/static/assets/{documentation-4d1fafe1.js → documentation-ca400224.js} +1 -1
  97. flowfile/web/static/assets/{dropDown-0b46dd77.js → dropDown-614b998d.js} +1 -1
  98. flowfile/web/static/assets/{fullEditor-ec4e4f95.js → fullEditor-f7971590.js} +2 -2
  99. flowfile/web/static/assets/{genericNodeSettings-def5879b.js → genericNodeSettings-4fe5f36b.js} +3 -3
  100. flowfile/web/static/assets/{index-681a3ed0.css → index-50508d4d.css} +8 -0
  101. flowfile/web/static/assets/{index-683fc198.js → index-5429bbf8.js} +208 -31
  102. flowfile/web/static/assets/nodeInput-5d0d6b79.js +41 -0
  103. flowfile/web/static/assets/outputCsv-076b85ab.js +86 -0
  104. flowfile/web/static/assets/{Output-48f81019.css → outputCsv-9cc59e0b.css} +0 -143
  105. flowfile/web/static/assets/outputExcel-0fd17dbe.js +56 -0
  106. flowfile/web/static/assets/outputExcel-b41305c0.css +102 -0
  107. flowfile/web/static/assets/outputParquet-b61e0847.js +31 -0
  108. flowfile/web/static/assets/outputParquet-cf8cf3f2.css +4 -0
  109. flowfile/web/static/assets/readCsv-a8bb8b61.js +179 -0
  110. flowfile/web/static/assets/readCsv-c767cb37.css +52 -0
  111. flowfile/web/static/assets/readExcel-67b4aee0.js +201 -0
  112. flowfile/web/static/assets/readExcel-806d2826.css +64 -0
  113. flowfile/web/static/assets/readParquet-48c81530.css +19 -0
  114. flowfile/web/static/assets/readParquet-92ce1dbc.js +23 -0
  115. flowfile/web/static/assets/{secretApi-baceb6f9.js → secretApi-68435402.js} +1 -1
  116. flowfile/web/static/assets/{selectDynamic-de91449a.js → selectDynamic-92e25ee3.js} +7 -7
  117. flowfile/web/static/assets/{selectDynamic-b062bc9b.css → selectDynamic-aa913ff4.css} +16 -16
  118. flowfile/web/static/assets/user-defined-icon-0ae16c90.png +0 -0
  119. flowfile/web/static/assets/{vue-codemirror.esm-dc5e3348.js → vue-codemirror.esm-41b0e0d7.js} +65 -36
  120. flowfile/web/static/assets/{vue-content-loader.es-ba94b82f.js → vue-content-loader.es-2c8e608f.js} +1 -1
  121. flowfile/web/static/index.html +2 -2
  122. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/METADATA +5 -3
  123. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/RECORD +191 -121
  124. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/WHEEL +1 -1
  125. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/entry_points.txt +1 -0
  126. flowfile_core/__init__.py +3 -0
  127. flowfile_core/configs/flow_logger.py +5 -13
  128. flowfile_core/configs/node_store/__init__.py +30 -0
  129. flowfile_core/configs/node_store/nodes.py +383 -99
  130. flowfile_core/configs/node_store/user_defined_node_registry.py +193 -0
  131. flowfile_core/configs/settings.py +2 -1
  132. flowfile_core/database/connection.py +5 -21
  133. flowfile_core/fileExplorer/funcs.py +239 -121
  134. flowfile_core/flowfile/analytics/analytics_processor.py +1 -0
  135. flowfile_core/flowfile/code_generator/code_generator.py +62 -64
  136. flowfile_core/flowfile/flow_data_engine/create/funcs.py +73 -56
  137. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +77 -86
  138. flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +4 -0
  139. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +19 -34
  140. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +36 -0
  141. flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +23 -23
  142. flowfile_core/flowfile/flow_data_engine/join/utils.py +1 -1
  143. flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +9 -4
  144. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +212 -86
  145. flowfile_core/flowfile/flow_data_engine/utils.py +2 -0
  146. flowfile_core/flowfile/flow_graph.py +240 -54
  147. flowfile_core/flowfile/flow_node/flow_node.py +48 -13
  148. flowfile_core/flowfile/flow_node/models.py +2 -1
  149. flowfile_core/flowfile/handler.py +24 -5
  150. flowfile_core/flowfile/manage/compatibility_enhancements.py +404 -41
  151. flowfile_core/flowfile/manage/io_flowfile.py +394 -0
  152. flowfile_core/flowfile/node_designer/__init__.py +47 -0
  153. flowfile_core/flowfile/node_designer/_type_registry.py +197 -0
  154. flowfile_core/flowfile/node_designer/custom_node.py +371 -0
  155. flowfile_core/flowfile/node_designer/ui_components.py +277 -0
  156. flowfile_core/flowfile/schema_callbacks.py +17 -10
  157. flowfile_core/flowfile/setting_generator/settings.py +15 -10
  158. flowfile_core/main.py +5 -1
  159. flowfile_core/routes/routes.py +73 -30
  160. flowfile_core/routes/user_defined_components.py +55 -0
  161. flowfile_core/schemas/cloud_storage_schemas.py +0 -2
  162. flowfile_core/schemas/input_schema.py +228 -65
  163. flowfile_core/schemas/output_model.py +5 -2
  164. flowfile_core/schemas/schemas.py +153 -35
  165. flowfile_core/schemas/transform_schema.py +1083 -412
  166. flowfile_core/schemas/yaml_types.py +103 -0
  167. flowfile_core/types.py +156 -0
  168. flowfile_core/utils/validate_setup.py +3 -1
  169. flowfile_frame/__init__.py +3 -1
  170. flowfile_frame/flow_frame.py +31 -24
  171. flowfile_frame/flow_frame_methods.py +12 -9
  172. flowfile_worker/__init__.py +9 -35
  173. flowfile_worker/create/__init__.py +3 -21
  174. flowfile_worker/create/funcs.py +68 -56
  175. flowfile_worker/create/models.py +130 -62
  176. flowfile_worker/main.py +5 -2
  177. flowfile_worker/routes.py +52 -13
  178. shared/__init__.py +15 -0
  179. shared/storage_config.py +258 -0
  180. tools/migrate/README.md +56 -0
  181. tools/migrate/__init__.py +12 -0
  182. tools/migrate/__main__.py +131 -0
  183. tools/migrate/legacy_schemas.py +621 -0
  184. tools/migrate/migrate.py +598 -0
  185. tools/migrate/tests/__init__.py +0 -0
  186. tools/migrate/tests/conftest.py +23 -0
  187. tools/migrate/tests/test_migrate.py +627 -0
  188. tools/migrate/tests/test_migration_e2e.py +1010 -0
  189. tools/migrate/tests/test_node_migrations.py +813 -0
  190. flowfile/web/static/assets/GraphSolver-17fd26db.css +0 -68
  191. flowfile/web/static/assets/Pivot-f415e85f.css +0 -35
  192. flowfile/web/static/assets/Read-80dc1675.css +0 -197
  193. flowfile/web/static/assets/Read-c3b1929c.js +0 -701
  194. flowfile/web/static/assets/RecordCount-4e95f98e.js +0 -122
  195. flowfile/web/static/assets/Union-89fd73dc.js +0 -146
  196. flowfile/web/static/assets/Unpivot-246e9bbd.css +0 -77
  197. flowfile/web/static/assets/nodeTitle-a16db7c3.js +0 -227
  198. flowfile/web/static/assets/nodeTitle-f4b12bcb.css +0 -134
  199. flowfile_core/flowfile/manage/open_flowfile.py +0 -135
  200. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info/licenses}/LICENSE +0 -0
  201. /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