Flowfile 0.3.9__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. flowfile/__init__.py +8 -1
  2. flowfile/api.py +1 -3
  3. flowfile/web/static/assets/{CloudConnectionManager-c97c25f8.js → CloudConnectionManager-0dfba9f2.js} +2 -2
  4. flowfile/web/static/assets/{CloudStorageReader-f1ff509e.js → CloudStorageReader-d5b1b6c9.js} +11 -78
  5. flowfile/web/static/assets/{CloudStorageWriter-034f8b78.js → CloudStorageWriter-00d87aad.js} +12 -79
  6. flowfile/web/static/assets/{CloudStorageWriter-49c9a4b2.css → CloudStorageWriter-b0ee067f.css} +24 -24
  7. flowfile/web/static/assets/ColumnSelector-4685e75d.js +83 -0
  8. flowfile/web/static/assets/ColumnSelector-47996a16.css +10 -0
  9. flowfile/web/static/assets/ContextMenu-23e909da.js +41 -0
  10. flowfile/web/static/assets/{SettingsSection-9c836ecc.css → ContextMenu-4c74eef1.css} +0 -21
  11. flowfile/web/static/assets/ContextMenu-63cfa99b.css +26 -0
  12. flowfile/web/static/assets/ContextMenu-70ae0c79.js +41 -0
  13. flowfile/web/static/assets/ContextMenu-c13f91d0.css +26 -0
  14. flowfile/web/static/assets/ContextMenu-f149cf7c.js +41 -0
  15. flowfile/web/static/assets/{CrossJoin-41efa4cb.css → CrossJoin-1119d18e.css} +18 -18
  16. flowfile/web/static/assets/{CrossJoin-9e156ebe.js → CrossJoin-702a3edd.js} +14 -84
  17. flowfile/web/static/assets/CustomNode-74a37f74.css +32 -0
  18. flowfile/web/static/assets/CustomNode-b1519993.js +211 -0
  19. flowfile/web/static/assets/{DatabaseConnectionSettings-d5c625b3.js → DatabaseConnectionSettings-6f3e4ea5.js} +3 -3
  20. flowfile/web/static/assets/{DatabaseManager-265adc5e.js → DatabaseManager-cf5ef661.js} +2 -2
  21. flowfile/web/static/assets/{DatabaseReader-f50c6558.css → DatabaseReader-ae61773c.css} +0 -27
  22. flowfile/web/static/assets/{DatabaseReader-0b10551e.js → DatabaseReader-d38c7295.js} +14 -114
  23. flowfile/web/static/assets/{DatabaseWriter-c17c6916.js → DatabaseWriter-b04ef46a.js} +13 -74
  24. flowfile/web/static/assets/{ExploreData-5bdae813.css → ExploreData-2d0cf4db.css} +8 -14
  25. flowfile/web/static/assets/ExploreData-5fa10ed8.js +192 -0
  26. flowfile/web/static/assets/{ExternalSource-3a66556c.js → ExternalSource-d39af878.js} +8 -79
  27. flowfile/web/static/assets/{Filter-91ad87e7.js → Filter-9b6d08db.js} +12 -85
  28. flowfile/web/static/assets/{Filter-a9d08ba1.css → Filter-f62091b3.css} +3 -3
  29. flowfile/web/static/assets/{Formula-3c395ab1.js → Formula-6b04fb1d.js} +20 -87
  30. flowfile/web/static/assets/{Formula-29f19d21.css → Formula-bb96803d.css} +4 -4
  31. flowfile/web/static/assets/{FuzzyMatch-6857de82.css → FuzzyMatch-1010f966.css} +42 -42
  32. flowfile/web/static/assets/{FuzzyMatch-2df0d230.js → FuzzyMatch-999521f4.js} +16 -87
  33. flowfile/web/static/assets/{GraphSolver-d285877f.js → GraphSolver-17dd2198.js} +13 -159
  34. flowfile/web/static/assets/GraphSolver-f0cb7bfb.css +22 -0
  35. flowfile/web/static/assets/{GroupBy-0bd1cc6b.js → GroupBy-6b039e18.js} +12 -75
  36. flowfile/web/static/assets/{Unique-b5615727.css → GroupBy-b9505323.css} +8 -8
  37. flowfile/web/static/assets/{Join-5a78a203.js → Join-24d0f113.js} +15 -85
  38. flowfile/web/static/assets/{Join-f45eff22.css → Join-fd79b451.css} +20 -20
  39. flowfile/web/static/assets/{ManualInput-a71b52c6.css → ManualInput-3246a08d.css} +20 -20
  40. flowfile/web/static/assets/{ManualInput-93aef9d6.js → ManualInput-34639209.js} +11 -82
  41. flowfile/web/static/assets/MultiSelect-0e8724a3.js +5 -0
  42. flowfile/web/static/assets/MultiSelect.vue_vue_type_script_setup_true_lang-b0e538c2.js +63 -0
  43. flowfile/web/static/assets/NumericInput-3d63a470.js +5 -0
  44. flowfile/web/static/assets/NumericInput.vue_vue_type_script_setup_true_lang-e0edeccc.js +35 -0
  45. flowfile/web/static/assets/Output-283fe388.css +37 -0
  46. flowfile/web/static/assets/{Output-411ecaee.js → Output-edea9802.js} +62 -273
  47. flowfile/web/static/assets/{Pivot-89db4b04.js → Pivot-61d19301.js} +14 -138
  48. flowfile/web/static/assets/Pivot-cf333e3d.css +22 -0
  49. flowfile/web/static/assets/PivotValidation-891ddfb0.css +13 -0
  50. flowfile/web/static/assets/PivotValidation-c46cd420.css +13 -0
  51. flowfile/web/static/assets/PivotValidation-de9f43fe.js +61 -0
  52. flowfile/web/static/assets/PivotValidation-f97fec5b.js +61 -0
  53. flowfile/web/static/assets/{PolarsCode-a9f974f8.js → PolarsCode-bc3c9984.js} +13 -80
  54. flowfile/web/static/assets/Read-64a3f259.js +218 -0
  55. flowfile/web/static/assets/Read-e808b239.css +62 -0
  56. flowfile/web/static/assets/RecordCount-3d5039be.js +53 -0
  57. flowfile/web/static/assets/{RecordId-55ae7d36.js → RecordId-597510e0.js} +8 -80
  58. flowfile/web/static/assets/SQLQueryComponent-36cef432.css +27 -0
  59. flowfile/web/static/assets/SQLQueryComponent-df51adbe.js +38 -0
  60. flowfile/web/static/assets/{Sample-b4a18476.js → Sample-4be0a507.js} +8 -77
  61. flowfile/web/static/assets/{SecretManager-b066d13a.js → SecretManager-4839be57.js} +2 -2
  62. flowfile/web/static/assets/{Select-727688dc.js → Select-9b72f201.js} +11 -85
  63. flowfile/web/static/assets/SettingsSection-2e4d03c4.css +21 -0
  64. flowfile/web/static/assets/SettingsSection-5c696bee.css +20 -0
  65. flowfile/web/static/assets/SettingsSection-71e6b7e3.css +21 -0
  66. flowfile/web/static/assets/SettingsSection-7ded385d.js +45 -0
  67. flowfile/web/static/assets/{SettingsSection-695ac487.js → SettingsSection-e1e9c953.js} +2 -40
  68. flowfile/web/static/assets/SettingsSection-f0f75a42.js +53 -0
  69. flowfile/web/static/assets/SingleSelect-6c777aac.js +5 -0
  70. flowfile/web/static/assets/SingleSelect.vue_vue_type_script_setup_true_lang-33e3ff9b.js +62 -0
  71. flowfile/web/static/assets/SliderInput-7cb93e62.js +40 -0
  72. flowfile/web/static/assets/SliderInput-b8fb6a8c.css +4 -0
  73. flowfile/web/static/assets/{GroupBy-ab1ea74b.css → Sort-3643d625.css} +8 -8
  74. flowfile/web/static/assets/{Sort-be3339a8.js → Sort-6cbde21a.js} +12 -97
  75. flowfile/web/static/assets/TextInput-d9a40c11.js +5 -0
  76. flowfile/web/static/assets/TextInput.vue_vue_type_script_setup_true_lang-5896c375.js +32 -0
  77. flowfile/web/static/assets/{TextToRows-c92d1ec2.css → TextToRows-5d2c1190.css} +9 -9
  78. flowfile/web/static/assets/{TextToRows-7b8998da.js → TextToRows-c4fcbf4d.js} +14 -83
  79. flowfile/web/static/assets/ToggleSwitch-4ef91d19.js +5 -0
  80. flowfile/web/static/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-38478c20.js +31 -0
  81. flowfile/web/static/assets/{UnavailableFields-8b0cb48e.js → UnavailableFields-a03f512c.js} +2 -2
  82. flowfile/web/static/assets/{Union-8d9ac7f9.css → Union-af6c3d9b.css} +6 -6
  83. flowfile/web/static/assets/Union-bfe9b996.js +77 -0
  84. flowfile/web/static/assets/{Unique-af5a80b4.js → Unique-5d023a27.js} +23 -104
  85. flowfile/web/static/assets/{Sort-7ccfa0fe.css → Unique-f9fb0809.css} +8 -8
  86. flowfile/web/static/assets/Unpivot-1e422df3.css +30 -0
  87. flowfile/web/static/assets/{Unpivot-5195d411.js → Unpivot-91cc5354.js} +12 -166
  88. flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +13 -0
  89. flowfile/web/static/assets/UnpivotValidation-7ee2de44.js +51 -0
  90. flowfile/web/static/assets/{ExploreData-18a4fe52.js → VueGraphicWalker-e51b9924.js} +4 -264
  91. flowfile/web/static/assets/VueGraphicWalker-ed5ab88b.css +6 -0
  92. flowfile/web/static/assets/{api-cb00cce6.js → api-c1bad5ca.js} +1 -1
  93. flowfile/web/static/assets/{api-023d1733.js → api-cf1221f0.js} +1 -1
  94. flowfile/web/static/assets/{designer-2197d782.css → designer-8da3ba3a.css} +859 -201
  95. flowfile/web/static/assets/{designer-6c322d8e.js → designer-9633482a.js} +2297 -733
  96. flowfile/web/static/assets/{documentation-4d1fafe1.js → documentation-ca400224.js} +1 -1
  97. flowfile/web/static/assets/{dropDown-0b46dd77.js → dropDown-614b998d.js} +1 -1
  98. flowfile/web/static/assets/{fullEditor-ec4e4f95.js → fullEditor-f7971590.js} +2 -2
  99. flowfile/web/static/assets/{genericNodeSettings-def5879b.js → genericNodeSettings-4fe5f36b.js} +3 -3
  100. flowfile/web/static/assets/{index-681a3ed0.css → index-50508d4d.css} +8 -0
  101. flowfile/web/static/assets/{index-683fc198.js → index-5429bbf8.js} +208 -31
  102. flowfile/web/static/assets/nodeInput-5d0d6b79.js +41 -0
  103. flowfile/web/static/assets/outputCsv-076b85ab.js +86 -0
  104. flowfile/web/static/assets/{Output-48f81019.css → outputCsv-9cc59e0b.css} +0 -143
  105. flowfile/web/static/assets/outputExcel-0fd17dbe.js +56 -0
  106. flowfile/web/static/assets/outputExcel-b41305c0.css +102 -0
  107. flowfile/web/static/assets/outputParquet-b61e0847.js +31 -0
  108. flowfile/web/static/assets/outputParquet-cf8cf3f2.css +4 -0
  109. flowfile/web/static/assets/readCsv-a8bb8b61.js +179 -0
  110. flowfile/web/static/assets/readCsv-c767cb37.css +52 -0
  111. flowfile/web/static/assets/readExcel-67b4aee0.js +201 -0
  112. flowfile/web/static/assets/readExcel-806d2826.css +64 -0
  113. flowfile/web/static/assets/readParquet-48c81530.css +19 -0
  114. flowfile/web/static/assets/readParquet-92ce1dbc.js +23 -0
  115. flowfile/web/static/assets/{secretApi-baceb6f9.js → secretApi-68435402.js} +1 -1
  116. flowfile/web/static/assets/{selectDynamic-de91449a.js → selectDynamic-92e25ee3.js} +7 -7
  117. flowfile/web/static/assets/{selectDynamic-b062bc9b.css → selectDynamic-aa913ff4.css} +16 -16
  118. flowfile/web/static/assets/user-defined-icon-0ae16c90.png +0 -0
  119. flowfile/web/static/assets/{vue-codemirror.esm-dc5e3348.js → vue-codemirror.esm-41b0e0d7.js} +65 -36
  120. flowfile/web/static/assets/{vue-content-loader.es-ba94b82f.js → vue-content-loader.es-2c8e608f.js} +1 -1
  121. flowfile/web/static/index.html +2 -2
  122. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/METADATA +5 -3
  123. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/RECORD +191 -121
  124. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/WHEEL +1 -1
  125. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info}/entry_points.txt +1 -0
  126. flowfile_core/__init__.py +3 -0
  127. flowfile_core/configs/flow_logger.py +5 -13
  128. flowfile_core/configs/node_store/__init__.py +30 -0
  129. flowfile_core/configs/node_store/nodes.py +383 -99
  130. flowfile_core/configs/node_store/user_defined_node_registry.py +193 -0
  131. flowfile_core/configs/settings.py +2 -1
  132. flowfile_core/database/connection.py +5 -21
  133. flowfile_core/fileExplorer/funcs.py +239 -121
  134. flowfile_core/flowfile/analytics/analytics_processor.py +1 -0
  135. flowfile_core/flowfile/code_generator/code_generator.py +62 -64
  136. flowfile_core/flowfile/flow_data_engine/create/funcs.py +73 -56
  137. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +77 -86
  138. flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +4 -0
  139. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +19 -34
  140. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +36 -0
  141. flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +23 -23
  142. flowfile_core/flowfile/flow_data_engine/join/utils.py +1 -1
  143. flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +9 -4
  144. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +212 -86
  145. flowfile_core/flowfile/flow_data_engine/utils.py +2 -0
  146. flowfile_core/flowfile/flow_graph.py +240 -54
  147. flowfile_core/flowfile/flow_node/flow_node.py +48 -13
  148. flowfile_core/flowfile/flow_node/models.py +2 -1
  149. flowfile_core/flowfile/handler.py +24 -5
  150. flowfile_core/flowfile/manage/compatibility_enhancements.py +404 -41
  151. flowfile_core/flowfile/manage/io_flowfile.py +394 -0
  152. flowfile_core/flowfile/node_designer/__init__.py +47 -0
  153. flowfile_core/flowfile/node_designer/_type_registry.py +197 -0
  154. flowfile_core/flowfile/node_designer/custom_node.py +371 -0
  155. flowfile_core/flowfile/node_designer/ui_components.py +277 -0
  156. flowfile_core/flowfile/schema_callbacks.py +17 -10
  157. flowfile_core/flowfile/setting_generator/settings.py +15 -10
  158. flowfile_core/main.py +5 -1
  159. flowfile_core/routes/routes.py +73 -30
  160. flowfile_core/routes/user_defined_components.py +55 -0
  161. flowfile_core/schemas/cloud_storage_schemas.py +0 -2
  162. flowfile_core/schemas/input_schema.py +228 -65
  163. flowfile_core/schemas/output_model.py +5 -2
  164. flowfile_core/schemas/schemas.py +153 -35
  165. flowfile_core/schemas/transform_schema.py +1083 -412
  166. flowfile_core/schemas/yaml_types.py +103 -0
  167. flowfile_core/types.py +156 -0
  168. flowfile_core/utils/validate_setup.py +3 -1
  169. flowfile_frame/__init__.py +3 -1
  170. flowfile_frame/flow_frame.py +31 -24
  171. flowfile_frame/flow_frame_methods.py +12 -9
  172. flowfile_worker/__init__.py +9 -35
  173. flowfile_worker/create/__init__.py +3 -21
  174. flowfile_worker/create/funcs.py +68 -56
  175. flowfile_worker/create/models.py +130 -62
  176. flowfile_worker/main.py +5 -2
  177. flowfile_worker/routes.py +52 -13
  178. shared/__init__.py +15 -0
  179. shared/storage_config.py +258 -0
  180. tools/migrate/README.md +56 -0
  181. tools/migrate/__init__.py +12 -0
  182. tools/migrate/__main__.py +131 -0
  183. tools/migrate/legacy_schemas.py +621 -0
  184. tools/migrate/migrate.py +598 -0
  185. tools/migrate/tests/__init__.py +0 -0
  186. tools/migrate/tests/conftest.py +23 -0
  187. tools/migrate/tests/test_migrate.py +627 -0
  188. tools/migrate/tests/test_migration_e2e.py +1010 -0
  189. tools/migrate/tests/test_node_migrations.py +813 -0
  190. flowfile/web/static/assets/GraphSolver-17fd26db.css +0 -68
  191. flowfile/web/static/assets/Pivot-f415e85f.css +0 -35
  192. flowfile/web/static/assets/Read-80dc1675.css +0 -197
  193. flowfile/web/static/assets/Read-c3b1929c.js +0 -701
  194. flowfile/web/static/assets/RecordCount-4e95f98e.js +0 -122
  195. flowfile/web/static/assets/Union-89fd73dc.js +0 -146
  196. flowfile/web/static/assets/Unpivot-246e9bbd.css +0 -77
  197. flowfile/web/static/assets/nodeTitle-a16db7c3.js +0 -227
  198. flowfile/web/static/assets/nodeTitle-f4b12bcb.css +0 -134
  199. flowfile_core/flowfile/manage/open_flowfile.py +0 -135
  200. {flowfile-0.3.9.dist-info → flowfile-0.5.1.dist-info/licenses}/LICENSE +0 -0
  201. /flowfile_core/flowfile/manage/manage_flowfile.py → /tools/__init__.py +0 -0
