Flowfile 0.5.6__py3-none-any.whl → 0.6.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 (256) hide show
  1. flowfile/api.py +8 -6
  2. flowfile/web/static/assets/{AdminView-c2c7942b.js → AdminView-C4K1DdHI.js} +28 -33
  3. flowfile/web/static/assets/{CloudConnectionView-7a3042c6.js → CloudConnectionView-BZbPvPUL.js} +39 -50
  4. flowfile/web/static/assets/{CloudStorageReader-24c54524.css → CloudStorageReader-BDByiqPI.css} +25 -25
  5. flowfile/web/static/assets/{CloudStorageReader-709c4037.js → CloudStorageReader-DLVukNJ7.js} +30 -35
  6. flowfile/web/static/assets/{CloudStorageWriter-604c51a8.js → CloudStorageWriter-Bfi-C1QW.js} +32 -37
  7. flowfile/web/static/assets/{CloudStorageWriter-60547855.css → CloudStorageWriter-y8jL8yjG.css} +24 -24
  8. flowfile/web/static/assets/{ColumnActionInput-d63d6746.js → ColumnActionInput-BpiCApw9.js} +7 -12
  9. flowfile/web/static/assets/{ColumnSelector-0c8cd1cd.js → ColumnSelector-CEAwedI7.js} +1 -2
  10. flowfile/web/static/assets/ContextMenu-CdojQu0w.js +9 -0
  11. flowfile/web/static/assets/ContextMenu-D12mhsy1.js +9 -0
  12. flowfile/web/static/assets/ContextMenu-EWUR98va.js +9 -0
  13. flowfile/web/static/assets/{ContextMenu.vue_vue_type_script_setup_true_lang-774c517c.js → ContextMenu.vue_vue_type_script_setup_true_lang-I4rXXd6G.js} +4 -5
  14. flowfile/web/static/assets/{CrossJoin-38e5b99a.js → CrossJoin-BOFfxkJO.js} +19 -18
  15. flowfile/web/static/assets/{CrossJoin-71b4cc10.css → CrossJoin-Cmbyt9im.css} +18 -18
  16. flowfile/web/static/assets/{CustomNode-76e8f3f5.js → CustomNode-Bhpezobq.js} +12 -17
  17. flowfile/web/static/assets/{DatabaseConnectionSettings-38155669.js → DatabaseConnectionSettings-Dw3bSJKB.js} +10 -11
  18. flowfile/web/static/assets/{DatabaseReader-5bf8c75b.css → DatabaseReader-D6pUNUCs.css} +21 -21
  19. flowfile/web/static/assets/{DatabaseReader-2e549c8f.js → DatabaseReader-m87ghlw0.js} +36 -34
  20. flowfile/web/static/assets/{DatabaseView-dc877c29.js → DatabaseView-CisSAtpe.js} +30 -38
  21. flowfile/web/static/assets/{DatabaseWriter-ffb91864.js → DatabaseWriter-Bbj9JLdL.js} +33 -35
  22. flowfile/web/static/assets/{DatabaseWriter-bdcf2c8b.css → DatabaseWriter-RBqdFLj8.css} +17 -17
  23. flowfile/web/static/assets/{DesignerView-a4466dab.js → DesignerView-DemDevTQ.js} +1752 -2054
  24. flowfile/web/static/assets/{DesignerView-71d4e9a1.css → DesignerView-Dm6OzlIc.css} +209 -168
  25. flowfile/web/static/assets/{DocumentationView-979afc84.js → DocumentationView-BrC1ZR3H.js} +3 -4
  26. flowfile/web/static/assets/{ExploreData-e4b92aaf.js → ExploreData-BMKcDuRb.js} +8 -10
  27. flowfile/web/static/assets/{ExternalSource-d08e7227.js → ExternalSource-BXrNNS-f.js} +40 -42
  28. flowfile/web/static/assets/{ExternalSource-7ac7373f.css → ExternalSource-NB6WVl5R.css} +14 -14
  29. flowfile/web/static/assets/{Filter-7add806d.js → Filter-C2MjsN6P.js} +36 -33
  30. flowfile/web/static/assets/{Filter-7494ea97.css → Filter-DCMGGuGC.css} +9 -9
  31. flowfile/web/static/assets/{Formula-53d58c43.css → Formula-BYafbDj8.css} +4 -4
  32. flowfile/web/static/assets/{Formula-36ab24d2.js → Formula-ufuy4mVD.js} +27 -26
  33. flowfile/web/static/assets/{FuzzyMatch-ad6361d6.css → FuzzyMatch-BGJAwgd0.css} +42 -42
  34. flowfile/web/static/assets/{FuzzyMatch-cc01bb04.js → FuzzyMatch-BOHODq3h.js} +36 -38
  35. flowfile/web/static/assets/{GraphSolver-4fb98f3b.js → GraphSolver-B6ZzpNGO.js} +23 -21
  36. flowfile/web/static/assets/{GraphSolver-4b4d7db9.css → GraphSolver-DFN83sj3.css} +4 -4
  37. flowfile/web/static/assets/{GroupBy-b3c8f429.js → GroupBy-B9BRNcfe.js} +30 -29
  38. flowfile/web/static/assets/{Sort-4abb7fae.css → GroupBy-x4ooP5np.css} +1 -1
  39. flowfile/web/static/assets/Join-Bx_g5bZz.css +118 -0
  40. flowfile/web/static/assets/{Join-096b7b26.js → Join-DsBEy1IH.js} +48 -43
  41. flowfile/web/static/assets/{LoginView-c33a246a.js → LoginView-Ct0rhdcO.js} +1 -2
  42. flowfile/web/static/assets/{ManualInput-39111f19.css → ManualInput-DlZmtMdt.css} +48 -48
  43. flowfile/web/static/assets/{ManualInput-7307e9b1.js → ManualInput-bC4BUgnG.js} +40 -41
  44. flowfile/web/static/assets/{MultiSelect-14822c48.js → MultiSelect-DIQ8PuTC.js} +2 -2
  45. flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-90c4d340.js → MultiSelect.vue_vue_type_script_setup_true_lang-BefHfqTI.js} +1 -1
  46. flowfile/web/static/assets/{NodeDesigner-5036c392.js → NodeDesigner-D39yzr2k.js} +178 -208
  47. flowfile/web/static/assets/{NodeDesigner-94cd4dd3.css → NodeDesigner-R0l6sYyY.css} +76 -76
  48. flowfile/web/static/assets/{NumericInput-15cf3b72.js → NumericInput-DMSX3oOr.js} +2 -2
  49. flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-91e679d7.js → NumericInput.vue_vue_type_script_setup_true_lang-d0YlVHAl.js} +1 -1
  50. flowfile/web/static/assets/{Output-1f8ed42c.js → Output-D0VoXGcW.js} +26 -34
  51. flowfile/web/static/assets/{Output-692dd25d.css → Output-DsmglIDy.css} +5 -5
  52. flowfile/web/static/assets/{Pivot-0e153f4e.js → Pivot-BnMB4sEe.js} +26 -26
  53. flowfile/web/static/assets/{Pivot-0eda81b4.css → Pivot-qKTyWxop.css} +4 -4
  54. flowfile/web/static/assets/{PivotValidation-81ec2a33.js → PivotValidation-B2lWvugt.js} +7 -9
  55. flowfile/web/static/assets/{PivotValidation-5a4f7c79.js → PivotValidation-BPlhRjpL.js} +7 -9
  56. flowfile/web/static/assets/{PolarsCode-a39f15ac.js → PolarsCode-5h0tHnWR.js} +22 -20
  57. flowfile/web/static/assets/{PopOver-ddcfe4f6.js → PopOver-BHpt5rsj.js} +5 -9
  58. flowfile/web/static/assets/{PopOver-d96599db.css → PopOver-CyYM4-rV.css} +1 -1
  59. flowfile/web/static/assets/{Read-90f366bc.css → Read-DJxkrTb_.css} +10 -10
  60. flowfile/web/static/assets/Read-TsLEFh3B.js +227 -0
  61. flowfile/web/static/assets/{RecordCount-e9048ccd.js → RecordCount-DkVixq9v.js} +18 -17
  62. flowfile/web/static/assets/{RecordId-ad02521d.js → RecordId-C2UEGlCf.js} +42 -39
  63. flowfile/web/static/assets/{SQLQueryComponent-2eeecf0b.js → SQLQueryComponent-Dr5KMoD3.js} +2 -3
  64. flowfile/web/static/assets/{Sample-9a68c23d.js → Sample-Cb3eQNmd.js} +30 -30
  65. flowfile/web/static/assets/{SecretSelector-2429f35a.js → SecretSelector-De2L2bSx.js} +3 -4
  66. flowfile/web/static/assets/{SecretsView-c6afc915.js → SecretsView-CheC9BPV.js} +13 -16
  67. flowfile/web/static/assets/{Select-fcd002b6.js → Select-CI8TloRs.js} +41 -36
  68. flowfile/web/static/assets/{SettingsSection-5ce15962.js → SettingsSection-B39ulIiI.js} +1 -2
  69. flowfile/web/static/assets/{SettingsSection-c6b1362c.js → SettingsSection-BiCc7S9h.js} +1 -2
  70. flowfile/web/static/assets/{SettingsSection-cebb91d5.js → SettingsSection-CITK_R7o.js} +2 -3
  71. flowfile/web/static/assets/{SettingsSection-26fe48d4.css → SettingsSection-D2GgY-Aq.css} +4 -4
  72. flowfile/web/static/assets/{SetupView-2d12e01f.js → SetupView-C1aXRDvp.js} +1 -2
  73. flowfile/web/static/assets/{SingleSelect-b67de4eb.js → SingleSelect-Kr_hz90m.js} +2 -2
  74. flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-eedb70eb.js → SingleSelect.vue_vue_type_script_setup_true_lang-Rxht5Z5N.js} +1 -1
  75. flowfile/web/static/assets/{SliderInput-fd8134ac.js → SliderInput-CLqpCxCb.js} +1 -2
  76. flowfile/web/static/assets/{GroupBy-5792782d.css → Sort-BIt2kc_p.css} +1 -1
  77. flowfile/web/static/assets/{Sort-c005a573.js → Sort-Dnw_J6Qi.js} +25 -25
  78. flowfile/web/static/assets/{TextInput-1bb31dab.js → TextInput-wdlunIZC.js} +2 -2
  79. flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-a51fe730.js → TextInput.vue_vue_type_script_setup_true_lang-Bcj3ywzv.js} +1 -1
  80. flowfile/web/static/assets/{TextToRows-4f363753.js → TextToRows-BhtyGWPq.js} +42 -49
  81. flowfile/web/static/assets/{TextToRows-12afb4f4.css → TextToRows-DivDOLDx.css} +9 -9
  82. flowfile/web/static/assets/{ToggleSwitch-ca0f2e5e.js → ToggleSwitch-B-6WzfFf.js} +2 -2
  83. flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-49aa41d8.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-Cj8LqT-b.js} +1 -1
  84. flowfile/web/static/assets/{UnavailableFields-f6147968.js → UnavailableFields-Yf6XSqFB.js} +2 -3
  85. flowfile/web/static/assets/{Union-c65f17b7.js → Union-CwpjeKYC.js} +20 -23
  86. flowfile/web/static/assets/{Unpivot-b6ad6427.css → Union-DQJcpp3-.css} +6 -6
  87. flowfile/web/static/assets/{Unique-a1d96fb2.js → Unique-25v3urqH.js} +75 -74
  88. flowfile/web/static/assets/{Union-d6a8d7d5.css → Unpivot-Deqh1gtI.css} +6 -6
  89. flowfile/web/static/assets/{Unpivot-c2657ff3.js → Unpivot-sYcTTXrq.js} +34 -27
  90. flowfile/web/static/assets/{UnpivotValidation-28e29a3b.js → UnpivotValidation-C5DDEKY2.js} +5 -7
  91. flowfile/web/static/assets/VueGraphicWalker-B8l1_Z92.js +131 -0
  92. flowfile/web/static/assets/VueGraphicWalker-Da_1-3me.css +21 -0
  93. flowfile/web/static/assets/{api-df48ec50.js → api-C0LvF-0C.js} +1 -1
  94. flowfile/web/static/assets/{api-ee542cf7.js → api-DaC83EO_.js} +1 -1
  95. flowfile/web/static/assets/client-C8Ygr6Gb.js +42 -0
  96. flowfile/web/static/assets/{dropDown-7576a76a.js → dropDown-D5YXaPRR.js} +7 -12
  97. flowfile/web/static/assets/{fullEditor-7583bef5.js → fullEditor-BVYnWm05.js} +300 -18
  98. flowfile/web/static/assets/genericNodeSettings-2wAu-QKn.css +75 -0
  99. flowfile/web/static/assets/genericNodeSettings-BBtW_Cpz.js +590 -0
  100. flowfile/web/static/assets/{VueGraphicWalker-2fc3ddd4.js → graphic-walker.es-VrK6vdGE.js} +92305 -89751
  101. flowfile/web/static/assets/index-BCJxPfM5.js +6693 -0
  102. flowfile/web/static/assets/{index-057d770d.js → index-CHPMUR0d.js} +150 -170
  103. flowfile/web/static/assets/index-DPkoZWq8.js +32 -0
  104. flowfile/web/static/assets/index-DnW_KC_I.js +277 -0
  105. flowfile/web/static/assets/index-UFXyfirV.css +10797 -0
  106. flowfile/web/static/assets/index-bcuE0Z0p.js +87456 -0
  107. flowfile/web/static/assets/{node.types-2c15bb7e.js → node.types-Dl4gtSW9.js} +2 -2
  108. flowfile/web/static/assets/{outputCsv-c492b15e.js → outputCsv-BELuBiJZ.js} +1 -2
  109. flowfile/web/static/assets/outputCsv-CdGkv-fN.css +2581 -0
  110. flowfile/web/static/assets/{outputExcel-13bfa10f.js → outputExcel-D0TTNM79.js} +1 -2
  111. flowfile/web/static/assets/{outputParquet-9be1523a.js → outputParquet-Cz9EbRHj.js} +1 -2
  112. flowfile/web/static/assets/{readCsv-5a49a8c9.js → readCsv-7bd3kUMI.js} +1 -2
  113. flowfile/web/static/assets/{readExcel-27c30ad8.js → readExcel-Cq8CCwIv.js} +3 -4
  114. flowfile/web/static/assets/{readParquet-c5244ad5.css → readParquet-CRDmBrsp.css} +4 -4
  115. flowfile/web/static/assets/{readParquet-446bde68.js → readParquet-DjR4mRaj.js} +4 -5
  116. flowfile/web/static/assets/{secrets.api-34431884.js → secrets.api-C9o2KE5V.js} +1 -1
  117. flowfile/web/static/assets/{selectDynamic-5754a2b1.js → selectDynamic-Bl5FVsME.js} +5 -7
  118. flowfile/web/static/assets/useNodeSettings-dMS9zmh_.js +69 -0
  119. flowfile/web/static/assets/{vue-codemirror.esm-8f46fb36.js → vue-codemirror.esm-CwaYwln0.js} +3469 -3064
  120. flowfile/web/static/assets/{vue-content-loader.es-808fe33a.js → vue-content-loader.es-CMoRXo7N.js} +3 -3
  121. flowfile/web/static/index.html +2 -3
  122. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/METADATA +2 -1
  123. flowfile-0.6.1.dist-info/RECORD +417 -0
  124. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/WHEEL +1 -1
  125. flowfile_core/auth/password.py +1 -0
  126. flowfile_core/database/init_db.py +7 -5
  127. flowfile_core/fileExplorer/funcs.py +2 -2
  128. flowfile_core/flowfile/code_generator/code_generator.py +13 -11
  129. flowfile_core/flowfile/filter_expressions.py +327 -0
  130. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +61 -59
  131. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +3 -29
  132. flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +45 -14
  133. flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +20 -3
  134. flowfile_core/flowfile/flow_data_engine/subprocess_operations/streaming.py +206 -0
  135. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +146 -24
  136. flowfile_core/flowfile/flow_graph.py +504 -190
  137. flowfile_core/flowfile/flow_node/__init__.py +32 -0
  138. flowfile_core/flowfile/flow_node/executor.py +404 -0
  139. flowfile_core/flowfile/flow_node/flow_node.py +207 -106
  140. flowfile_core/flowfile/flow_node/models.py +40 -0
  141. flowfile_core/flowfile/flow_node/output_field_config_applier.py +217 -0
  142. flowfile_core/flowfile/flow_node/schema_utils.py +78 -0
  143. flowfile_core/flowfile/flow_node/state.py +155 -0
  144. flowfile_core/flowfile/history_manager.py +401 -0
  145. flowfile_core/flowfile/manage/compatibility_enhancements.py +9 -0
  146. flowfile_core/flowfile/manage/io_flowfile.py +3 -1
  147. flowfile_core/flowfile/sources/external_sources/sql_source/models.py +20 -4
  148. flowfile_core/flowfile/util/execution_orderer.py +89 -36
  149. flowfile_core/routes/auth.py +8 -9
  150. flowfile_core/routes/routes.py +320 -101
  151. flowfile_core/routes/user_defined_components.py +18 -16
  152. flowfile_core/schemas/history_schema.py +220 -0
  153. flowfile_core/schemas/input_schema.py +130 -6
  154. flowfile_core/schemas/schemas.py +9 -0
  155. flowfile_core/schemas/transform_schema.py +27 -5
  156. flowfile_core/schemas/yaml_types.py +23 -5
  157. flowfile_frame/adding_expr.py +18 -126
  158. flowfile_frame/callable_utils.py +261 -0
  159. flowfile_frame/database/connection_manager.py +0 -1
  160. flowfile_frame/expr.py +8 -4
  161. flowfile_frame/flow_frame.py +41 -41
  162. flowfile_frame/lazy.py +3 -12
  163. flowfile_frame/lazy_methods.py +5 -64
  164. flowfile_frame/utils.py +13 -32
  165. flowfile_worker/funcs.py +6 -4
  166. flowfile_worker/main.py +2 -0
  167. flowfile_worker/models.py +31 -11
  168. flowfile_worker/routes.py +60 -35
  169. flowfile_worker/spawner.py +7 -1
  170. flowfile_worker/streaming.py +335 -0
  171. flowfile/web/static/assets/ContextMenu-366bf1b4.js +0 -9
  172. flowfile/web/static/assets/ContextMenu-85cf5b44.js +0 -9
  173. flowfile/web/static/assets/ContextMenu-9d28ae6d.js +0 -9
  174. flowfile/web/static/assets/Join-28b5e18f.css +0 -109
  175. flowfile/web/static/assets/Read-39b63932.js +0 -222
  176. flowfile/web/static/assets/VueGraphicWalker-430f0b86.css +0 -6
  177. flowfile/web/static/assets/database_reader-ce1e55f3.svg +0 -24
  178. flowfile/web/static/assets/database_writer-b4ad0753.svg +0 -23
  179. flowfile/web/static/assets/element-icons-9c88a535.woff +0 -0
  180. flowfile/web/static/assets/element-icons-de5eb258.ttf +0 -0
  181. flowfile/web/static/assets/genericNodeSettings-0155288b.js +0 -136
  182. flowfile/web/static/assets/genericNodeSettings-3b2507ea.css +0 -46
  183. flowfile/web/static/assets/index-aeec439d.js +0 -38
  184. flowfile/web/static/assets/index-ca6799de.js +0 -62760
  185. flowfile/web/static/assets/index-d60c9dd4.css +0 -10777
  186. flowfile/web/static/assets/nodeInput-d478b9ac.js +0 -2
  187. flowfile/web/static/assets/outputCsv-cc84e09f.css +0 -2499
  188. flowfile-0.5.6.dist-info/RECORD +0 -407
  189. /flowfile/web/static/assets/{AdminView-f53bad23.css → AdminView-B2Dthl3u.css} +0 -0
  190. /flowfile/web/static/assets/{CloudConnectionView-cf85f943.css → CloudConnectionView-BdFYGWV7.css} +0 -0
  191. /flowfile/web/static/assets/{ColumnActionInput-c44b7aee.css → ColumnActionInput-dCasSIC9.css} +0 -0
  192. /flowfile/web/static/assets/{ColumnSelector-371637fb.css → ColumnSelector-j6sEOjo1.css} +0 -0
  193. /flowfile/web/static/assets/{CustomNode-edb9b939.css → CustomNode-VPlajG0j.css} +0 -0
  194. /flowfile/web/static/assets/{DatabaseConnectionSettings-c20a1e16.css → DatabaseConnectionSettings-B78hXYgu.css} +0 -0
  195. /flowfile/web/static/assets/{DatabaseView-6655afd6.css → DatabaseView-B-_adk1s.css} +0 -0
  196. /flowfile/web/static/assets/{DocumentationView-9ea6e871.css → DocumentationView-CL7iipFL.css} +0 -0
  197. /flowfile/web/static/assets/{ExploreData-10c5acc8.css → ExploreData-DHjv0Plr.css} +0 -0
  198. /flowfile/web/static/assets/{LoginView-d325d632.css → LoginView-DN1BXY3e.css} +0 -0
  199. /flowfile/web/static/assets/{PivotValidation-0e905b1a.css → PivotValidation-DK-FARWe.css} +0 -0
  200. /flowfile/web/static/assets/{PivotValidation-41b57ad6.css → PivotValidation-FUa9F47u.css} +0 -0
  201. /flowfile/web/static/assets/{PolarsCode-2b1f1f23.css → PolarsCode-G-gRSrSc.css} +0 -0
  202. /flowfile/web/static/assets/{SQLQueryComponent-edb90b98.css → SQLQueryComponent-oAbWw0r-.css} +0 -0
  203. /flowfile/web/static/assets/{SecretSelector-6329f743.css → SecretSelector-CJSadIZx.css} +0 -0
  204. /flowfile/web/static/assets/{SecretsView-aa291340.css → SecretsView-DbzIRAba.css} +0 -0
  205. /flowfile/web/static/assets/{SettingsSection-8f980839.css → SettingsSection-BGcJnH6q.css} +0 -0
  206. /flowfile/web/static/assets/{SettingsSection-07fbbc39.css → SettingsSection-DDWn_EGW.css} +0 -0
  207. /flowfile/web/static/assets/{SetupView-ec26f76a.css → SetupView-CI1nd-5Z.css} +0 -0
  208. /flowfile/web/static/assets/{SliderInput-f2e4f23c.css → SliderInput-BRk-q_Dk.css} +0 -0
  209. /flowfile/web/static/assets/{UnavailableFields-394a1f78.css → UnavailableFields-DRKDImKe.css} +0 -0
  210. /flowfile/web/static/assets/{Unique-2b705521.css → Unique-Absb0aON.css} +0 -0
  211. /flowfile/web/static/assets/{UnpivotValidation-d5ca3b7b.css → UnpivotValidation-DSBkFgS-.css} +0 -0
  212. /flowfile/web/static/assets/{airbyte-292aa232.png → airbyte-W0xvIXwZ.png} +0 -0
  213. /flowfile/web/static/assets/{cloud_storage_reader-aa1415d6.png → cloud_storage_reader-3GpSCk90.png} +0 -0
  214. /flowfile/web/static/assets/{cross_join-d30c0290.png → cross_join-B0qpgYoV.png} +0 -0
  215. /flowfile/web/static/assets/{dropDown-1d6acbd9.css → dropDown-CE0VF5_P.css} +0 -0
  216. /flowfile/web/static/assets/{explore_data-8a0a2861.png → explore_data-tX6olPPL.png} +0 -0
  217. /flowfile/web/static/assets/{fa-brands-400-808443ae.ttf → fa-brands-400-D1LuMI3I.ttf} +0 -0
  218. /flowfile/web/static/assets/{fa-brands-400-d7236a19.woff2 → fa-brands-400-D_cYUPeE.woff2} +0 -0
  219. /flowfile/web/static/assets/{fa-regular-400-e3456d12.woff2 → fa-regular-400-BjRzuEpd.woff2} +0 -0
  220. /flowfile/web/static/assets/{fa-regular-400-54cf6086.ttf → fa-regular-400-DZaxPHgR.ttf} +0 -0
  221. /flowfile/web/static/assets/{fa-solid-900-aa759986.woff2 → fa-solid-900-CTAAxXor.woff2} +0 -0
  222. /flowfile/web/static/assets/{fa-solid-900-d2f05935.ttf → fa-solid-900-D0aA9rwL.ttf} +0 -0
  223. /flowfile/web/static/assets/{fa-v4compatibility-0ce9033c.woff2 → fa-v4compatibility-C9RhG_FT.woff2} +0 -0
  224. /flowfile/web/static/assets/{fa-v4compatibility-30f6abf6.ttf → fa-v4compatibility-CCth-dXg.ttf} +0 -0
  225. /flowfile/web/static/assets/{filter-d7708bda.png → filter-WRdZyUOw.png} +0 -0
  226. /flowfile/web/static/assets/{formula-eeeb1611.png → formula-CgM7uHVI.png} +0 -0
  227. /flowfile/web/static/assets/{fullEditor-fe9f7e18.css → fullEditor-CmDI7T9F.css} +0 -0
  228. /flowfile/web/static/assets/{fuzzy_match-40c161b2.png → fuzzy_match-Yon3k5Tc.png} +0 -0
  229. /flowfile/web/static/assets/{graph_solver-8b7888b8.png → graph_solver-BlMrBttD.png} +0 -0
  230. /flowfile/web/static/assets/{group_by-80561fc3.png → group_by-Gici0CSS.png} +0 -0
  231. /flowfile/web/static/assets/{input_data-ab2eb678.png → input_data-BRdGecLc.png} +0 -0
  232. /flowfile/web/static/assets/{join-349043ae.png → join-BITWRu73.png} +0 -0
  233. /flowfile/web/static/assets/{manual_input-ae98f31d.png → manual_input-CFvo_EUS.png} +0 -0
  234. /flowfile/web/static/assets/{old_join-5d0eb604.png → old_join-B9bkpPqv.png} +0 -0
  235. /flowfile/web/static/assets/{output-06ec0371.png → output-Dp7-ZpC4.png} +0 -0
  236. /flowfile/web/static/assets/{outputExcel-f5d272b2.css → outputExcel-CKgRe2iT.css} +0 -0
  237. /flowfile/web/static/assets/{outputParquet-54597c3c.css → outputParquet-d7j407cK.css} +0 -0
  238. /flowfile/web/static/assets/{pivot-9660df51.png → pivot-DSxKhNlD.png} +0 -0
  239. /flowfile/web/static/assets/{polars_code-05ce5dc6.png → polars_code-DxiztZ1c.png} +0 -0
  240. /flowfile/web/static/assets/{readCsv-3bfac4c3.css → readCsv-BG-1Jilp.css} +0 -0
  241. /flowfile/web/static/assets/{readExcel-3db6b763.css → readExcel-DBQXKPtC.css} +0 -0
  242. /flowfile/web/static/assets/{record_count-dab44eb5.png → record_count-DCeaLtpS.png} +0 -0
  243. /flowfile/web/static/assets/{record_id-0b15856b.png → record_id-FeUjyIFh.png} +0 -0
  244. /flowfile/web/static/assets/{sample-693a88b5.png → sample-DeqfRiB-.png} +0 -0
  245. /flowfile/web/static/assets/{select-b0d0437a.png → select-D4JjbdjS.png} +0 -0
  246. /flowfile/web/static/assets/{selectDynamic-f2fb394f.css → selectDynamic-CjeTPUUo.css} +0 -0
  247. /flowfile/web/static/assets/{sort-2aa579f0.png → sort-DGwUG9WS.png} +0 -0
  248. /flowfile/web/static/assets/{summarize-2a099231.png → summarize-DFaNHpfp.png} +0 -0
  249. /flowfile/web/static/assets/{text_to_rows-859b29ea.png → text_to_rows-BdiAewrN.png} +0 -0
  250. /flowfile/web/static/assets/{union-2d8609f4.png → union-DCK-LSMq.png} +0 -0
  251. /flowfile/web/static/assets/{unique-1958b98a.png → unique-CdP3zZIq.png} +0 -0
  252. /flowfile/web/static/assets/{unpivot-d3cb4b5b.png → unpivot-CHttrEt8.png} +0 -0
  253. /flowfile/web/static/assets/{user-defined-icon-0ae16c90.png → user-defined-icon-BcIp2Vzo.png} +0 -0
  254. /flowfile/web/static/assets/{view-7a0f0be1.png → view-DUSRwjvq.png} +0 -0
  255. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/entry_points.txt +0 -0
  256. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,10 @@
