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.
- flowfile/__init__.py +6 -1
- flowfile/api.py +1 -1
- flowfile/web/static/assets/{CloudConnectionManager-d7c2c028.js → CloudConnectionManager-d3248f8d.js} +2 -2
- flowfile/web/static/assets/{CloudStorageReader-d467329f.js → CloudStorageReader-d65bf041.js} +6 -6
- flowfile/web/static/assets/{CloudStorageWriter-071b8b00.js → CloudStorageWriter-e83be3ed.js} +6 -6
- flowfile/web/static/assets/ColumnSelector-47996a16.css +10 -0
- flowfile/web/static/assets/ColumnSelector-cce661cf.js +83 -0
- flowfile/web/static/assets/{ContextMenu-a51e19ea.js → ContextMenu-11a4652a.js} +1 -1
- flowfile/web/static/assets/{ContextMenu-2dea5e27.js → ContextMenu-160afb08.js} +1 -1
- flowfile/web/static/assets/{ContextMenu-785554c4.js → ContextMenu-cf18d2cc.js} +1 -1
- flowfile/web/static/assets/{CrossJoin-cf68ec7a.js → CrossJoin-d395d38c.js} +7 -7
- flowfile/web/static/assets/CustomNode-74a37f74.css +32 -0
- flowfile/web/static/assets/CustomNode-b812dc0b.js +211 -0
- flowfile/web/static/assets/{DatabaseConnectionSettings-435c5dd8.js → DatabaseConnectionSettings-7000bf2c.js} +2 -2
- flowfile/web/static/assets/{DatabaseManager-349e33a8.js → DatabaseManager-9662ec5b.js} +2 -2
- flowfile/web/static/assets/{DatabaseReader-8075bd28.js → DatabaseReader-4f035d0c.js} +9 -9
- flowfile/web/static/assets/{DatabaseWriter-3e2dda89.js → DatabaseWriter-f65dcd54.js} +8 -8
- flowfile/web/static/assets/{ExploreData-76ec698c.js → ExploreData-94c43dfc.js} +5 -5
- flowfile/web/static/assets/{ExternalSource-609a265c.js → ExternalSource-ac04b3cc.js} +5 -5
- flowfile/web/static/assets/{Filter-97cff793.js → Filter-812dcbca.js} +7 -7
- flowfile/web/static/assets/{Formula-09de0ec9.js → Formula-71472193.js} +9 -9
- flowfile/web/static/assets/{FuzzyMatch-bdf70248.js → FuzzyMatch-b317f631.js} +8 -8
- flowfile/web/static/assets/{GraphSolver-0b5a0e05.js → GraphSolver-754a234f.js} +6 -6
- flowfile/web/static/assets/{GroupBy-eaddadde.js → GroupBy-6c6f9802.js} +5 -5
- flowfile/web/static/assets/{Join-3313371b.js → Join-a1b800be.js} +8 -8
- flowfile/web/static/assets/{ManualInput-e8bfc0be.js → ManualInput-a9640276.js} +4 -4
- flowfile/web/static/assets/MultiSelect-97213888.js +5 -0
- flowfile/web/static/assets/MultiSelect.vue_vue_type_script_setup_true_lang-6ffe088a.js +63 -0
- flowfile/web/static/assets/NumericInput-e638088a.js +5 -0
- flowfile/web/static/assets/NumericInput.vue_vue_type_script_setup_true_lang-90eb2cba.js +35 -0
- flowfile/web/static/assets/{Output-7303bb09.js → Output-76750610.js} +6 -6
- flowfile/web/static/assets/{Pivot-3b1c54ef.js → Pivot-7814803f.js} +7 -7
- flowfile/web/static/assets/{PivotValidation-3bb36c8f.js → PivotValidation-76dd431a.js} +1 -1
- flowfile/web/static/assets/{PivotValidation-eaa819c0.js → PivotValidation-f92137d2.js} +1 -1
- flowfile/web/static/assets/{PolarsCode-aa12e25d.js → PolarsCode-889c3008.js} +5 -5
- flowfile/web/static/assets/{Read-a2bfc618.js → Read-637b72a7.js} +8 -8
- flowfile/web/static/assets/{RecordCount-aa0dc082.js → RecordCount-2b050c41.js} +4 -4
- flowfile/web/static/assets/{RecordId-48ee1a3b.js → RecordId-81df7784.js} +6 -6
- flowfile/web/static/assets/{SQLQueryComponent-e149dbf2.js → SQLQueryComponent-88dcfe53.js} +1 -1
- flowfile/web/static/assets/{Sample-f06cb97a.js → Sample-258ad2a9.js} +4 -4
- flowfile/web/static/assets/{SecretManager-37f34886.js → SecretManager-2a2cb7e2.js} +2 -2
- flowfile/web/static/assets/{Select-b60e6c47.js → Select-850215fd.js} +7 -7
- flowfile/web/static/assets/{SettingsSection-75b6cf4f.js → SettingsSection-0e8d9123.js} +1 -1
- flowfile/web/static/assets/{SettingsSection-e57a672e.js → SettingsSection-29b4fa6b.js} +1 -1
- flowfile/web/static/assets/{SettingsSection-70e5a7b1.js → SettingsSection-55bae608.js} +1 -1
- flowfile/web/static/assets/SingleSelect-bebd408b.js +5 -0
- flowfile/web/static/assets/SingleSelect.vue_vue_type_script_setup_true_lang-6093741c.js +62 -0
- flowfile/web/static/assets/SliderInput-6a05ab61.js +40 -0
- flowfile/web/static/assets/SliderInput-b8fb6a8c.css +4 -0
- flowfile/web/static/assets/{Sort-51b1ee4d.js → Sort-10ab48ed.js} +5 -5
- flowfile/web/static/assets/TextInput-df9d6259.js +5 -0
- flowfile/web/static/assets/TextInput.vue_vue_type_script_setup_true_lang-000e1178.js +32 -0
- flowfile/web/static/assets/{TextToRows-26835f8f.js → TextToRows-6c2d93d8.js} +7 -7
- flowfile/web/static/assets/ToggleSwitch-0ff7ac52.js +5 -0
- flowfile/web/static/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-c6dc3029.js +31 -0
- flowfile/web/static/assets/{UnavailableFields-88a4cd0c.js → UnavailableFields-1bab97cb.js} +2 -2
- flowfile/web/static/assets/{Union-4d0088eb.js → Union-b563478a.js} +4 -4
- flowfile/web/static/assets/{Unique-7d554a62.js → Unique-f90db5db.js} +7 -7
- flowfile/web/static/assets/{Unpivot-4668595c.js → Unpivot-bcb0025f.js} +6 -6
- flowfile/web/static/assets/{UnpivotValidation-d4f0e0e8.js → UnpivotValidation-c4e73b04.js} +1 -1
- flowfile/web/static/assets/{VueGraphicWalker-5324d566.js → VueGraphicWalker-bb8535e2.js} +1 -1
- flowfile/web/static/assets/{api-271ed117.js → api-2d6adc4f.js} +1 -1
- flowfile/web/static/assets/{api-31e4fea6.js → api-4c8e3822.js} +1 -1
- flowfile/web/static/assets/{designer-091bdc3f.css → designer-e3c150ec.css} +41 -41
- flowfile/web/static/assets/{designer-bf3d9487.js → designer-f3656d8c.js} +23 -15
- flowfile/web/static/assets/{documentation-4d0a1cea.js → documentation-52b241e7.js} +1 -1
- flowfile/web/static/assets/{dropDown-025888df.js → dropDown-1bca8a74.js} +1 -1
- flowfile/web/static/assets/{fullEditor-1df991ec.js → fullEditor-2985687e.js} +2 -2
- flowfile/web/static/assets/{genericNodeSettings-d3b2b2ac.js → genericNodeSettings-0476ba4e.js} +3 -3
- flowfile/web/static/assets/{index-d0518598.js → index-246f201c.js} +6 -6
- flowfile/web/static/assets/{index-681a3ed0.css → index-50508d4d.css} +8 -0
- flowfile/web/static/assets/{outputCsv-d8457527.js → outputCsv-d686eeaf.js} +1 -1
- flowfile/web/static/assets/{outputExcel-be89153e.js → outputExcel-8809ea2f.js} +1 -1
- flowfile/web/static/assets/{outputParquet-fabb445a.js → outputParquet-53ba645a.js} +1 -1
- flowfile/web/static/assets/{readCsv-e8359522.js → readCsv-053bf97b.js} +1 -1
- flowfile/web/static/assets/{readExcel-dabaf51b.js → readExcel-ad531eab.js} +3 -3
- flowfile/web/static/assets/{readParquet-e0771ef2.js → readParquet-58e899a1.js} +1 -1
- flowfile/web/static/assets/{secretApi-ce823eee.js → secretApi-538058f3.js} +1 -1
- flowfile/web/static/assets/{selectDynamic-5476546e.js → selectDynamic-b38de2ba.js} +3 -3
- flowfile/web/static/assets/user-defined-icon-0ae16c90.png +0 -0
- flowfile/web/static/assets/{vue-codemirror.esm-9ed00d50.js → vue-codemirror.esm-db9b8936.js} +33 -3
- flowfile/web/static/assets/{vue-content-loader.es-7bca2d9b.js → vue-content-loader.es-b5f3ac30.js} +1 -1
- flowfile/web/static/index.html +2 -2
- {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/METADATA +3 -2
- {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/RECORD +111 -85
- {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/WHEEL +1 -1
- flowfile_core/configs/node_store/__init__.py +30 -0
- flowfile_core/configs/node_store/nodes.py +383 -358
- flowfile_core/configs/node_store/user_defined_node_registry.py +193 -0
- 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_graph.py +20 -1
- flowfile_core/flowfile/flow_node/flow_node.py +4 -4
- flowfile_core/flowfile/manage/open_flowfile.py +9 -1
- 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/data_types.py +146 -0
- flowfile_core/flowfile/node_designer/ui_components.py +277 -0
- flowfile_core/flowfile/schema_callbacks.py +1 -1
- flowfile_core/main.py +2 -1
- flowfile_core/routes/routes.py +16 -20
- flowfile_core/routes/user_defined_components.py +55 -0
- flowfile_core/schemas/input_schema.py +8 -1
- flowfile_core/schemas/schemas.py +6 -3
- flowfile_core/utils/validate_setup.py +3 -1
- flowfile_frame/flow_frame.py +16 -6
- shared/storage_config.py +17 -2
- {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info}/entry_points.txt +0 -0
- {flowfile-0.3.10.dist-info → flowfile-0.4.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -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.flowfile.node_designer.data_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
|
|
@@ -121,7 +121,7 @@ def pre_calculate_pivot_schema(node_input_schema: List[FlowfileColumn],
|
|
|
121
121
|
pivot_input.index_columns]
|
|
122
122
|
val_column_schema = get_schema_of_column(node_input_schema, pivot_input.value_col)
|
|
123
123
|
if output_fields is not None and len(output_fields) > 0:
|
|
124
|
-
return index_columns_schema+[FlowfileColumn(PlType(
|
|
124
|
+
return index_columns_schema+[FlowfileColumn(PlType(column_name=output_field.name,
|
|
125
125
|
pl_datatype=output_field.data_type)) for output_field in
|
|
126
126
|
output_fields]
|
|
127
127
|
|
flowfile_core/main.py
CHANGED
|
@@ -18,6 +18,7 @@ from flowfile_core.routes.routes import router
|
|
|
18
18
|
from flowfile_core.routes.public import router as public_router
|
|
19
19
|
from flowfile_core.routes.logs import router as logs_router
|
|
20
20
|
from flowfile_core.routes.cloud_connections import router as cloud_connections_router
|
|
21
|
+
from flowfile_core.routes.user_defined_components import router as user_defined_components_router
|
|
21
22
|
|
|
22
23
|
from flowfile_core.configs.flow_logger import clear_all_flow_logs
|
|
23
24
|
storage.cleanup_directories()
|
|
@@ -79,7 +80,7 @@ app.include_router(logs_router, tags=["logs"])
|
|
|
79
80
|
app.include_router(auth_router, prefix="/auth", tags=["auth"])
|
|
80
81
|
app.include_router(secrets_router, prefix="/secrets", tags=["secrets"])
|
|
81
82
|
app.include_router(cloud_connections_router, prefix="/cloud_connections", tags=["cloud_connections"])
|
|
82
|
-
|
|
83
|
+
app.include_router(user_defined_components_router, prefix="/user_defined_components", tags=["user_defined_components"])
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
@app.post("/shutdown")
|
flowfile_core/routes/routes.py
CHANGED
|
@@ -12,45 +12,40 @@ import logging
|
|
|
12
12
|
import os
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import List, Dict, Any, Optional
|
|
15
|
-
from sqlalchemy.orm import Session
|
|
16
15
|
|
|
17
16
|
from fastapi import APIRouter, File, UploadFile, BackgroundTasks, HTTPException, status, Body, Depends
|
|
18
17
|
from fastapi.responses import JSONResponse, Response
|
|
19
18
|
# External dependencies
|
|
20
19
|
from polars_expr_transformer.function_overview import get_all_expressions, get_expression_overview
|
|
20
|
+
from sqlalchemy.orm import Session
|
|
21
21
|
|
|
22
|
+
from flowfile_core import flow_file_handler
|
|
22
23
|
# Core modules
|
|
23
24
|
from flowfile_core.auth.jwt import get_current_active_user
|
|
24
25
|
from flowfile_core.configs import logger
|
|
25
|
-
from flowfile_core.configs.node_store import
|
|
26
|
-
from flowfile_core.
|
|
26
|
+
from flowfile_core.configs.node_store import nodes_list, check_if_has_default_setting
|
|
27
|
+
from flowfile_core.database.connection import get_db
|
|
27
28
|
# File handling
|
|
28
29
|
from flowfile_core.fileExplorer.funcs import (
|
|
29
30
|
SecureFileExplorer,
|
|
30
31
|
FileInfo,
|
|
31
32
|
get_files_from_directory
|
|
32
33
|
)
|
|
33
|
-
from flowfile_core.flowfile.flow_graph import add_connection, delete_connection
|
|
34
|
-
from flowfile_core.flowfile.code_generator.code_generator import export_flow_to_polars
|
|
35
34
|
from flowfile_core.flowfile.analytics.analytics_processor import AnalyticsProcessor
|
|
35
|
+
from flowfile_core.flowfile.code_generator.code_generator import export_flow_to_polars
|
|
36
|
+
from flowfile_core.flowfile.database_connection_manager.db_connections import (store_database_connection,
|
|
37
|
+
get_database_connection,
|
|
38
|
+
delete_database_connection,
|
|
39
|
+
get_all_database_connections_interface)
|
|
36
40
|
from flowfile_core.flowfile.extensions import get_instant_func_results
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
from flowfile_core.flowfile.flow_graph import add_connection, delete_connection
|
|
39
42
|
from flowfile_core.flowfile.sources.external_sources.sql_source.sql_source import create_sql_source_from_db_settings
|
|
40
43
|
from flowfile_core.run_lock import get_flow_run_lock
|
|
41
|
-
# Schema and models
|
|
42
|
-
|
|
43
|
-
from shared.storage_config import storage
|
|
44
44
|
from flowfile_core.schemas import input_schema, schemas, output_model
|
|
45
45
|
from flowfile_core.utils import excel_file_manager
|
|
46
46
|
from flowfile_core.utils.fileManager import create_dir
|
|
47
47
|
from flowfile_core.utils.utils import camel_case_to_snake_case
|
|
48
|
-
from
|
|
49
|
-
from flowfile_core.flowfile.database_connection_manager.db_connections import (store_database_connection,
|
|
50
|
-
get_database_connection,
|
|
51
|
-
delete_database_connection,
|
|
52
|
-
get_all_database_connections_interface)
|
|
53
|
-
from flowfile_core.database.connection import get_db
|
|
48
|
+
from shared.storage_config import storage
|
|
54
49
|
|
|
55
50
|
|
|
56
51
|
router = APIRouter(dependencies=[Depends(get_current_active_user)])
|
|
@@ -359,7 +354,7 @@ def add_node(flow_id: int, node_id: int, node_type: str, pos_x: int = 0, pos_y:
|
|
|
359
354
|
logger.info("Adding node")
|
|
360
355
|
flow.add_node_promise(node_promise)
|
|
361
356
|
|
|
362
|
-
if
|
|
357
|
+
if check_if_has_default_setting(node_type):
|
|
363
358
|
logger.info(f'Found standard settings for {node_type}, trying to upload them')
|
|
364
359
|
setting_name_ref = 'node' + node_type.replace('_', '')
|
|
365
360
|
node_model = get_node_model(setting_name_ref)
|
|
@@ -545,10 +540,11 @@ def get_list_of_saved_flows(path: str):
|
|
|
545
540
|
except:
|
|
546
541
|
return []
|
|
547
542
|
|
|
548
|
-
|
|
549
|
-
|
|
543
|
+
|
|
544
|
+
@router.get('/node_list', response_model=List[schemas.NodeTemplate])
|
|
545
|
+
def get_node_list() -> List[schemas.NodeTemplate]:
|
|
550
546
|
"""Retrieves the list of all available node types and their templates."""
|
|
551
|
-
return
|
|
547
|
+
return nodes_list
|
|
552
548
|
|
|
553
549
|
|
|
554
550
|
@router.get('/node', response_model=output_model.NodeData, tags=['editor'])
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Dict, Any
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Depends
|
|
5
|
+
|
|
6
|
+
from flowfile_core import flow_file_handler
|
|
7
|
+
# Core modules
|
|
8
|
+
from flowfile_core.auth.jwt import get_current_active_user
|
|
9
|
+
from flowfile_core.configs import logger
|
|
10
|
+
from flowfile_core.configs.node_store import CUSTOM_NODE_STORE
|
|
11
|
+
# File handling
|
|
12
|
+
from flowfile_core.schemas import input_schema
|
|
13
|
+
from flowfile_core.utils.utils import camel_case_to_snake_case
|
|
14
|
+
|
|
15
|
+
# External dependencies
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
router = APIRouter()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.get("/custom-node-schema", summary="Get a simple UI schema")
|
|
22
|
+
def get_simple_custom_object(flow_id: int, node_id: int):
|
|
23
|
+
"""
|
|
24
|
+
This endpoint returns a hardcoded JSON object that represents the UI
|
|
25
|
+
for our SimpleFilterNode.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
node = flow_file_handler.get_node(flow_id=flow_id, node_id=node_id)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
31
|
+
user_defined_node = CUSTOM_NODE_STORE.get(node.node_type)
|
|
32
|
+
|
|
33
|
+
if not user_defined_node:
|
|
34
|
+
raise HTTPException(status_code=404, detail=f"Node type '{node.node_type}' not found")
|
|
35
|
+
if node.is_setup:
|
|
36
|
+
settings = node.setting_input.settings
|
|
37
|
+
return user_defined_node.from_settings(settings).get_frontend_schema()
|
|
38
|
+
return user_defined_node().get_frontend_schema()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.post("/update_user_defined_node", tags=["transform"])
|
|
42
|
+
def update_user_defined_node(input_data: Dict[str, Any], node_type: str, current_user=Depends(get_current_active_user)):
|
|
43
|
+
input_data['user_id'] = current_user.id
|
|
44
|
+
node_type = camel_case_to_snake_case(node_type)
|
|
45
|
+
flow_id = int(input_data.get('flow_id'))
|
|
46
|
+
logger.info(f'Updating the data for flow: {flow_id}, node {input_data["node_id"]}')
|
|
47
|
+
flow = flow_file_handler.get_flow(flow_id)
|
|
48
|
+
user_defined_model = CUSTOM_NODE_STORE.get(node_type)
|
|
49
|
+
if not user_defined_model:
|
|
50
|
+
raise HTTPException(status_code=404, detail=f"Node type '{node_type}' not found")
|
|
51
|
+
|
|
52
|
+
user_defined_node_settings = input_schema.UserDefinedNode.model_validate(input_data)
|
|
53
|
+
initialized_model = user_defined_model.from_settings(user_defined_node_settings.settings)
|
|
54
|
+
|
|
55
|
+
flow.add_user_defined_node(custom_node=initialized_model, user_defined_node_settings=user_defined_node_settings)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import List, Optional, Literal, Iterator
|
|
1
|
+
from typing import List, Optional, Literal, Iterator, Any
|
|
2
2
|
from flowfile_core.schemas import transform_schema
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
import os
|
|
@@ -195,6 +195,8 @@ class NodeBase(BaseModel):
|
|
|
195
195
|
description: Optional[str] = ''
|
|
196
196
|
user_id: Optional[int] = None
|
|
197
197
|
is_flow_output: Optional[bool] = False
|
|
198
|
+
is_user_defined: Optional[bool] = False # Indicator if the node is a user defined node
|
|
199
|
+
|
|
198
200
|
|
|
199
201
|
class NodeSingleInput(NodeBase):
|
|
200
202
|
"""A base model for any node that takes a single data input."""
|
|
@@ -516,3 +518,8 @@ class NodeRecordCount(NodeSingleInput):
|
|
|
516
518
|
class NodePolarsCode(NodeMultiInput):
|
|
517
519
|
"""Settings for a node that executes arbitrary user-provided Polars code."""
|
|
518
520
|
polars_code_input: transform_schema.PolarsCodeInput
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
class UserDefinedNode(NodeMultiInput):
|
|
524
|
+
"""Settings for a node that contains the user defined node information"""
|
|
525
|
+
settings: Any
|
flowfile_core/schemas/schemas.py
CHANGED
|
@@ -5,6 +5,9 @@ from flowfile_core.configs.settings import OFFLOAD_TO_WORKER
|
|
|
5
5
|
ExecutionModeLiteral = Literal['Development', 'Performance']
|
|
6
6
|
ExecutionLocationsLiteral = Literal['local', 'remote']
|
|
7
7
|
|
|
8
|
+
# Type literals for classifying nodes.
|
|
9
|
+
NodeTypeLiteral = Literal['input', 'output', 'process']
|
|
10
|
+
TransformTypeLiteral = Literal['narrow', 'wide', 'other']
|
|
8
11
|
|
|
9
12
|
def get_global_execution_location() -> ExecutionLocationsLiteral:
|
|
10
13
|
"""
|
|
@@ -135,11 +138,14 @@ class NodeTemplate(BaseModel):
|
|
|
135
138
|
output: int
|
|
136
139
|
image: str
|
|
137
140
|
multi: bool = False
|
|
141
|
+
node_type: NodeTypeLiteral
|
|
142
|
+
transform_type: TransformTypeLiteral
|
|
138
143
|
node_group: str
|
|
139
144
|
prod_ready: bool = True
|
|
140
145
|
can_be_start: bool = False
|
|
141
146
|
drawer_title: str = "Node title"
|
|
142
147
|
drawer_intro: str = "Drawer into"
|
|
148
|
+
custom_node: Optional[bool] = False
|
|
143
149
|
|
|
144
150
|
|
|
145
151
|
class NodeInformation(BaseModel):
|
|
@@ -263,9 +269,6 @@ class VueFlowInput(BaseModel):
|
|
|
263
269
|
node_inputs: List[NodeInput]
|
|
264
270
|
|
|
265
271
|
|
|
266
|
-
# Type literals for classifying nodes.
|
|
267
|
-
NodeTypeLiteral = Literal['input', 'output', 'process']
|
|
268
|
-
TransformTypeLiteral = Literal['narrow', 'wide', 'other']
|
|
269
272
|
|
|
270
273
|
|
|
271
274
|
class NodeDefault(BaseModel):
|
|
@@ -3,7 +3,7 @@ as have a component in flowfile_frontend"""
|
|
|
3
3
|
|
|
4
4
|
from flowfile_core.schemas import input_schema
|
|
5
5
|
from flowfile_core.flowfile.flow_graph import FlowGraph
|
|
6
|
-
from flowfile_core.configs.node_store
|
|
6
|
+
from flowfile_core.configs.node_store import nodes_list, NodeTemplate
|
|
7
7
|
import inspect
|
|
8
8
|
|
|
9
9
|
|
|
@@ -31,6 +31,8 @@ def validate_setup():
|
|
|
31
31
|
Raises ValueError if any node is missing either.
|
|
32
32
|
"""
|
|
33
33
|
for node in nodes_list:
|
|
34
|
+
if node.custom_node:
|
|
35
|
+
continue
|
|
34
36
|
check_if_node_has_add_function_in_flow_graph(node)
|
|
35
37
|
check_if_node_has_input_schema_definition(node)
|
|
36
38
|
|
flowfile_frame/flow_frame.py
CHANGED
|
@@ -27,6 +27,8 @@ from flowfile_frame.join import _normalize_columns_to_list, _create_join_mapping
|
|
|
27
27
|
from flowfile_frame.utils import _check_if_convertible_to_code
|
|
28
28
|
from flowfile_frame.config import logger
|
|
29
29
|
from flowfile_frame.cloud_storage.frame_helpers import add_write_ff_to_cloud_storage
|
|
30
|
+
from collections.abc import Mapping
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
def can_be_expr(param: inspect.Parameter) -> bool:
|
|
@@ -890,13 +892,18 @@ class FlowFrame:
|
|
|
890
892
|
self.flow_graph.add_record_count(node_number_of_records)
|
|
891
893
|
return self._create_child_frame(new_node_id)
|
|
892
894
|
|
|
893
|
-
def
|
|
895
|
+
def rename(self, mapping: Mapping[str, str], *, strict: bool = True,
|
|
896
|
+
description: str = None) -> "FlowFrame":
|
|
897
|
+
"""Rename columns based on a mapping or function."""
|
|
898
|
+
return self.select([col(old_name).alias(new_name) for old_name, new_name in mapping.items()],
|
|
899
|
+
description=description, _keep_missing=True)
|
|
900
|
+
|
|
901
|
+
def select(self, *columns: Union[str, Expr, Selector], description: Optional[str] = None, _keep_missing: bool = False) -> "FlowFrame":
|
|
894
902
|
"""
|
|
895
903
|
Select columns from the frame.
|
|
896
904
|
"""
|
|
897
905
|
columns_iterable = list(_parse_inputs_as_iterable(columns))
|
|
898
906
|
new_node_id = generate_node_id()
|
|
899
|
-
|
|
900
907
|
if (len(columns_iterable) == 1 and isinstance(columns_iterable[0], Expr)
|
|
901
908
|
and str(columns_iterable[0]) == "pl.Expr(len()).alias('number_of_records')"):
|
|
902
909
|
return self._add_number_of_records(new_node_id, description)
|
|
@@ -914,7 +921,6 @@ class FlowFrame:
|
|
|
914
921
|
for expr_input in effective_columns_iterable:
|
|
915
922
|
current_expr_obj = expr_input
|
|
916
923
|
is_simple_col_for_native = False
|
|
917
|
-
|
|
918
924
|
if isinstance(expr_input, str):
|
|
919
925
|
current_expr_obj = col(expr_input)
|
|
920
926
|
selected_col_names_for_native.append(transform_schema.SelectInput(old_name=expr_input))
|
|
@@ -942,14 +948,18 @@ class FlowFrame:
|
|
|
942
948
|
if can_use_native_node:
|
|
943
949
|
existing_cols = self.columns
|
|
944
950
|
selected_col_names = {select_col.old_name for select_col in selected_col_names_for_native}
|
|
945
|
-
|
|
951
|
+
not_selected_columns = [transform_schema.SelectInput(c, keep=_keep_missing) for c in existing_cols if
|
|
946
952
|
c not in selected_col_names]
|
|
947
|
-
selected_col_names_for_native.extend(
|
|
953
|
+
selected_col_names_for_native.extend(not_selected_columns)
|
|
954
|
+
if _keep_missing:
|
|
955
|
+
lookup_selection = {_col.old_name: _col for _col in selected_col_names_for_native}
|
|
956
|
+
selected_col_names_for_native = [lookup_selection.get(_col) for
|
|
957
|
+
_col in existing_cols if _col in lookup_selection]
|
|
948
958
|
select_settings = input_schema.NodeSelect(
|
|
949
959
|
flow_id=self.flow_graph.flow_id,
|
|
950
960
|
node_id=new_node_id,
|
|
951
961
|
select_input=selected_col_names_for_native,
|
|
952
|
-
keep_missing=
|
|
962
|
+
keep_missing=_keep_missing,
|
|
953
963
|
pos_x=200,
|
|
954
964
|
pos_y=100,
|
|
955
965
|
is_setup=True,
|
shared/storage_config.py
CHANGED
|
@@ -9,7 +9,7 @@ from typing import Optional, Literal
|
|
|
9
9
|
|
|
10
10
|
DirectoryOptions = Literal["temp_directory", "logs_directory",
|
|
11
11
|
"system_logs_directory", "database_directory",
|
|
12
|
-
"cache_directory", "flows_directory"]
|
|
12
|
+
"cache_directory", "flows_directory", "user_defined_nodes_directory"]
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class FlowfileStorage:
|
|
@@ -87,6 +87,19 @@ class FlowfileStorage:
|
|
|
87
87
|
# Local development - uploads in ~/.flowfile/uploads
|
|
88
88
|
return self.base_directory / "uploads"
|
|
89
89
|
|
|
90
|
+
@property
|
|
91
|
+
def user_defined_nodes_directory(self) -> Path:
|
|
92
|
+
"""Directory for user-defined custom nodes (user-accessible)."""
|
|
93
|
+
if os.environ.get("RUNNING_IN_DOCKER") == "true":
|
|
94
|
+
return self.user_data_directory / "user_defined_nodes"
|
|
95
|
+
else:
|
|
96
|
+
return self.base_directory / "user_defined_nodes"
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def user_defined_nodes_icons(self) -> Path:
|
|
100
|
+
"""Directory for user-defined custom node icon (user-accessible)."""
|
|
101
|
+
return self.user_defined_nodes_directory / "icons"
|
|
102
|
+
|
|
90
103
|
@property
|
|
91
104
|
def outputs_directory(self) -> Path:
|
|
92
105
|
"""Directory for user outputs (user-accessible)."""
|
|
@@ -134,6 +147,8 @@ class FlowfileStorage:
|
|
|
134
147
|
self.flows_directory,
|
|
135
148
|
self.uploads_directory,
|
|
136
149
|
self.outputs_directory,
|
|
150
|
+
self.user_defined_nodes_directory,
|
|
151
|
+
self.user_defined_nodes_icons,
|
|
137
152
|
]
|
|
138
153
|
|
|
139
154
|
for directory in internal_directories + user_directories:
|
|
@@ -240,4 +255,4 @@ def get_logs_directory() -> str:
|
|
|
240
255
|
|
|
241
256
|
def get_system_logs_directory() -> str:
|
|
242
257
|
"""Get system logs directory path as string."""
|
|
243
|
-
return str(storage.system_logs_directory)
|
|
258
|
+
return str(storage.system_logs_directory)
|
|
File without changes
|
|
File without changes
|