@@ -1,70 +1,433 @@
1
+ """
2
+ Compatibility enhancements for opening old flowfile versions.
3
+ Migrates old schema structures to new ones during file load.
4
+ """
5
+ import pickle
6
+ from typing import Any
7
+ from pathlib import Path
8
+
1
9
  from flowfile_core.schemas import schemas, input_schema
10
+ from tools.migrate.legacy_schemas import LEGACY_CLASS_MAP
11
+
12
+
13
+ # =============================================================================
14
+ # LEGACY PICKLE LOADING
15
+ # =============================================================================
16
+
17
+ class LegacyUnpickler(pickle.Unpickler):
18
+ """
19
+ Custom unpickler that redirects class lookups to legacy dataclass definitions.
20
+
21
+ When loading old .flowfile pickles, transform_schema classes were dataclasses.
22
+ Now they're Pydantic BaseModels. This unpickler intercepts those classes and
23
+ loads them as the legacy dataclass versions, which can then be migrated.
24
+ """
25
+
26
+ def find_class(self, module: str, name: str):
27
+ """Override to redirect transform_schema dataclasses to legacy definitions."""
28
+ if name in LEGACY_CLASS_MAP:
29
+ return LEGACY_CLASS_MAP[name]
30
+ return super().find_class(module, name)
31
+
32
+
33
+ def load_flowfile_pickle(path: str) -> Any:
34
+ """
35
+ Load a flowfile pickle using legacy-compatible unpickling.
36
+
37
+ This handles old flowfiles where transform_schema classes were dataclasses
38
+ by loading them as legacy dataclass instances, which can then be migrated
39
+ to the new Pydantic BaseModel versions.
40
+
41
+ Args:
42
+ path: Path to the .flowfile pickle
43
+
44
+ Returns:
45
+ The deserialized FlowInformation object
46
+ """
47
+ resolved_path = Path(path).resolve()
48
+ with open(resolved_path, 'rb') as f:
49
+ return LegacyUnpickler(f).load()
2
50
 