1
1
  import datetime
2
+ import functools
2
3
  import json
3
4
  import os
5
+ import threading
4
6
  from collections.abc import Callable
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
5
8
  from copy import deepcopy
6
9
  from functools import partial
7
10
  from importlib.metadata import PackageNotFoundError, version
@@ -39,6 +42,7 @@ from flowfile_core.flowfile.flow_data_engine.subprocess_operations.subprocess_op
39
42
  ExternalDfFetcher,
40
43
  )
41
44
  from flowfile_core.flowfile.flow_node.flow_node import FlowNode
45
+ from flowfile_core.flowfile.flow_node.schema_utils import create_schema_callback_with_output_config
42
46
  from flowfile_core.flowfile.graph_tree.graph_tree import (
43
47
  add_un_drawn_nodes,
44
48
  build_flow_paths,
@@ -56,8 +60,9 @@ from flowfile_core.flowfile.sources.external_sources.factory import data_source_
56
60
  from flowfile_core.flowfile.sources.external_sources.sql_source import models as sql_models
57
61
  from flowfile_core.flowfile.sources.external_sources.sql_source import utils as sql_utils
58
62
  from flowfile_core.flowfile.sources.external_sources.sql_source.sql_source import BaseSqlSource, SqlSource
63
+ from flowfile_core.flowfile.filter_expressions import build_filter_expression
59
64
  from flowfile_core.flowfile.util.calculate_layout import calculate_layered_layout
60
- from flowfile_core.flowfile.util.execution_orderer import compute_execution_plan
65
+ from flowfile_core.flowfile.util.execution_orderer import ExecutionPlan, ExecutionStage, compute_execution_plan
61
66
  from flowfile_core.flowfile.utils import snake_case_to_camel_case
62
67
  from flowfile_core.schemas import input_schema, schemas, transform_schema
63
68
  from flowfile_core.schemas.cloud_storage_schemas import (
@@ -67,6 +72,7 @@ from flowfile_core.schemas.cloud_storage_schemas import (
67
72
  FullCloudStorageConnection,
68
73
  get_cloud_storage_write_settings_worker_interface,
69
74
  )
75
+ from flowfile_core.schemas.history_schema import HistoryActionType, HistoryState, UndoRedoResult
70
76
  from flowfile_core.schemas.output_model import NodeData, NodeResult, RunInformation
71
77
  from flowfile_core.schemas.transform_schema import FuzzyMatchInputManager
72
78
  from flowfile_core.secret_manager.secret_manager import decrypt_secret, get_encrypted_secret
@@ -88,6 +94,53 @@ def represent_list_json(dumper, data):
88
94
  yaml.add_representer(list, represent_list_json)
89
95
 
90
96
 
97
+ def with_history_capture(action_type: "HistoryActionType", description_template: str = "Update {node_type} settings"):
98
+ """Decorator to automatically capture history for FlowGraph methods.
99
+
100
+ Wraps a method to capture state before execution and record history
101
+ only if the state actually changed. Respects the flow's track_history setting.
102
+
103
+ Args:
104
+ action_type: The type of history action (e.g., HistoryActionType.UPDATE_SETTINGS).
105
+ description_template: Template string for the history description.
106
+ Can use {node_type} placeholder which will be replaced with the actual node type.
107
+
108
+ Example:
109
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
110
+ def add_filter(self, filter_settings: input_schema.NodeFilter):
111
+ # ... implementation
112
+ """
113
+ def decorator(func: Callable) -> Callable:
114
+ @functools.wraps(func)
115
+ def wrapper(self: "FlowGraph", *args, **kwargs):
116
+ # Skip history capture if tracking is disabled
117
+ if not self.flow_settings.track_history:
118
+ return func(self, *args, **kwargs)
119
+
120
+ # Get the first argument (settings input) from args or kwargs
121
+ settings_input = args[0] if args else next(iter(kwargs.values()), None)
122
+
123
+ # Extract node info from the settings input
124
+ node_id = getattr(settings_input, 'node_id', None) if settings_input else None
125
+ node_type = getattr(settings_input, 'node_type', func.__name__.replace('add_', '')) if settings_input else func.__name__.replace('add_', '')
126
+
127
+ # Capture state before the operation
128
+ pre_snapshot = self.get_flowfile_data()
129
+
130
+ # Execute the actual method
131
+ result = func(self, *args, **kwargs)
132
+
133
+ # Record history if state changed
134
+ self._history_manager.capture_if_changed(
135
+ self, pre_snapshot, action_type,
136
+ description_template.format(node_type=node_type),
137
+ node_id
138
+ )
139
+ return result
140
+ return wrapper
141
+ return decorator
142
+
143
+
91
144
  def get_xlsx_schema(
92
145
  engine: str,
93
146
  file_path: str,
@@ -148,15 +201,19 @@ def skip_node_message(flow_logger: FlowLogger, nodes: list[FlowNode]) -> None:
148
201
  flow_logger.warning(f"skipping nodes:\n{msg}")
149
202
 
150
203
 
151
- def execution_order_message(flow_logger: FlowLogger, nodes: list[FlowNode]) -> None:
152
- """Logs an informational message showing the determined execution order of nodes.
204
+ def execution_order_message(flow_logger: FlowLogger, stages: list[ExecutionStage]) -> None:
205
+ """Logs an informational message showing the determined execution order with parallel stages.
153
206
 
154
207
  Args:
155
208
  flow_logger: The logger instance for the flow.
156
- nodes: A list of FlowNode objects in the order they will be executed.
209
+ stages: A list of ExecutionStage objects in execution order.
157
210
  """
158
- msg = "\n".join(str(node) for node in nodes)
159
- flow_logger.info(f"execution order:\n{msg}")
211
+ lines: list[str] = []
212
+ for i, stage in enumerate(stages):
213
+ node_strs = ", ".join(str(node) for node in stage)
214
+ parallel_tag = " (parallel)" if len(stage) > 1 else ""
215
+ lines.append(f" Stage {i}{parallel_tag}: [{node_strs}]")
216
+ flow_logger.info(f"execution order:\n" + "\n".join(lines))
160
217
 
161
218
 
162
219
  def get_xlsx_schema_callback(
@@ -299,6 +356,13 @@ class FlowGraph:
299
356
  self.cache_results = cache_results
300
357
  self.__name__ = name if name else "flow_" + str(id(self))
301
358
  self.depends_on = {}
359
+
360
+ # Initialize history manager for undo/redo support
361
+ from flowfile_core.flowfile.history_manager import HistoryManager
362
+ from flowfile_core.schemas.history_schema import HistoryConfig
363
+ history_config = HistoryConfig(enabled=flow_settings.track_history)
364
+ self._history_manager = HistoryManager(config=history_config)
365
+
302
366
  if path_ref is not None:
303
367
  self.add_datasource(input_schema.NodeDatasource(file_path=path_ref))
304
368
  elif input_flow is not None:
@@ -316,6 +380,215 @@ class FlowGraph:
316
380
  self.reset()
317
381
  self._flow_settings = flow_settings
318
382
 
383
+ # ==================== History Management Methods ====================
384
+
385
+ def capture_history_snapshot(
386
+ self,
387
+ action_type: HistoryActionType,
388
+ description: str,
389
+ node_id: int = None,
390
+ ) -> bool:
391
+ """Capture the current state before a change for undo support.
392
+
393
+ Args:
394
+ action_type: The type of action being performed.
395
+ description: Human-readable description of the action.
396
+ node_id: Optional ID of the affected node.
397
+
398
+ Returns:
399
+ True if snapshot was captured, False if skipped.
400
+ """
401
+ return self._history_manager.capture_snapshot(self, action_type, description, node_id)
402
+
403
+ def capture_history_if_changed(
404
+ self,
405
+ pre_snapshot: schemas.FlowfileData,
406
+ action_type: HistoryActionType,
407
+ description: str,
408
+ node_id: int = None,
409
+ ) -> bool:
410
+ """Capture history only if the flow state actually changed.
411
+
412
+ Use this for settings updates where the change might be a no-op.
413
+ Call this AFTER the change is applied.
414
+
415
+ Args:
416
+ pre_snapshot: The FlowfileData captured BEFORE the change.
417
+ action_type: The type of action that was performed.
418
+ description: Human-readable description of the action.
419
+ node_id: Optional ID of the affected node.
420
+
421
+ Returns:
422
+ True if a change was detected and snapshot was captured.
423
+ """
424
+ return self._history_manager.capture_if_changed(
425
+ self, pre_snapshot, action_type, description, node_id
426
+ )
427
+
428
+ def undo(self) -> UndoRedoResult:
429
+ """Undo the last action by restoring to the previous state.
430
+
431
+ Returns:
432
+ UndoRedoResult indicating success or failure.
433
+ """
434
+ return self._history_manager.undo(self)
435
+
436
+ def redo(self) -> UndoRedoResult:
437
+ """Redo the last undone action.
438
+
439
+ Returns:
440
+ UndoRedoResult indicating success or failure.
441
+ """
442
+ return self._history_manager.redo(self)
443
+
444
+ def get_history_state(self) -> HistoryState:
445
+ """Get the current state of the history system.
446
+
447
+ Returns:
448
+ HistoryState with information about available undo/redo operations.
449
+ """
450
+ return self._history_manager.get_state()
451
+
452
+ def _execute_with_history(
453
+ self,
454
+ operation: Callable[[], Any],
455
+ action_type: HistoryActionType,
456
+ description: str,
457
+ node_id: int = None,
458
+ ) -> Any:
459
+ """Execute an operation with automatic history capture.
460
+
461
+ This helper captures the state before the operation, executes it,
462
+ and records history only if the state actually changed.
463
+
464
+ Args:
465
+ operation: A callable that performs the actual operation.
466
+ action_type: The type of action being performed.
467
+ description: Human-readable description of the action.
468
+ node_id: Optional ID of the affected node.
469
+
470
+ Returns:
471
+ The result of the operation (if any).
472
+ """
473
+ # Skip history capture if tracking is disabled for this flow
474
+ if not self.flow_settings.track_history:
475
+ return operation()
476
+
477
+ pre_snapshot = self.get_flowfile_data()
478
+ result = operation()
479
+ self._history_manager.capture_if_changed(
480
+ self, pre_snapshot, action_type, description, node_id
481
+ )
482
+ return result
483
+
484
+ def restore_from_snapshot(self, snapshot: schemas.FlowfileData) -> None:
485
+ """Clear current state and rebuild from a snapshot.
486
+
487
+ This method is used internally by undo/redo to restore a previous state.
488
+
489
+ Args:
490
+ snapshot: The FlowfileData snapshot to restore from.
491
+ """
492
+ from flowfile_core.flowfile.manage.io_flowfile import (
493
+ _flowfile_data_to_flow_information,
494
+ determine_insertion_order,
495
+ )
496
+
497
+ # Preserve the current flow_id
498
+ original_flow_id = self._flow_id
499
+
500
+ # Convert snapshot to FlowInformation
501
+ flow_info = _flowfile_data_to_flow_information(snapshot)
502
+
503
+ # Clear current state
504
+ self._node_db.clear()
505
+ self._node_ids.clear()
506
+ self._flow_starts.clear()
507
+ self._results = None
508
+
509
+ # Restore flow settings (preserve original flow_id)
510
+ self._flow_settings = flow_info.flow_settings
511
+ self._flow_settings.flow_id = original_flow_id
512
+ self._flow_id = original_flow_id
513
+ self.__name__ = flow_info.flow_name or self.__name__
514
+
515
+ # Determine node insertion order
516
+ ingestion_order = determine_insertion_order(flow_info)
517
+
518
+ # First pass: Create all nodes as promises
519
+ for node_id in ingestion_order:
520
+ node_info = flow_info.data[node_id]
521
+ node_promise = input_schema.NodePromise(
522
+ flow_id=original_flow_id,
523
+ node_id=node_info.id,
524
+ pos_x=node_info.x_position or 0,
525
+ pos_y=node_info.y_position or 0,
526
+ node_type=node_info.type,
527
+ )
528
+ if hasattr(node_info.setting_input, "cache_results"):
529
+ node_promise.cache_results = node_info.setting_input.cache_results
530
+ self.add_node_promise(node_promise)
531
+
532
+ # Second pass: Apply settings using add_<node_type> methods
533
+ for node_id in ingestion_order:
534
+ node_info = flow_info.data[node_id]
535
+ if node_info.is_setup and node_info.setting_input is not None:
536
+ # Update flow_id in setting_input
537
+ if hasattr(node_info.setting_input, "flow_id"):
538
+ node_info.setting_input.flow_id = original_flow_id
539
+
540
+ if hasattr(node_info.setting_input, "is_user_defined") and node_info.setting_input.is_user_defined:
541
+ if node_info.type in CUSTOM_NODE_STORE:
542
+ user_defined_node_class = CUSTOM_NODE_STORE[node_info.type]
543
+ self.add_user_defined_node(
544
+ custom_node=user_defined_node_class.from_settings(node_info.setting_input.settings),
545
+ user_defined_node_settings=node_info.setting_input,
546
+ )
547
+ else:
548
+ add_method = getattr(self, "add_" + node_info.type, None)
549
+ if add_method:
550
+ add_method(node_info.setting_input)
551
+
552
+ # Third pass: Restore connections
553
+ for node_id in ingestion_order:
554
+ node_info = flow_info.data[node_id]
555
+ from_node = self.get_node(node_id)
556
+ if from_node is None:
557
+ continue
558
+
559
+ for output_node_id in node_info.outputs or []:
560
+ to_node = self.get_node(output_node_id)
561
+ if to_node is None:
562
+ continue
563
+
564
+ output_node_info = flow_info.data.get(output_node_id)
565
+ if output_node_info is None:
566
+ continue
567
+
568
+ # Determine connection type
569
+ is_left_input = (output_node_info.left_input_id == node_id) and (
570
+ to_node.left_input is None or to_node.left_input.node_id != node_id
571
+ )
572
+ is_right_input = (output_node_info.right_input_id == node_id) and (
573
+ to_node.right_input is None or to_node.right_input.node_id != node_id
574
+ )
575
+ is_main_input = node_id in (output_node_info.input_ids or [])
576
+
577
+ if is_left_input:
578
+ insert_type = "left"
579
+ elif is_right_input:
580
+ insert_type = "right"
581
+ elif is_main_input:
582
+ insert_type = "main"
583
+ else:
584
+ continue
585
+
586
+ to_node.add_node_connection(from_node, insert_type)
587
+
588
+ logger.info(f"Restored flow from snapshot with {len(self._node_db)} nodes")
589
+
590
+ # ==================== End History Management Methods ====================
591
+
319
592
  def add_node_to_starting_list(self, node: FlowNode) -> None:
320
593
  """Adds a node to the list of starting nodes for the flow if not already present.
321
594
 
@@ -325,39 +598,51 @@ class FlowGraph:
325
598
  if node.node_id not in {self_node.node_id for self_node in self._flow_starts}:
326
599
  self._flow_starts.append(node)
327
600
 
328
- def add_node_promise(self, node_promise: input_schema.NodePromise):
601
+ def add_node_promise(self, node_promise: input_schema.NodePromise, track_history: bool = True):
329
602
  """Adds a placeholder node to the graph that is not yet fully configured.
330
603
 
331
604
  Useful for building the graph structure before all settings are available.
605
+ Automatically captures history for undo/redo support.
332
606
 
333
607
  Args:
334
608
  node_promise: A promise object containing basic node information.
609
+ track_history: Whether to track this change in history (default True).
335
610
  """
611
+ def _do_add():
612
+ def placeholder(n: FlowNode = None):
613
+ if n is None:
614
+ return FlowDataEngine()
615
+ return n
616
+
617
+ self.add_node_step(
618
+ node_id=node_promise.node_id,
619
+ node_type=node_promise.node_type,
620
+ function=placeholder,
621
+ setting_input=node_promise,
622
+ )
623
+ if node_promise.is_user_defined:
624
+ node_needs_settings: bool
625
+ custom_node = CUSTOM_NODE_STORE.get(node_promise.node_type)
626
+ if custom_node is None:
627
+ raise Exception(f"Custom node type '{node_promise.node_type}' not found in registry.")
628
+ settings_schema = custom_node.model_fields["settings_schema"].default
629
+ node_needs_settings = settings_schema is not None and not settings_schema.is_empty()
630
+ if not node_needs_settings:
631
+ user_defined_node_settings = input_schema.UserDefinedNode(settings={}, **node_promise.model_dump())
632
+ initialized_model = custom_node()
633
+ self.add_user_defined_node(
634
+ custom_node=initialized_model, user_defined_node_settings=user_defined_node_settings
635
+ )
336
636
 
337
- def placeholder(n: FlowNode = None):
338
- if n is None:
339
- return FlowDataEngine()
340
- return n
341
-
342
- self.add_node_step(
343
- node_id=node_promise.node_id,
344
- node_type=node_promise.node_type,
345
- function=placeholder,
346
- setting_input=node_promise,
347
- )
348
- if node_promise.is_user_defined:
349
- node_needs_settings: bool
350
- custom_node = CUSTOM_NODE_STORE.get(node_promise.node_type)
351
- if custom_node is None:
352
- raise Exception(f"Custom node type '{node_promise.node_type}' not found in registry.")
353
- settings_schema = custom_node.model_fields["settings_schema"].default
354
- node_needs_settings = settings_schema is not None and not settings_schema.is_empty()
355
- if not node_needs_settings:
356
- user_defined_node_settings = input_schema.UserDefinedNode(settings={}, **node_promise.model_dump())
357
- initialized_model = custom_node()
358
- self.add_user_defined_node(
359
- custom_node=initialized_model, user_defined_node_settings=user_defined_node_settings
360
- )
637
+ if track_history:
638
+ self._execute_with_history(
639
+ _do_add,
640
+ HistoryActionType.ADD_NODE,
641
+ f"Add {node_promise.node_type} node",
642
+ node_id=node_promise.node_id,
643
+ )
644
+ else:
645
+ _do_add()
361
646
 
362
647
  def apply_layout(self, y_spacing: int = 150, x_spacing: int = 200, initial_y: int = 100):
363
648
  """Calculates and applies a layered layout to all nodes in the graph.
@@ -497,9 +782,10 @@ class FlowGraph:
497
782
  add_un_drawn_nodes(drawn_nodes, node_info, lines)
498
783
 
499
784
  try:
500
- skip_nodes, ordered_nodes = compute_execution_plan(
785
+ execution_plan = compute_execution_plan(
501
786
  nodes=self.nodes, flow_starts=self._flow_starts + self.get_implicit_starter_nodes()
502
787
  )
788
+ ordered_nodes = execution_plan.all_nodes
503
789
  if ordered_nodes:
504
790
  for i, node in enumerate(ordered_nodes, 1):
505
791
  lines.append(f" {i:3d}. {node_info[node.node_id].label}")
@@ -579,6 +865,7 @@ class FlowGraph:
579
865
  node = self.get_node(user_defined_node_settings.node_id)
580
866
  self.add_node_to_starting_list(node)
581
867
 
868
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
582
869
  def add_pivot(self, pivot_settings: input_schema.NodePivot):
583
870
  """Adds a pivot node to the graph.
584
871
 
@@ -607,6 +894,7 @@ class FlowGraph:
607
894
 
608
895
  node.schema_callback = schema_callback
609
896
 
897
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
610
898
  def add_unpivot(self, unpivot_settings: input_schema.NodeUnpivot):
611
899
  """Adds an unpivot node to the graph.
612
900
 
@@ -625,6 +913,7 @@ class FlowGraph:
625
913
  input_node_ids=[unpivot_settings.depending_on_id],
626
914
  )
627
915
 
916
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
628
917
  def add_union(self, union_settings: input_schema.NodeUnion):
629
918
  """Adds a union node to combine multiple data streams.
630
919
 
@@ -644,15 +933,30 @@ class FlowGraph:
644
933
  input_node_ids=union_settings.depending_on_ids,
645
934
  )
646
935
 
647
- def add_initial_node_analysis(self, node_promise: input_schema.NodePromise):
936
+ def add_initial_node_analysis(self, node_promise: input_schema.NodePromise, track_history: bool = True):
648
937
  """Adds a data exploration/analysis node based on a node promise.
649
938
 
939
+ Automatically captures history for undo/redo support.
940
+
650
941
  Args:
651
942
  node_promise: The promise representing the node to be analyzed.
943
+ track_history: Whether to track this change in history (default True).
652
944
  """
653
- node_analysis = create_graphic_walker_node_from_node_promise(node_promise)
654
- self.add_explore_data(node_analysis)
945
+ def _do_add():
946
+ node_analysis = create_graphic_walker_node_from_node_promise(node_promise)
947
+ self.add_explore_data(node_analysis)
948
+
949
+ if track_history:
950
+ self._execute_with_history(
951
+ _do_add,
952
+ HistoryActionType.ADD_NODE,
953
+ f"Add {node_promise.node_type} node",
954
+ node_id=node_promise.node_id,
955
+ )
956
+ else:
957
+ _do_add()
655
958
 
959
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
656
960
  def add_explore_data(self, node_analysis: input_schema.NodeExploreData):
657
961
  """Adds a specialized node for data exploration and visualization.
658
962
 
@@ -697,6 +1001,7 @@ class FlowGraph:
697
1001
  )
698
1002
  node = self.get_node(node_analysis.node_id)
699
1003
 
1004
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
700
1005
  def add_group_by(self, group_by_settings: input_schema.NodeGroupBy):
701
1006
  """Adds a group-by aggregation node to the graph.
702
1007
 
@@ -729,124 +1034,13 @@ class FlowGraph:
729
1034
 
730
1035
  node.schema_callback = schema_callback
731
1036
 
1037
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
732
1038
  def add_filter(self, filter_settings: input_schema.NodeFilter):
733
1039
  """Adds a filter node to the graph.
734
1040
 
735
1041
  Args:
736
1042
  filter_settings: The settings for the filter operation.
737
1043
  """
738
- from flowfile_core.schemas.transform_schema import FilterOperator
739
-
740
- def _build_basic_filter_expression(
741
- basic_filter: transform_schema.BasicFilter, field_data_type: str | None = None
742
- ) -> str:
743
- """Build a filter expression string from a BasicFilter object.
744
-
745
- Uses the Flowfile expression language that is compatible with polars_expr_transformer.
746
-
747
- Args:
748
- basic_filter: The basic filter configuration.
749
- field_data_type: The data type of the field (optional, for smart quoting).
750
-
751
- Returns:
752
- A filter expression string compatible with polars_expr_transformer.
753
- """
754
- field = f"[{basic_filter.field}]"
755
- value = basic_filter.value
756
- value2 = basic_filter.value2
757
-
758
- is_numeric_value = value.replace(".", "", 1).replace("-", "", 1).isnumeric() if value else False
759
- should_quote = field_data_type == "str" or not is_numeric_value
760
-
761
- try:
762
- operator = basic_filter.get_operator()
763
- except (ValueError, AttributeError):
764
- operator = FilterOperator.from_symbol(str(basic_filter.operator))
765
-
766
- if operator == FilterOperator.EQUALS:
767
- if should_quote:
768
- return f'{field}="{value}"'
769
- return f"{field}={value}"
770
-
771
- elif operator == FilterOperator.NOT_EQUALS:
772
- if should_quote:
773
- return f'{field}!="{value}"'
774
- return f"{field}!={value}"
775
-
776
- elif operator == FilterOperator.GREATER_THAN:
777
- if should_quote:
778
- return f'{field}>"{value}"'
779
- return f"{field}>{value}"
780
-
781
- elif operator == FilterOperator.GREATER_THAN_OR_EQUALS:
782
- if should_quote:
783
- return f'{field}>="{value}"'
784
- return f"{field}>={value}"
785
-
786
- elif operator == FilterOperator.LESS_THAN:
787
- if should_quote:
788
- return f'{field}<"{value}"'
789
- return f"{field}<{value}"
790
-
791
- elif operator == FilterOperator.LESS_THAN_OR_EQUALS:
792
- if should_quote:
793
- return f'{field}<="{value}"'
794
- return f"{field}<={value}"
795
-
796
- elif operator == FilterOperator.CONTAINS:
797
- return f'contains({field}, "{value}")'
798
-
799
- elif operator == FilterOperator.NOT_CONTAINS:
800
- return f'contains({field}, "{value}") = false'
801
-
802
- elif operator == FilterOperator.STARTS_WITH:
803
- return f'left({field}, {len(value)}) = "{value}"'
804
-
805
- elif operator == FilterOperator.ENDS_WITH:
806
- return f'right({field}, {len(value)}) = "{value}"'
807
-
808
- elif operator == FilterOperator.IS_NULL:
809
- return f"is_empty({field})"
810
-
811
- elif operator == FilterOperator.IS_NOT_NULL:
812
- return f"is_not_empty({field})"
813
-
814
- elif operator == FilterOperator.IN:
815
- values = [v.strip() for v in value.split(",")]
816
- if len(values) == 1:
817
- if should_quote:
818
- return f'{field}="{values[0]}"'
819
- return f"{field}={values[0]}"
820
- if should_quote:
821
- conditions = [f'({field}="{v}")' for v in values]
822
- else:
823
- conditions = [f"({field}={v})" for v in values]
824
- return " | ".join(conditions)
825
-
826
- elif operator == FilterOperator.NOT_IN:
827
- values = [v.strip() for v in value.split(",")]
828
- if len(values) == 1:
829
- if should_quote:
830
- return f'{field}!="{values[0]}"'
831
- return f"{field}!={values[0]}"
832
- if should_quote:
833
- conditions = [f'({field}!="{v}")' for v in values]
834
- else:
835
- conditions = [f"({field}!={v})" for v in values]
836
- return " & ".join(conditions)
837
-
838
- elif operator == FilterOperator.BETWEEN:
839
- if value2 is None:
840
- raise ValueError("BETWEEN operator requires value2")
841
- if should_quote:
842
- return f'({field}>="{value}") & ({field}<="{value2}")'
843
- return f"({field}>={value}) & ({field}<={value2})"
844
-
845
- else:
846
- # Fallback for unknown operators - use legacy format
847
- if should_quote:
848
- return f'{field}{operator.to_symbol()}"{value}"'
849
- return f"{field}{operator.to_symbol()}{value}"
850
1044
 
851
1045
  def _func(fl: FlowDataEngine):
852
1046
  is_advanced = filter_settings.filter_input.is_advanced()
@@ -865,7 +1059,7 @@ class FlowGraph:
865
1059
  except Exception:
866
1060
  field_data_type = None
867
1061
 
868
- expression = _build_basic_filter_expression(basic_filter, field_data_type)
1062
+ expression = build_filter_expression(basic_filter, field_data_type)
869
1063
  filter_settings.filter_input.advanced_filter = expression
870
1064
  return fl.do_filter(expression)
871
1065
 
@@ -878,6 +1072,7 @@ class FlowGraph:
878
1072
  input_node_ids=[filter_settings.depending_on_id],
879
1073
  )
880
1074
 
1075
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
881
1076
  def add_record_count(self, node_number_of_records: input_schema.NodeRecordCount):
882
1077
  """Adds a filter node to the graph.
883
1078
 
@@ -896,6 +1091,7 @@ class FlowGraph:
896
1091
  input_node_ids=[node_number_of_records.depending_on_id],
897
1092
  )
898
1093
 
1094
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
899
1095
  def add_polars_code(self, node_polars_code: input_schema.NodePolarsCode):
900
1096
  """Adds a node that executes custom Polars code.
901
1097
 
@@ -940,6 +1136,7 @@ class FlowGraph:
940
1136
  node_id=node_promise.node_id, node_type=node_promise.node_type, function=_func, setting_input=node_promise
941
1137
  )
