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