3
51
 
52
+ # =============================================================================
53
+ # DATACLASS DETECTION AND MIGRATION
54
+ # =============================================================================
55
+
56
+ def _is_dataclass_instance(obj: Any) -> bool:
57
+ """Check if an object is a dataclass instance (not a Pydantic model)."""
58
+ return hasattr(obj, '__dataclass_fields__') and not hasattr(obj, 'model_dump')
59
+
60
+
61
+ def _migrate_dataclass_to_basemodel(obj: Any, model_class: type) -> Any:
62
+ """Convert a dataclass instance to a Pydantic BaseModel instance."""
63
+ if obj is None:
64
+ return None
65
+
66
+ if not _is_dataclass_instance(obj):
67
+ return obj # Already a BaseModel or dict
68
+
69
+ from dataclasses import fields, asdict
70
+ try:
71
+ data = asdict(obj)
72
+ except Exception:
73
+ # Fallback: manually extract attributes
74
+ data = {f.name: getattr(obj, f.name, None) for f in fields(obj)}
75
+
76
+ return model_class.model_validate(data)
77
+
78
+
79
+ # =============================================================================
80
+ # NODE-SPECIFIC COMPATIBILITY FUNCTIONS
81
+ # =============================================================================
82
+
4
83
  def ensure_compatibility_node_read(node_read: input_schema.NodeRead):
