Flowfile 0.3.9__py3-none-any.whl → 0.4.0__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.
Potentially problematic release.
This version of Flowfile might be problematic. Click here for more details.
- flowfile/__init__.py +6 -1
- flowfile/api.py +0 -1
- flowfile/web/static/assets/{CloudConnectionManager-c97c25f8.js → CloudConnectionManager-109ecc3c.js} +2 -2
- flowfile/web/static/assets/{CloudStorageReader-f1ff509e.js → CloudStorageReader-19cdd67a.js} +11 -78
- flowfile/web/static/assets/{CloudStorageWriter-034f8b78.js → CloudStorageWriter-48e0ae20.js} +12 -79
- flowfile/web/static/assets/{CloudStorageWriter-49c9a4b2.css → CloudStorageWriter-b0ee067f.css} +24 -24
- flowfile/web/static/assets/ColumnSelector-47996a16.css +10 -0
- flowfile/web/static/assets/ColumnSelector-ecaf7c44.js +83 -0
- flowfile/web/static/assets/ContextMenu-2b348c4c.js +41 -0
- flowfile/web/static/assets/{SettingsSection-9c836ecc.css → ContextMenu-4c74eef1.css} +0 -21
- flowfile/web/static/assets/ContextMenu-63cfa99b.css +26 -0
- flowfile/web/static/assets/ContextMenu-a779eed7.js +41 -0
- flowfile/web/static/assets/ContextMenu-c13f91d0.css +26 -0
- flowfile/web/static/assets/ContextMenu-eca26a03.js +41 -0
- flowfile/web/static/assets/{CrossJoin-41efa4cb.css → CrossJoin-1119d18e.css} +18 -18
- flowfile/web/static/assets/{CrossJoin-9e156ebe.js → CrossJoin-a88f8142.js} +14 -84
- flowfile/web/static/assets/CustomNode-74a37f74.css +32 -0
- flowfile/web/static/assets/CustomNode-cb863dff.js +211 -0
- flowfile/web/static/assets/{DatabaseConnectionSettings-d5c625b3.js → DatabaseConnectionSettings-819d3267.js} +3 -3
- flowfile/web/static/assets/{DatabaseManager-265adc5e.js → DatabaseManager-84ee2834.js} +2 -2
- flowfile/web/static/assets/{DatabaseReader-0b10551e.js → DatabaseReader-060dd412.js} +14 -114
- flowfile/web/static/assets/{DatabaseReader-f50c6558.css → DatabaseReader-ae61773c.css} +0 -27
- flowfile/web/static/assets/{DatabaseWriter-c17c6916.js → DatabaseWriter-7fc7750f.js} +13 -74
- flowfile/web/static/assets/{ExploreData-5bdae813.css → ExploreData-2d0cf4db.css} +8 -14
- flowfile/web/static/assets/ExploreData-82c95991.js +192 -0
- flowfile/web/static/assets/{ExternalSource-3a66556c.js → ExternalSource-e1a6ddc7.js} +8 -79
- flowfile/web/static/assets/{Filter-91ad87e7.js → Filter-8aca894a.js} +12 -85
- flowfile/web/static/assets/{Filter-a9d08ba1.css → Filter-f62091b3.css} +3 -3
- flowfile/web/static/assets/{Formula-29f19d21.css → Formula-bb96803d.css} +4 -4
- flowfile/web/static/assets/{Formula-3c395ab1.js → Formula-e33686d9.js} +18 -85
- flowfile/web/static/assets/{FuzzyMatch-6857de82.css → FuzzyMatch-1010f966.css} +42 -42
- flowfile/web/static/assets/{FuzzyMatch-2df0d230.js → FuzzyMatch-abda150d.js} +16 -87
- flowfile/web/static/assets/{GraphSolver-d285877f.js → GraphSolver-4ecad1d7.js} +13 -159
- flowfile/web/static/assets/GraphSolver-f0cb7bfb.css +22 -0
- flowfile/web/static/assets/{GroupBy-0bd1cc6b.js → GroupBy-656d07f3.js} +12 -75
- flowfile/web/static/assets/{Unique-b5615727.css → GroupBy-b9505323.css} +8 -8
- flowfile/web/static/assets/{Join-5a78a203.js → Join-b84ec849.js} +15 -85
- flowfile/web/static/assets/{Join-f45eff22.css → Join-fd79b451.css} +20 -20
- flowfile/web/static/assets/{ManualInput-a71b52c6.css → ManualInput-3246a08d.css} +20 -20
- flowfile/web/static/assets/{ManualInput-93aef9d6.js → ManualInput-346f4135.js} +11 -82
- flowfile/web/static/assets/MultiSelect-61b98268.js +5 -0
- flowfile/web/static/assets/MultiSelect.vue_vue_type_script_setup_true_lang-2a7c8312.js +63 -0
- flowfile/web/static/assets/NumericInput-e36602c2.js +5 -0
- flowfile/web/static/assets/NumericInput.vue_vue_type_script_setup_true_lang-211a1990.js +35 -0
- flowfile/web/static/assets/Output-ddc9079f.css +37 -0
- flowfile/web/static/assets/{Output-411ecaee.js → Output-eb041599.js} +13 -243
- flowfile/web/static/assets/Pivot-cf333e3d.css +22 -0
- flowfile/web/static/assets/{Pivot-89db4b04.js → Pivot-f5c774f4.js} +14 -138
- flowfile/web/static/assets/PivotValidation-26546cbc.js +61 -0
- flowfile/web/static/assets/PivotValidation-891ddfb0.css +13 -0
- flowfile/web/static/assets/PivotValidation-c46cd420.css +13 -0
- flowfile/web/static/assets/PivotValidation-e150a24b.js +61 -0
- flowfile/web/static/assets/{PolarsCode-a9f974f8.js → PolarsCode-da3a7abf.js} +13 -80
- flowfile/web/static/assets/Read-0c768769.js +243 -0
- flowfile/web/static/assets/Read-6b17491f.css +62 -0
- flowfile/web/static/assets/RecordCount-84736276.js +53 -0
- flowfile/web/static/assets/{RecordId-55ae7d36.js → RecordId-60055e6d.js} +8 -80
- flowfile/web/static/assets/SQLQueryComponent-36cef432.css +27 -0
- flowfile/web/static/assets/SQLQueryComponent-8a486004.js +38 -0
- flowfile/web/static/assets/{Sample-b4a18476.js → Sample-2d662611.js} +8 -77
- flowfile/web/static/assets/{SecretManager-b066d13a.js → SecretManager-ef586cab.js} +2 -2
- flowfile/web/static/assets/{Select-727688dc.js → Select-2e4a6965.js} +11 -85
- flowfile/web/static/assets/SettingsSection-2e4d03c4.css +21 -0
- flowfile/web/static/assets/{SettingsSection-695ac487.js → SettingsSection-310b61c0.js} +2 -40
- flowfile/web/static/assets/SettingsSection-5634f439.js +45 -0
- flowfile/web/static/assets/SettingsSection-5c696bee.css +20 -0
- flowfile/web/static/assets/SettingsSection-71e6b7e3.css +21 -0
- flowfile/web/static/assets/SettingsSection-7c68b19f.js +53 -0
- flowfile/web/static/assets/SingleSelect-7298811a.js +5 -0
- flowfile/web/static/assets/SingleSelect.vue_vue_type_script_setup_true_lang-43807bad.js +62 -0
- flowfile/web/static/assets/SliderInput-53105476.js +40 -0
- flowfile/web/static/assets/SliderInput-b8fb6a8c.css +4 -0
- flowfile/web/static/assets/{GroupBy-ab1ea74b.css → Sort-3643d625.css} +8 -8
- flowfile/web/static/assets/{Sort-be3339a8.js → Sort-4fdebe74.js} +12 -97
- flowfile/web/static/assets/TextInput-28366b7e.js +5 -0
- flowfile/web/static/assets/TextInput.vue_vue_type_script_setup_true_lang-9cad14ba.js +32 -0
- flowfile/web/static/assets/{TextToRows-c92d1ec2.css → TextToRows-5d2c1190.css} +9 -9
- flowfile/web/static/assets/{TextToRows-7b8998da.js → TextToRows-73ffa692.js} +14 -83
- flowfile/web/static/assets/ToggleSwitch-598add30.js +5 -0
- flowfile/web/static/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-f620cd32.js +31 -0
- flowfile/web/static/assets/{UnavailableFields-8b0cb48e.js → UnavailableFields-66239e83.js} +2 -2
- flowfile/web/static/assets/Union-26b10614.js +77 -0
- flowfile/web/static/assets/{Union-8d9ac7f9.css → Union-af6c3d9b.css} +6 -6
- flowfile/web/static/assets/{Unique-af5a80b4.js → Unique-33b9edbb.js} +22 -91
- flowfile/web/static/assets/{Sort-7ccfa0fe.css → Unique-f9fb0809.css} +8 -8
- flowfile/web/static/assets/Unpivot-1e422df3.css +30 -0
- flowfile/web/static/assets/{Unpivot-5195d411.js → Unpivot-ef69d0e2.js} +12 -166
- flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +13 -0
- flowfile/web/static/assets/UnpivotValidation-8658388e.js +51 -0
- flowfile/web/static/assets/{ExploreData-18a4fe52.js → VueGraphicWalker-4d7861f4.js} +4 -264
- flowfile/web/static/assets/VueGraphicWalker-ed5ab88b.css +6 -0
- flowfile/web/static/assets/{api-023d1733.js → api-2d1394bd.js} +1 -1
- flowfile/web/static/assets/{api-cb00cce6.js → api-c908fffe.js} +1 -1
- flowfile/web/static/assets/{designer-6c322d8e.js → designer-1667687d.js} +2201 -705
- flowfile/web/static/assets/{designer-2197d782.css → designer-665e9408.css} +836 -201
- flowfile/web/static/assets/{documentation-4d1fafe1.js → documentation-5eed779e.js} +1 -1
- flowfile/web/static/assets/{dropDown-0b46dd77.js → dropDown-41ebe3c2.js} +1 -1
- flowfile/web/static/assets/{fullEditor-ec4e4f95.js → fullEditor-0670d32d.js} +2 -2
- flowfile/web/static/assets/{genericNodeSettings-def5879b.js → genericNodeSettings-38410ebf.js} +3 -3
- flowfile/web/static/assets/{index-681a3ed0.css → index-50508d4d.css} +8 -0
- flowfile/web/static/assets/{index-683fc198.js → index-5ec791df.js} +210 -31
- flowfile/web/static/assets/outputCsv-059583b6.js +86 -0
- flowfile/web/static/assets/{Output-48f81019.css → outputCsv-9cc59e0b.css} +0 -143
- flowfile/web/static/assets/outputExcel-76b1e02c.js +56 -0
- flowfile/web/static/assets/outputExcel-b41305c0.css +102 -0
- flowfile/web/static/assets/outputParquet-440fd4c7.js +31 -0
- flowfile/web/static/assets/outputParquet-cf8cf3f2.css +4 -0
- flowfile/web/static/assets/readCsv-9813903a.js +178 -0
- flowfile/web/static/assets/readCsv-bca3ed53.css +52 -0
- flowfile/web/static/assets/readExcel-7f40d237.js +203 -0
- flowfile/web/static/assets/readExcel-e1b381ea.css +64 -0
- flowfile/web/static/assets/readParquet-22d56002.js +26 -0
- flowfile/web/static/assets/readParquet-cee068e2.css +19 -0
- flowfile/web/static/assets/{secretApi-baceb6f9.js → secretApi-b3cb072e.js} +1 -1
- flowfile/web/static/assets/{selectDynamic-de91449a.js → selectDynamic-7ad95bca.js} +7 -7
- flowfile/web/static/assets/{selectDynamic-b062bc9b.css → selectDynamic-aa913ff4.css} +16 -16
- flowfile/web/static/assets/user-defined-icon-0ae16c90.png +0 -0
- flowfile/web/static/assets/{vue-codemirror.esm-dc5e3348.js → vue-codemirror.esm-b1dfaa46.js} +59 -33
- flowfile/web/static/assets/{vue-content-loader.es-ba94b82f.js → vue-content-loader.es-22bac17c.js} +1 -1
- flowfile/web/static/index.html +2 -2
- {flowfile-0.3.9.dist-info → flowfile-0.4.0.dist-info}/METADATA +1 -1
- {flowfile-0.3.9.dist-info → flowfile-0.4.0.dist-info}/RECORD +160 -102
- flowfile_core/configs/flow_logger.py +5 -13
- flowfile_core/configs/node_store/__init__.py +30 -0
- flowfile_core/configs/node_store/nodes.py +383 -99
- flowfile_core/configs/node_store/user_defined_node_registry.py +193 -0
- flowfile_core/configs/settings.py +2 -1
- flowfile_core/database/connection.py +5 -21
- flowfile_core/fileExplorer/funcs.py +239 -121
- flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +4 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +19 -34
- flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +36 -0
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +28 -8
- flowfile_core/flowfile/flow_graph.py +117 -34
- flowfile_core/flowfile/flow_node/flow_node.py +45 -13
- flowfile_core/flowfile/handler.py +22 -3
- 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 +8 -4
- flowfile_core/flowfile/setting_generator/settings.py +0 -1
- flowfile_core/main.py +5 -1
- flowfile_core/routes/routes.py +73 -28
- flowfile_core/routes/user_defined_components.py +55 -0
- flowfile_core/schemas/input_schema.py +7 -1
- flowfile_core/schemas/output_model.py +5 -2
- flowfile_core/schemas/schemas.py +8 -3
- flowfile_core/schemas/transform_schema.py +1 -0
- flowfile_core/utils/validate_setup.py +3 -1
- flowfile_worker/__init__.py +6 -35
- flowfile_worker/main.py +5 -2
- flowfile_worker/routes.py +47 -5
- shared/__init__.py +15 -0
- shared/storage_config.py +258 -0
- flowfile/web/static/assets/GraphSolver-17fd26db.css +0 -68
- flowfile/web/static/assets/Pivot-f415e85f.css +0 -35
- flowfile/web/static/assets/Read-80dc1675.css +0 -197
- flowfile/web/static/assets/Read-c3b1929c.js +0 -701
- flowfile/web/static/assets/RecordCount-4e95f98e.js +0 -122
- flowfile/web/static/assets/Union-89fd73dc.js +0 -146
- flowfile/web/static/assets/Unpivot-246e9bbd.css +0 -77
- flowfile/web/static/assets/nodeTitle-a16db7c3.js +0 -227
- flowfile/web/static/assets/nodeTitle-f4b12bcb.css +0 -134
- {flowfile-0.3.9.dist-info → flowfile-0.4.0.dist-info}/LICENSE +0 -0
- {flowfile-0.3.9.dist-info → flowfile-0.4.0.dist-info}/WHEEL +0 -0
- {flowfile-0.3.9.dist-info → flowfile-0.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -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)
|
|
@@ -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, Callable
|
|
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
|
+
)
|