942
1138
 
1139
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
943
1140
  def add_unique(self, unique_settings: input_schema.NodeUnique):
944
1141
  """Adds a node to find and remove duplicate rows.
945
1142
 
@@ -959,6 +1156,7 @@ class FlowGraph:
959
1156
  input_node_ids=[unique_settings.depending_on_id],
960
1157
  )
961
1158
 
1159
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
962
1160
  def add_graph_solver(self, graph_solver_settings: input_schema.NodeGraphSolver):
963
1161
  """Adds a node that solves graph-like problems within the data.
964
1162
 
@@ -982,6 +1180,7 @@ class FlowGraph:
982
1180
  input_node_ids=[graph_solver_settings.depending_on_id],
983
1181
  )
984
1182
 
1183
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
985
1184
  def add_formula(self, function_settings: input_schema.NodeFormula):
986
1185
  """Adds a node that applies a formula to create or modify a column.
987
1186
 
@@ -1024,6 +1223,7 @@ class FlowGraph:
1024
1223
  else:
1025
1224
  return True, ""
1026
1225
 
1226
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1027
1227
  def add_cross_join(self, cross_join_settings: input_schema.NodeCrossJoin) -> "FlowGraph":
1028
1228
  """Adds a cross join node to the graph.
1029
1229
 