5
- if hasattr(node_read, 'received_file'):
6
- if not hasattr(node_read.received_file, 'fields'):
7
- print('setting fields')
8
- setattr(node_read.received_file, 'fields', [])
84
+ """Migrate old NodeRead/ReceivedTable structure to new table_settings format."""
85
+ if not hasattr(node_read, 'received_file') or node_read.received_file is None:
86
+ return
87
+
88
+ received_file = node_read.received_file
89
+
90
+ # Ensure fields list exists
91
+ if not hasattr(received_file, 'fields'):
92
+ setattr(received_file, 'fields', [])
93
+
94
+ # Check if already migrated (has table_settings as proper object, not dict)
95
+ if hasattr(received_file, 'table_settings') and received_file.table_settings is not None:
96
+ if not isinstance(received_file.table_settings, dict):
97
+ return
98
+
99
+ # Determine file_type - use existing or infer from attributes
100
+ file_type = getattr(received_file, 'file_type', None)
101
+ if file_type is None:
102
+ path = getattr(received_file, 'path', '') or ''
103
+ if path.endswith('.parquet'):
104
+ file_type = 'parquet'
105
+ elif path.endswith(('.xlsx', '.xls')):
106
+ file_type = 'excel'
107
+ elif path.endswith('.json'):
108
+ file_type = 'json'
109
+ else:
110
+ file_type = 'csv'
111
+
112
+ # Build table_settings based on file_type, extracting old flat attributes
113
+ table_settings_dict = _build_input_table_settings(received_file, file_type)
114
+
115
+ # Re-validate the entire ReceivedTable to get proper Pydantic model
116
+ received_file_dict = received_file.model_dump()
117
+ received_file_dict['file_type'] = file_type
118
+ received_file_dict['table_settings'] = table_settings_dict
119
+
120
+ # Create new validated ReceivedTable and replace
121
+ new_received_file = input_schema.ReceivedTable.model_validate(received_file_dict)
122
+ node_read.received_file = new_received_file
123
+
124
+
125
+ def _build_input_table_settings(received_file: Any, file_type: str) -> dict:
126
+ """Build appropriate table_settings dict from old flat attributes."""
127
+
128
+ if file_type == 'csv':
129
+ return {
130
+ 'file_type': 'csv',
131
+ 'reference': getattr(received_file, 'reference', ''),
132
+ 'starting_from_line': getattr(received_file, 'starting_from_line', 0),
133
+ 'delimiter': getattr(received_file, 'delimiter', ','),
134
+ 'has_headers': getattr(received_file, 'has_headers', True),
135
+ 'encoding': getattr(received_file, 'encoding', 'utf-8'),
136
+ 'parquet_ref': getattr(received_file, 'parquet_ref', None),
137
+ 'row_delimiter': getattr(received_file, 'row_delimiter', '\n'),
138
+ 'quote_char': getattr(received_file, 'quote_char', '"'),
139
+ 'infer_schema_length': getattr(received_file, 'infer_schema_length', 10_000),
140
+ 'truncate_ragged_lines': getattr(received_file, 'truncate_ragged_lines', False),
141
+ 'ignore_errors': getattr(received_file, 'ignore_errors', False),
142
+ }
143
+
144
+ elif file_type == 'json':
145
+ return {
146
+ 'file_type': 'json',
147
+ 'reference': getattr(received_file, 'reference', ''),
148
+ 'starting_from_line': getattr(received_file, 'starting_from_line', 0),
149
+ 'delimiter': getattr(received_file, 'delimiter', ','),
150
+ 'has_headers': getattr(received_file, 'has_headers', True),
151
+ 'encoding': getattr(received_file, 'encoding', 'utf-8'),
152
+ 'parquet_ref': getattr(received_file, 'parquet_ref', None),
153
+ 'row_delimiter': getattr(received_file, 'row_delimiter', '\n'),
154
+ 'quote_char': getattr(received_file, 'quote_char', '"'),
155
+ 'infer_schema_length': getattr(received_file, 'infer_schema_length', 10_000),
156
+ 'truncate_ragged_lines': getattr(received_file, 'truncate_ragged_lines', False),
157
+ 'ignore_errors': getattr(received_file, 'ignore_errors', False),
158
+ }
159
+
160
+ elif file_type == 'parquet':
161
+ return {'file_type': 'parquet'}
162
+
163
+ elif file_type == 'excel':
164
+ return {
165
+ 'file_type': 'excel',
166
+ 'sheet_name': getattr(received_file, 'sheet_name', None),
167
+ 'start_row': getattr(received_file, 'start_row', 0),
168
+ 'start_column': getattr(received_file, 'start_column', 0),
169
+ 'end_row': getattr(received_file, 'end_row', 0),
170
+ 'end_column': getattr(received_file, 'end_column', 0),
171
+ 'has_headers': getattr(received_file, 'has_headers', True),
172
+ 'type_inference': getattr(received_file, 'type_inference', False),
173
+ }
174
+
175
+ # Default to csv settings
176
+ return {'file_type': 'csv', 'delimiter': ',', 'encoding': 'utf-8', 'has_headers': True}
9
177
 
10
178
 
11
179
  def ensure_compatibility_node_output(node_output: input_schema.NodeOutput):
12
- if hasattr(node_output, 'output_settings'):
13
- if not hasattr(node_output.output_settings, 'abs_file_path'):
14
- new_output_settings = input_schema.OutputSettings.model_validate(node_output.output_settings.model_dump())
15
- setattr(node_output, 'output_settings', new_output_settings)
180
+ """Migrate old OutputSettings structure to new table_settings format."""
181
+ if not hasattr(node_output, 'output_settings') or node_output.output_settings is None:
182
+ return
183
+
184
+ output_settings = node_output.output_settings
185
+
186
+ # Check if already migrated (has table_settings as proper object, not dict)
187
+ if hasattr(output_settings, 'table_settings') and output_settings.table_settings is not None:
188
+ if not isinstance(output_settings.table_settings, dict):
189
+ return
190
+
191
+ # Migrate from old separate fields to new table_settings
192
+ file_type = getattr(output_settings, 'file_type', 'csv')
193
+ table_settings_dict = _build_output_table_settings(output_settings, file_type)
194
+
195
+ # Re-validate the entire OutputSettings to get proper Pydantic model
196
+ output_settings_dict = output_settings.model_dump()
197
+ output_settings_dict['table_settings'] = table_settings_dict
198
+
199
+ # Remove old fields if they exist
200
+ for old_field in ['output_csv_table', 'output_parquet_table', 'output_excel_table']:
201
+ output_settings_dict.pop(old_field, None)
202
+
203
+ # Create new validated OutputSettings and replace
204
+ new_output_settings = input_schema.OutputSettings.model_validate(output_settings_dict)
205
+ node_output.output_settings = new_output_settings
206
+
207
+
208
+ def _build_output_table_settings(output_settings: Any, file_type: str) -> dict:
209
+ """Build appropriate output table_settings from old separate table fields."""
210
+
211
+ if file_type == 'csv':
212
+ old_csv = getattr(output_settings, 'output_csv_table', None)
213
+ if old_csv is not None:
214
+ return {
215
+ 'file_type': 'csv',
216
+ 'delimiter': getattr(old_csv, 'delimiter', ','),
217
+ 'encoding': getattr(old_csv, 'encoding', 'utf-8'),
218
+ }
219
+ return {'file_type': 'csv', 'delimiter': ',', 'encoding': 'utf-8'}
220
+
221
+ elif file_type == 'parquet':
222
+ return {'file_type': 'parquet'}
223
+
224
+ elif file_type == 'excel':
225
+ old_excel = getattr(output_settings, 'output_excel_table', None)
226
+ if old_excel is not None:
227
+ return {
228
+ 'file_type': 'excel',
229
+ 'sheet_name': getattr(old_excel, 'sheet_name', 'Sheet1'),
230
+ }
231
+ return {'file_type': 'excel', 'sheet_name': 'Sheet1'}
232
+
233
+ return {'file_type': 'csv', 'delimiter': ',', 'encoding': 'utf-8'}
16
234
 