@@ -1056,6 +1256,7 @@ class FlowGraph:
1056
1256
  )
1057
1257
  return self
1058
1258
 
1259
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1059
1260
  def add_join(self, join_settings: input_schema.NodeJoin) -> "FlowGraph":
1060
1261
  """Adds a join node to combine two data streams based on key columns.
1061
1262
 
@@ -1088,6 +1289,7 @@ class FlowGraph:
1088
1289
  )
1089
1290
  return self
1090
1291
 
1292
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1091
1293
  def add_fuzzy_match(self, fuzzy_settings: input_schema.NodeFuzzyMatch) -> "FlowGraph":
1092
1294
  """Adds a fuzzy matching node to join data on approximate string matches.
1093
1295
 
@@ -1141,6 +1343,7 @@ class FlowGraph:
1141
1343
 
1142
1344
  return self
1143
1345
 
1346
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1144
1347
  def add_text_to_rows(self, node_text_to_rows: input_schema.NodeTextToRows) -> "FlowGraph":
1145
1348
  """Adds a node that splits cell values into multiple rows.
1146
1349
 
@@ -1167,6 +1370,7 @@ class FlowGraph:
1167
1370
  )
1168
1371
  return self
1169
1372
 
1373
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1170
1374
  def add_sort(self, sort_settings: input_schema.NodeSort) -> "FlowGraph":
1171
1375
  """Adds a node to sort the data based on one or more columns.