17
235
 
18
236
  def ensure_compatibility_node_select(node_select: input_schema.NodeSelect):
19
- if hasattr(node_select, 'select_input'):
20
- if any(not hasattr(select_input, 'position') for select_input in node_select.select_input):
21
- for _index, select_input in enumerate(node_select.select_input):
22
- setattr(select_input, 'position', _index)
23
- if not hasattr(node_select, 'sorted_by'):
24
- setattr(node_select, 'sorted_by', 'none')
237
+ """Ensure NodeSelect has position attributes, sorted_by field, and handle dataclass migrations."""
238
+ if not hasattr(node_select, 'select_input'):
239
+ return
240
+
241
+ # Handle dataclass -> BaseModel migration for select_input items
242
+ if node_select.select_input:
243
+ from flowfile_core.schemas import transform_schema
244
+ new_select_input = []
245
+ needs_migration = any(_is_dataclass_instance(si) for si in node_select.select_input)
246
+
247
+ if needs_migration:
248
+ for si in node_select.select_input:
249
+ if _is_dataclass_instance(si):
250
+ new_si = _migrate_dataclass_to_basemodel(si, transform_schema.SelectInput)
251
+ new_select_input.append(new_si)
252
+ else:
253
+ new_select_input.append(si)
254
+ node_select.select_input = new_select_input
255
+
256
+ # Ensure position attributes exist
257
+ if any(not hasattr(select_input, 'position') for select_input in node_select.select_input):
258
+ for _index, select_input in enumerate(node_select.select_input):
259
+ setattr(select_input, 'position', _index)
260
+
261
+ if not hasattr(node_select, 'sorted_by'):
262
+ setattr(node_select, 'sorted_by', 'none')
25
263
 