1172
1376
 
@@ -1189,6 +1393,7 @@ class FlowGraph:
1189
1393
  )
1190
1394
  return self
1191
1395
 
1396
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1192
1397
  def add_sample(self, sample_settings: input_schema.NodeSample) -> "FlowGraph":
1193
1398
  """Adds a node to take a random or top-N sample of the data.
1194
1399
 
@@ -1211,6 +1416,7 @@ class FlowGraph:
1211
1416
  )
1212
1417
  return self
1213
1418
 
1419
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1214
1420
  def add_record_id(self, record_id_settings: input_schema.NodeRecordId) -> "FlowGraph":
1215
1421
  """Adds a node to create a new column with a unique ID for each record.
1216
1422
 
@@ -1234,6 +1440,7 @@ class FlowGraph:
1234
1440
  )
1235
1441
  return self
1236
1442
 
1443
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1237
1444
  def add_select(self, select_settings: input_schema.NodeSelect) -> "FlowGraph":
1238
1445
  """Adds a node to select, rename, reorder, or drop columns.
1239
1446
 
@@ -1359,6 +1566,34 @@ class FlowGraph:
1359
1566
  Returns:
1360
1567
  The created or updated FlowNode object.
1361
1568
  """
1569
+ # Wrap schema_callback with output_field_config support
1570
+ # If the node has output_field_config enabled, use it for schema prediction
1571
+ output_field_config = getattr(setting_input, 'output_field_config', None) if setting_input else None
1572
+
1573
+ logger.info(
1574
+ f"add_node_step: node_id={node_id}, node_type={node_type}, "
1575
+ f"has_setting_input={setting_input is not None}, "
1576
+ f"has_output_field_config={output_field_config is not None}, "
1577
+ f"config_enabled={output_field_config.enabled if output_field_config else False}, "
1578
+ f"has_schema_callback={schema_callback is not None}"
1579
+ )
1580
+
1581
+ # IMPORTANT: Always create wrapped callback if output_field_config exists (even if enabled=False)
1582
+ # This ensures nodes like PolarsCode get a schema callback when output_field_config is defined
1583
+ if output_field_config:
1584
+ if output_field_config.enabled:
1585
+ logger.info(
1586
+ f"add_node_step: Creating/wrapping schema_callback for node {node_id} with output_field_config "
1587
+ f"(validation_mode={output_field_config.validation_mode_behavior}, {len(output_field_config.fields)} fields, "
1588
+ f"base_callback={'present' if schema_callback else 'None'})"
1589
+ )
1590
+ else:
1591
+ logger.debug(f"add_node_step: output_field_config present for node {node_id} but disabled")
1592
+
1593
+ # Even if schema_callback is None, create a wrapped one for output_field_config
1594
+ schema_callback = create_schema_callback_with_output_config(schema_callback, output_field_config)
1595
+ logger.info(f"add_node_step: schema_callback {'created' if schema_callback else 'failed'} for node {node_id}")
1596
+
1362
1597
  existing_node = self.get_node(node_id)
1363
1598
  if existing_node is not None:
1364
1599
  if existing_node.node_type != node_type:
@@ -1420,6 +1655,7 @@ class FlowGraph:
1420
1655
  self._output_cols.append(column)
1421
1656
  return self
1422
1657
 
1658
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1423
1659
  def add_output(self, output_file: input_schema.NodeOutput):
1424
1660
  """Adds an output node to write the final data to a destination.
1425
1661
 
@@ -1453,6 +1689,7 @@ class FlowGraph:
1453
1689
  input_node_ids=[input_node_id],
1454
1690
  )
1455
1691
 
1692
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1456
1693
  def add_database_writer(self, node_database_writer: input_schema.NodeDatabaseWriter):
1457
1694
  """Adds a node to write data to a database.
1458
1695
 
@@ -1514,6 +1751,7 @@ class FlowGraph:
1514
1751
  )
1515
1752
  node = self.get_node(node_database_writer.node_id)
1516
1753
 
1754
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1517
1755
  def add_database_reader(self, node_database_reader: input_schema.NodeDatabaseReader):
1518
1756
  """Adds a node to read data from a database.
1519
1757
 
@@ -1616,6 +1854,7 @@ class FlowGraph:
1616
1854
  logger.info("Adding sql source")
1617
1855
  self.add_external_source(external_source_input)
1618
1856
 
1857
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1619
1858
  def add_cloud_storage_writer(self, node_cloud_storage_writer: input_schema.NodeCloudStorageWriter) -> None:
1620
1859
  """Adds a node to write data to a cloud storage provider.