26
264
 
27
265
  def ensure_compatibility_node_joins(node_settings: input_schema.NodeFuzzyMatch | input_schema.NodeJoin):
28
- if any(not hasattr(r, 'position') for r in node_settings.join_input.right_select.renames):
29
- for _index, select_input in enumerate(node_settings.join_input.right_select.renames +
30
- node_settings.join_input.left_select.renames):
266
+ """Ensure join nodes have position attributes on renames and handle dataclass migrations."""
267
+ if not hasattr(node_settings, 'join_input') or node_settings.join_input is None:
268
+ return
269
+
270
+ join_input = node_settings.join_input
271
+
272
+ # Check if right_select and left_select exist
273
+ if not hasattr(join_input, 'right_select') or not hasattr(join_input, 'left_select'):
274
+ return
275
+
276
+ from flowfile_core.schemas import transform_schema
277
+
278
+ # Handle dataclass -> BaseModel migration for join_mapping
279
+ if hasattr(join_input, 'join_mapping') and join_input.join_mapping:
280
+ new_mapping = []
281
+ for jm in join_input.join_mapping:
282
+ if _is_dataclass_instance(jm):
283
+ new_jm = _migrate_dataclass_to_basemodel(jm, transform_schema.JoinMap)
284
+ new_mapping.append(new_jm)
285
+ else:
286
+ new_mapping.append(jm)
287
+ join_input.join_mapping = new_mapping
288
+
289
+ # Handle dataclass -> BaseModel migration for renames in selects
290
+ for select_attr in ['right_select', 'left_select']:
291
+ select = getattr(join_input, select_attr, None)
292
+ if select is None:
293
+ continue
294
+
295
+ renames = getattr(select, 'renames', []) or []
296
+ if renames and any(_is_dataclass_instance(r) for r in renames):
297
+ new_renames = []
298
+ for r in renames:
299
+ if _is_dataclass_instance(r):
300
+ new_r = _migrate_dataclass_to_basemodel(r, transform_schema.SelectInput)
301
+ new_renames.append(new_r)
302
+ else:
303
+ new_renames.append(r)
304
+ select.renames = new_renames
305
+
306
+ right_renames = getattr(join_input.right_select, 'renames', []) or []
307
+ left_renames = getattr(join_input.left_select, 'renames', []) or []
308
+
309
+ # Ensure position attributes exist
310
+ if any(not hasattr(r, 'position') for r in right_renames + left_renames):
311
+ for _index, select_input in enumerate(right_renames + left_renames):
31
312
  setattr(select_input, 'position', _index)
32
313
 
33
314
 
34
315
  def ensure_description(node: input_schema.NodeBase):
316
+ """Ensure node has description field."""
35
317
  if not hasattr(node, 'description'):
36
318
  setattr(node, 'description', '')
37
319
 
38
320
 
39
321
  def ensure_compatibility_node_polars(node_polars: input_schema.NodePolarsCode):
322
+ """Migrate old NodePolarsCode structure:
323
+ - depending_on_id (single) -> depending_on_ids (list)
324
+ - PolarsCodeInput from dataclass to BaseModel
325
+ """
326
+ # Handle depending_on_id -> depending_on_ids migration
40
327
  if hasattr(node_polars, 'depending_on_id'):
41
- setattr(node_polars, 'depending_on_ids', [getattr(node_polars, 'depending_on_id')])
328
+ old_id = getattr(node_polars, 'depending_on_id', None)
329
+ if not hasattr(node_polars, 'depending_on_ids') or node_polars.depending_on_ids is None:
330
+ if old_id is not None:
331
+ setattr(node_polars, 'depending_on_ids', [old_id])
332
+ else:
333
+ setattr(node_polars, 'depending_on_ids', [])
42
334
 
335
+ # Handle PolarsCodeInput dataclass -> BaseModel migration
336
+ if hasattr(node_polars, 'polars_code_input') and node_polars.polars_code_input is not None:
337
+ polars_code_input = node_polars.polars_code_input
43
338
 
44
- def ensure_compatibility(flow_storage_obj: schemas.FlowInformation, flow_path: str):
45
- if not hasattr(flow_storage_obj, 'flow_settings'):
46
- flow_settings = schemas.FlowSettings(flow_id=flow_storage_obj.flow_id, path=flow_path,
47
- name=flow_storage_obj.flow_name)
339
+ if _is_dataclass_instance(polars_code_input):
340
+ from flowfile_core.schemas import transform_schema
341
+ new_polars_code_input = _migrate_dataclass_to_basemodel(
342
+ polars_code_input, transform_schema.PolarsCodeInput
343
+ )
344
+ node_polars.polars_code_input = new_polars_code_input
345
+
346
+
347
+ # =============================================================================
348
+ # FLOW-LEVEL COMPATIBILITY
349
+ # =============================================================================
350
+
351
+ def ensure_flow_settings(flow_storage_obj: schemas.FlowInformation, flow_path: str):
352
+ """Ensure flow_settings exists and has all required fields."""
353
+ if not hasattr(flow_storage_obj, 'flow_settings') or flow_storage_obj.flow_settings is None:
354
+ flow_settings = schemas.FlowSettings(
355
+ flow_id=flow_storage_obj.flow_id,
356
+ path=flow_path,
357
+ name=flow_storage_obj.flow_name
358
+ )
48
359
  setattr(flow_storage_obj, 'flow_settings', flow_settings)