1621
1860
 
@@ -1678,6 +1917,7 @@ class FlowGraph:
1678
1917
 
1679
1918
  node = self.get_node(node_cloud_storage_writer.node_id)
1680
1919
 
1920
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1681
1921
  def add_cloud_storage_reader(self, node_cloud_storage_reader: input_schema.NodeCloudStorageReader) -> None:
1682
1922
  """Adds a cloud storage read node to the flow graph.
1683
1923
 
@@ -1711,6 +1951,7 @@ class FlowGraph:
1711
1951
  )
1712
1952
  self.add_node_to_starting_list(node)
1713
1953
 
1954
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1714
1955
  def add_external_source(self, external_source_input: input_schema.NodeExternalSource):
1715
1956
  """Adds a node for a custom external data source.
1716
1957
 
@@ -1783,6 +2024,7 @@ class FlowGraph:
1783
2024
  setting_input=external_source_input,
1784
2025
  )
1785
2026
 
2027
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1786
2028
  def add_read(self, input_file: input_schema.NodeRead):
1787
2029
  """Adds a node to read data from a local file (e.g., CSV, Parquet, Excel).
1788
2030
 
@@ -1870,6 +2112,7 @@ class FlowGraph:
1870
2112
  node.user_provided_schema_callback = schema_callback
1871
2113
  return self
1872
2114
 
2115
+ @with_history_capture(HistoryActionType.UPDATE_SETTINGS)
1873
2116
  def add_datasource(self, input_file: input_schema.NodeDatasource | input_schema.NodeManualInput) -> "FlowGraph":
1874
2117
  """Adds a data source node to the graph.
1875
2118
 
@@ -1977,10 +2220,10 @@ class FlowGraph:
1977
2220
  flow_node = self._node_db.get(node_id)
1978
2221
  if not flow_node:
1979
2222
  raise Exception("Node not found found")