49
360
  flow_storage_obj = schemas.FlowInformation.model_validate(flow_storage_obj)
50
- elif not hasattr(getattr(flow_storage_obj, 'flow_settings'), 'execution_location'):
51
- setattr(getattr(flow_storage_obj, 'flow_settings'), 'execution_location', "remote")
52
- elif not hasattr(flow_storage_obj.flow_settings, 'is_running'):
53
- setattr(flow_storage_obj.flow_settings, 'is_running', False)
54
- setattr(flow_storage_obj.flow_settings, 'is_canceled', False)
55
- if not hasattr(flow_storage_obj.flow_settings, 'show_detailed_progress'):
56
- setattr(flow_storage_obj.flow_settings, 'show_detailed_progress', True)
361
+ return flow_storage_obj
362
+
363
+ fs = flow_storage_obj.flow_settings
364
+
365
+ if not hasattr(fs, 'execution_location'):
366
+ setattr(fs, 'execution_location', "remote")
367
+
368
+ if not hasattr(fs, 'is_running'):
369
+ setattr(fs, 'is_running', False)
370
+
371
+ if not hasattr(fs, 'is_canceled'):
372
+ setattr(fs, 'is_canceled', False)
373
+
374
+ if not hasattr(fs, 'show_detailed_progress'):
375
+ setattr(fs, 'show_detailed_progress', True)
376
+
377
+ return flow_storage_obj
378
+
379
+
380
+ # =============================================================================
381
+ # MAIN ENTRY POINT
382
+ # =============================================================================
383
+
384
+ def ensure_compatibility(flow_storage_obj: schemas.FlowInformation, flow_path: str):
385
+ """
386
+ Main compatibility function - migrates old flowfile schemas to current version.
387
+
388
+ Handles migrations for:
389
+ - FlowSettings structure
390
+ - NodeRead (ReceivedTable with table_settings)
391
+ - NodeOutput (OutputSettings with table_settings)
392
+ - NodeSelect (position attributes, dataclass -> BaseModel)
393
+ - NodeJoin/NodeFuzzyMatch (join input positions, dataclass -> BaseModel)
394
+ - NodePolarsCode (depending_on_ids, dataclass -> BaseModel)
395
+ - Node descriptions
396
+ """
397
+ flow_storage_obj = ensure_flow_settings(flow_storage_obj, flow_path)
398
+
57
399
  for _id, node_information in flow_storage_obj.data.items():
58
- if not hasattr(node_information, 'setting_input'):
400
+ if not hasattr(node_information, 'setting_input') or node_information.setting_input is None:
59
401
  continue
60
- if node_information.setting_input.__class__.__name__ == 'NodeRead':
61
- ensure_compatibility_node_read(node_information.setting_input)
62
- elif node_information.setting_input.__class__.__name__ == 'NodeSelect':
63
- ensure_compatibility_node_select(node_information.setting_input)
64
- elif node_information.setting_input.__class__.__name__ == 'NodeOutput':
65
- ensure_compatibility_node_output(node_information.setting_input)
66
- elif node_information.setting_input.__class__.__name__ in ('NodeJoin', 'NodeFuzzyMatch'):
67
- ensure_compatibility_node_joins(node_information.setting_input)
68
- elif node_information.setting_input.__class__.__name__ == 'NodePolarsCode':
69
- ensure_compatibility_node_polars(node_information.setting_input)
70
- ensure_description(node_information.setting_input)
402
+
403
+ setting_input = node_information.setting_input
404
+ class_name = setting_input.__class__.__name__
405
+
406
+ if class_name == 'NodeRead':
407
+ ensure_compatibility_node_read(setting_input)
408
+ elif class_name == 'NodeSelect':
409
+ ensure_compatibility_node_select(setting_input)
410
+ elif class_name == 'NodeOutput':
411
+ ensure_compatibility_node_output(setting_input)
412
+ elif class_name in ('NodeJoin', 'NodeFuzzyMatch'):
413
+ ensure_compatibility_node_joins(setting_input)
414
+ elif class_name == 'NodePolarsCode':
415
+ ensure_compatibility_node_polars(setting_input)
416
+
417
+ ensure_description(setting_input)
418
+
419
+ return flow_storage_obj
420
+
421
+
422
+ def load_and_migrate_flowfile(flow_path: str) -> schemas.FlowInformation:
423
+ """
424
+ Convenience function: Load a flowfile and apply all compatibility migrations.
425
+
426
+ Args:
427
+ flow_path: Path to the .flowfile pickle
428
+
429
+ Returns:
430
+ Fully migrated FlowInformation object
431
+ """
432
+ flow_storage_obj = load_flowfile_pickle(flow_path)
433
+ return ensure_compatibility(flow_storage_obj, flow_path)