1980
- skip_nodes, execution_order = compute_execution_plan(
2223
+ execution_plan = compute_execution_plan(
1981
2224
  nodes=self.nodes, flow_starts=self._flow_starts + self.get_implicit_starter_nodes()
1982
2225
  )
1983
- if flow_node.node_id in [skip_node.node_id for skip_node in skip_nodes]:
2226
+ if flow_node.node_id in [skip_node.node_id for skip_node in execution_plan.skip_nodes]:
1984
2227
  raise Exception("Node can not be executed because it does not have it's inputs")
1985
2228
 
1986
2229
  def create_initial_run_information(self, number_of_nodes: int, run_type: Literal["fetch_one", "full_run"]):
@@ -2049,11 +2292,66 @@ class FlowGraph:
2049
2292
  finally:
2050
2293
  self.flow_settings.is_running = False
2051
2294
 
2295
+ def _execute_single_node(
2296
+ self,
2297
+ node: FlowNode,
2298
+ performance_mode: bool,
2299
+ run_info_lock: threading.Lock,
2300
+ ) -> tuple[NodeResult, FlowNode]:
2301
+ """Executes a single node, records its result, and returns both.
2302
+
2303
+ Thread-safe: uses run_info_lock when mutating shared run information.
2304
+
2305
+ Args:
2306
+ node: The node to execute.
2307
+ performance_mode: Whether to run in performance mode.
2308
+ run_info_lock: Lock protecting shared RunInformation state.
2309
+
2310
+ Returns:
2311
+ A (NodeResult, FlowNode) tuple for post-stage failure propagation.
2312
+ """
2313
+ node_logger = self.flow_logger.get_node_logger(node.node_id)
2314
+ node_result = NodeResult(node_id=node.node_id, node_name=node.name)
2315
+
2316
+ with run_info_lock:
2317
+ self.latest_run_info.node_step_result.append(node_result)
2318
+
2319
+ logger.info(f"Starting to run: node {node.node_id}, start time: {node_result.start_timestamp}")
2320
+ node.execute_node(
2321
+ run_location=self.flow_settings.execution_location,
2322
+ performance_mode=performance_mode,
2323
+ node_logger=node_logger,
2324
+ )
2325
+ try:
2326
+ node_result.error = str(node.results.errors)
2327
+ if self.flow_settings.is_canceled:
2328
+ node_result.success = None
2329
+ node_result.is_running = False
2330
+ return node_result, node
2331
+ node_result.success = node.results.errors is None
2332
+ node_result.end_timestamp = time()
2333
+ node_result.run_time = int(node_result.end_timestamp - node_result.start_timestamp)
2334
+ node_result.is_running = False
2335
+ except Exception as e:
2336
+ node_result.error = "Node did not run"
2337
+ node_result.success = False
2338
+ node_result.end_timestamp = time()
2339
+ node_result.run_time = int(node_result.end_timestamp - node_result.start_timestamp)
2340
+ node_result.is_running = False
2341
+ node_logger.error(f"Error in node {node.node_id}: {e}")
2342
+
2343
+ node_logger.info(f"Completed node with success: {node_result.success}")
2344
+ with run_info_lock:
2345
+ self.latest_run_info.nodes_completed += 1
2346
+
2347
+ return node_result, node
2348
+
2052
2349
  def run_graph(self) -> RunInformation | None:
2053
2350
  """Executes the entire data flow graph from start to finish.
2054
2351
 
2055
- It determines the correct execution order, runs each node,
2056
- collects results, and handles errors and cancellations.
2352
+ Independent nodes within the same execution stage are run in parallel
2353
+ using threads. Stages are processed sequentially so that all dependencies
2354
+ are satisfied before a stage begins.
2057
2355
 
2058
2356
  Returns:
2059
2357
  A RunInformation object summarizing the execution results.
@@ -2068,54 +2366,64 @@ class FlowGraph:
2068
2366
  self.flow_settings.is_canceled = False
2069
2367
  self.flow_logger.clear_log_file()
2070
2368
  self.flow_logger.info("Starting to run flowfile flow...")
2071
- skip_nodes, execution_order = compute_execution_plan(
2369
+ execution_plan = compute_execution_plan(
2072
2370
  nodes=self.nodes, flow_starts=self._flow_starts + self.get_implicit_starter_nodes()
2073
2371
  )
2074
2372
 
2075
- self.latest_run_info = self.create_initial_run_information(len(execution_order), "full_run")
2373
+ self.latest_run_info = self.create_initial_run_information(
2374
+ execution_plan.node_count, "full_run"
2375
+ )
2076
2376
 
2077
- skip_node_message(self.flow_logger, skip_nodes)
2078
- execution_order_message(self.flow_logger, execution_order)
2377
+ skip_node_message(self.flow_logger, execution_plan.skip_nodes)
2378
+ execution_order_message(self.flow_logger, execution_plan.stages)
2079
2379
  performance_mode = self.flow_settings.execution_mode == "Performance"
2080
2380
 
2081
- for node in execution_order:
2082
- node_logger = self.flow_logger.get_node_logger(node.node_id)
2381
+ run_info_lock = threading.Lock()
2382
+ skip_node_ids: set[str | int] = {n.node_id for n in execution_plan.skip_nodes}
2383
+
2384
+ for stage in execution_plan.stages:
2083
2385
  if self.flow_settings.is_canceled:
2084
2386
  self.flow_logger.info("Flow canceled")
2085
2387
  break
2086
- if node in skip_nodes:
2087
- node_logger.info(f"Skipping node {node.node_id}")
2388
+
2389
+ nodes_to_run = [n for n in stage.nodes if n.node_id not in skip_node_ids]
2390
+
2391
+ for skipped in stage.nodes:
2392
+ if skipped.node_id in skip_node_ids:
2393
+ node_logger = self.flow_logger.get_node_logger(skipped.node_id)
2394
+ node_logger.info(f"Skipping node {skipped.node_id}")
2395
+
2396
+ if not nodes_to_run:
2088
2397
  continue
2089
- node_result = NodeResult(node_id=node.node_id, node_name=node.name)
2090
- self.latest_run_info.node_step_result.append(node_result)
2091
- logger.info(f"Starting to run: node {node.node_id}, start time: {node_result.start_timestamp}")
2092
- node.execute_node(
2093
- run_location=self.flow_settings.execution_location,
2094
- performance_mode=performance_mode,
2095
- node_logger=node_logger,
2096
- )
2097
- try:
2098
- node_result.error = str(node.results.errors)
2099
- if self.flow_settings.is_canceled:
2100
- node_result.success = None
2101
- node_result.success = None
2102
- node_result.is_running = False
2103
- continue
2104
- node_result.success = node.results.errors is None
2105
- node_result.end_timestamp = time()
2106
- node_result.run_time = int(node_result.end_timestamp - node_result.start_timestamp)
2107
- node_result.is_running = False
2108
- except Exception as e:
2109
- node_result.error = "Node did not run"
2110
- node_result.success = False
2111
- node_result.end_timestamp = time()
2112
- node_result.run_time = int(node_result.end_timestamp - node_result.start_timestamp)
2113
- node_result.is_running = False
2114
- node_logger.error(f"Error in node {node.node_id}: {e}")
2115
- if not node_result.success:
2116
- skip_nodes.extend(list(node.get_all_dependent_nodes()))
2117
- node_logger.info(f"Completed node with success: {node_result.success}")
2118
- self.latest_run_info.nodes_completed += 1
2398
+
2399
+ is_local = self.flow_settings.execution_location == "local"
2400
+ max_workers = 1 if is_local else self.flow_settings.max_parallel_workers
2401
+ if len(nodes_to_run) == 1 or max_workers == 1:
2402
+ # Single node or parallelism disabled — run sequentially
2403
+ stage_results = [
2404
+ self._execute_single_node(node, performance_mode, run_info_lock)
2405
+ for node in nodes_to_run
2406
+ ]
2407
+ else:
2408
+ # Multiple independent nodes — run in parallel
2409
+ stage_results: list[tuple[NodeResult, FlowNode]] = []
2410
+ workers = min(max_workers, len(nodes_to_run))
2411
+ with ThreadPoolExecutor(max_workers=workers) as executor:
2412
+ futures = {
2413
+ executor.submit(
2414
+ self._execute_single_node, node, performance_mode, run_info_lock
2415
+ ): node
2416
+ for node in nodes_to_run
2417
+ }
2418
+ for future in as_completed(futures):
2419
+ stage_results.append(future.result())
2420
+
2421
+ # After the stage completes, propagate failures to downstream nodes
2422
+ for node_result, node in stage_results:
2423
+ if not node_result.success:
2424
+ for dep in node.get_all_dependent_nodes():
2425
+ skip_node_ids.add(dep.node_id)
2426
+
2119
2427
  self.latest_run_info.end_time = datetime.datetime.now()
2120
2428
  self.flow_logger.info("Flow completed!")
2121
2429
  self.end_datetime = datetime.datetime.now()
@@ -2189,6 +2497,7 @@ class FlowGraph:
2189
2497
  type=node_info.type,
2190
2498
  is_start_node=node.node_id in start_node_ids,
2191
2499
  description=node_info.description,
2500
+ node_reference=node_info.node_reference,
2192
2501
  x_position=int(node_info.x_position),
2193
2502
  y_position=int(node_info.y_position),
2194
2503
  left_input_id=node_info.left_input_id,
@@ -2205,6 +2514,7 @@ class FlowGraph:
2205
2514
  execution_location=self.flow_settings.execution_location,
2206
2515
  auto_save=self.flow_settings.auto_save,
2207
2516
  show_detailed_progress=self.flow_settings.show_detailed_progress,
2517
+ max_parallel_workers=self.flow_settings.max_parallel_workers,
2208
2518
  )
2209
2519
  return schemas.FlowfileData(
2210
2520
  flowfile_version=__version__,
@@ -2456,6 +2766,10 @@ def combine_existing_settings_and_new_settings(setting_input: Any, new_settings:
2456
2766
  if hasattr(new_settings, field) and getattr(new_settings, field) is not None:
2457
2767
  setattr(copied_setting_input, field, getattr(new_settings, field))
2458
2768
 
2769
+ # Reset node_reference to None when copying (so it defaults to df_{node_id})
2770
+ if hasattr(copied_setting_input, "node_reference"):
2771
+ copied_setting_input.node_reference = None
2772
+
2459
2773
  return copied_setting_input
2460
2774
 
2461
2775