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
@@ -0,0 +1,401 @@
1
+ """
2
+ History Manager for undo/redo functionality in flow graphs.
3
+
4
+ This module provides the HistoryManager class which manages undo/redo stacks
5
+ and enables users to revert or reapply changes to their flow graphs.
6
+
7
+ Optimizations:
8
+ - Compressed snapshots using zlib (60-80% memory reduction)
9
+ - Pre-computed hashes for O(1) snapshot comparison
10
+ - __slots__ for memory-efficient entry storage
11
+ """
12
+
13
+ from collections import deque
14
+ from time import time
15
+ from typing import TYPE_CHECKING
16
+
17
+ from flowfile_core.configs import logger
18
+ from flowfile_core.schemas.history_schema import (
19
+ CompressedSnapshot,
20
+ HistoryActionType,
21
+ HistoryConfig,
22
+ HistoryEntry,
23
+ HistoryState,
24
+ UndoRedoResult,
25
+ )
26
+ from flowfile_core.schemas.schemas import FlowfileData
27
+
28
+ if TYPE_CHECKING:
29
+ from flowfile_core.flowfile.flow_graph import FlowGraph
30
+
31
+
32
+ class HistoryManager:
33
+ """Manages undo/redo history for a FlowGraph.
34
+
35
+ Uses two deques (undo_stack and redo_stack) to track state changes.
36
+ Snapshots are captured BEFORE changes occur, so undo restores to that state.
37
+
38
+ Memory Optimization:
39
+ - Snapshots are compressed using zlib (typically 60-80% size reduction)
40
+ - Hashes are pre-computed for O(1) equality checks
41
+ - HistoryEntry uses __slots__ for reduced memory overhead
42
+ """
43
+
44
+ __slots__ = ('_config', '_undo_stack', '_redo_stack', '_is_restoring', '_last_snapshot_hash')
45
+
46
+ def __init__(self, config: HistoryConfig | None = None):
47
+ """Initialize the HistoryManager.
48
+
49
+ Args:
50
+ config: Optional configuration for history behavior.
51
+ """
52
+ self._config = config or HistoryConfig()
53
+ self._undo_stack: deque[HistoryEntry] = deque(maxlen=self._config.max_stack_size)
54
+ self._redo_stack: deque[HistoryEntry] = deque(maxlen=self._config.max_stack_size)
55
+ self._is_restoring: bool = False
56
+ self._last_snapshot_hash: int | None = None
57
+
58
+ @property
59
+ def config(self) -> HistoryConfig:
60
+ """Get the history configuration."""
61
+ return self._config
62
+
63
+ @config.setter
64
+ def config(self, config: HistoryConfig):
65
+ """Set the history configuration.
66
+
67
+ Note: Changing max_stack_size won't resize existing stacks.
68
+ """
69
+ self._config = config
70
+
71
+ def _create_entry(
72
+ self,
73
+ snapshot_dict: dict,
74
+ action_type: HistoryActionType,
75
+ description: str,
76
+ node_id: int | None = None,
77
+ ) -> HistoryEntry:
78
+ """Create a history entry with the configured compression settings.
79
+
80
+ Args:
81
+ snapshot_dict: The flow state dictionary.
82
+ action_type: The type of action.
83
+ description: Human-readable description.
84
+ node_id: Optional affected node ID.
85
+
86
+ Returns:
87
+ A new HistoryEntry instance.
88
+ """
89
+ return HistoryEntry.from_dict(
90
+ snapshot_dict=snapshot_dict,
91
+ action_type=action_type,
92
+ description=description,
93
+ timestamp=time(),
94
+ node_id=node_id,
95
+ compression_level=self._config.compression_level if self._config.use_compression else 1,
96
+ )
97
+
98
+ def capture_snapshot(
99
+ self,
100
+ flow_graph: "FlowGraph",
101
+ action_type: HistoryActionType,
102
+ description: str,
103
+ node_id: int | None = None,
104
+ ) -> bool:
105
+ """Capture the current state of the flow graph BEFORE a change.
106
+
107
+ This method captures state BEFORE an operation. We detect duplicates by
108
+ comparing against the last CAPTURED snapshot (top of undo stack), not
109
+ against _last_snapshot_hash (which tracks the post-operation state).
110
+
111
+ Args:
112
+ flow_graph: The FlowGraph to capture.
113
+ action_type: The type of action being performed.
114
+ description: Human-readable description of the action.
115
+ node_id: Optional ID of the affected node.
116
+
117
+ Returns:
118
+ True if snapshot was captured, False if skipped (disabled or restoring).
119
+ """
120
+ logger.info(f"History: capture_snapshot called for '{description}' (enabled={self._config.enabled}, restoring={self._is_restoring})")
121
+
122
+ if not self._config.enabled:
123
+ logger.info(f"History: Skipping '{description}' - history disabled")
124
+ return False
125
+
126
+ if self._is_restoring:
127
+ logger.info(f"History: Skipping '{description}' - currently restoring")
128
+ return False
129
+
130
+ try:
131
+ # Get the current state as FlowfileData
132
+ flowfile_data = flow_graph.get_flowfile_data()
133
+ snapshot_dict = flowfile_data.model_dump()
134
+
135
+ # Compute hash for duplicate detection
136
+ current_hash = CompressedSnapshot._compute_hash(snapshot_dict)
137
+
138
+ # Compare against the LAST CAPTURED snapshot (top of undo stack), not _last_snapshot_hash
139
+ # This correctly detects if we're capturing the same pre-state twice,
140
+ # without being confused by post-operation hash updates from capture_if_changed
141
+ if self._undo_stack:
142
+ last_entry_hash = self._undo_stack[-1].snapshot_hash
143
+ if last_entry_hash == current_hash:
144
+ logger.info(f"History: Skipping duplicate snapshot for: {description}")
145
+ return False
146
+
147
+ # Create compressed entry
148
+ entry = self._create_entry(snapshot_dict, action_type, description, node_id)
149
+
150
+ # Add to undo stack
151
+ self._undo_stack.append(entry)
152
+
153
+ # Clear redo stack when new action is performed
154
+ self._redo_stack.clear()
155
+
156
+ logger.info(
157
+ f"History: Captured '{description}' "
158
+ f"(undo_stack={len(self._undo_stack)}, redo_stack={len(self._redo_stack)})"
159
+ )
160
+ return True
161
+
162
+ except Exception as e:
163
+ logger.error(f"History: Failed to capture snapshot for '{description}': {e}")
164
+ return False
165
+
166
+ def capture_if_changed(
167
+ self,
168
+ flow_graph: "FlowGraph",
169
+ pre_snapshot: FlowfileData,
170
+ action_type: HistoryActionType,
171
+ description: str,
172
+ node_id: int | None = None,
173
+ ) -> bool:
174
+ """Capture history only if the flow state actually changed.
175
+
176
+ Use this for settings updates where the change might be a no-op.
177
+ Call this AFTER the change is applied.
178
+
179
+ Args:
180
+ flow_graph: The FlowGraph after the change.
181
+ pre_snapshot: The FlowfileData captured BEFORE the change.
182
+ action_type: The type of action that was performed.
183
+ description: Human-readable description of the action.
184
+ node_id: Optional ID of the affected node.
185
+
186
+ Returns:
187
+ True if a change was detected and snapshot was captured.
188
+ """
189
+ if not self._config.enabled:
190
+ logger.debug(f"History: Skipping '{description}' (if_changed) - history disabled")
191
+ return False
192
+
193
+ if self._is_restoring:
194
+ logger.debug(f"History: Skipping '{description}' (if_changed) - currently restoring")
195
+ return False
196
+
197
+ try:
198
+ # Get the current (post-change) state
199
+ current_snapshot = flow_graph.get_flowfile_data()
200
+ current_dict = current_snapshot.model_dump()
201
+ pre_dict = pre_snapshot.model_dump()
202
+
203
+ # Fast hash comparison (no JSON serialization)
204
+ pre_hash = CompressedSnapshot._compute_hash(pre_dict)
205
+ current_hash = CompressedSnapshot._compute_hash(current_dict)
206
+
207
+ if pre_hash == current_hash:
208
+ logger.debug(f"History: No change detected for: {description}")
209
+ return False
210
+
211
+ # State changed - capture the BEFORE state (compressed)
212
+ entry = self._create_entry(pre_dict, action_type, description, node_id)
213
+
214
+ # Add to undo stack
215
+ self._undo_stack.append(entry)
216
+ self._last_snapshot_hash = current_hash
217
+
218
+ # Clear redo stack when new action is performed
219
+ self._redo_stack.clear()
220
+
221
+ logger.info(
222
+ f"History: Captured '{description}' (after change detection) "
223
+ f"(undo_stack={len(self._undo_stack)}, redo_stack={len(self._redo_stack)})"
224
+ )
225
+ return True
226
+
227
+ except Exception as e:
228
+ logger.error(f"History: Failed to capture snapshot for '{description}': {e}")
229
+ return False
230
+
231
+ def undo(self, flow_graph: "FlowGraph") -> UndoRedoResult:
232
+ """Undo the last action by restoring to the previous state.
233
+
234
+ Args:
235
+ flow_graph: The FlowGraph to restore.
236
+
237
+ Returns:
238
+ UndoRedoResult indicating success or failure.
239
+ """
240
+ if not self._undo_stack:
241
+ return UndoRedoResult(
242
+ success=False,
243
+ error_message="Nothing to undo",
244
+ )
245
+
246
+ try:
247
+ # Set flag to prevent capturing during restore
248
+ self._is_restoring = True
249
+
250
+ # Get the entry to restore from
251
+ entry = self._undo_stack.pop()
252
+
253
+ # Save current state to redo stack BEFORE restoring
254
+ current_snapshot = flow_graph.get_flowfile_data()
255
+ current_dict = current_snapshot.model_dump()
256
+ redo_entry = self._create_entry(
257
+ current_dict,
258
+ entry.action_type,
259
+ entry.description,
260
+ entry.node_id,
261
+ )
262
+ self._redo_stack.append(redo_entry)
263
+
264
+ # Decompress and restore the flow graph from the snapshot
265
+ snapshot_dict = entry.get_snapshot()
266
+ snapshot_data = FlowfileData.model_validate(snapshot_dict)
267
+ flow_graph.restore_from_snapshot(snapshot_data)
268
+
269
+ # Update last snapshot hash
270
+ self._last_snapshot_hash = entry.snapshot_hash
271
+
272
+ logger.info(f"Undo successful: {entry.description}")
273
+ return UndoRedoResult(
274
+ success=True,
275
+ action_description=entry.description,
276
+ )
277
+
278
+ except Exception as e:
279
+ logger.error(f"Undo failed: {e}")
280
+ return UndoRedoResult(
281
+ success=False,
282
+ error_message=str(e),
283
+ )
284
+
285
+ finally:
286
+ self._is_restoring = False
287
+
288
+ def redo(self, flow_graph: "FlowGraph") -> UndoRedoResult:
289
+ """Redo the last undone action.
290
+
291
+ Args:
292
+ flow_graph: The FlowGraph to restore.
293
+
294
+ Returns:
295
+ UndoRedoResult indicating success or failure.
296
+ """
297
+ if not self._redo_stack:
298
+ return UndoRedoResult(
299
+ success=False,
300
+ error_message="Nothing to redo",
301
+ )
302
+
303
+ try:
304
+ # Set flag to prevent capturing during restore
305
+ self._is_restoring = True
306
+
307
+ # Get the entry to restore from
308
+ entry = self._redo_stack.pop()
309
+
310
+ # Save current state to undo stack BEFORE restoring
311
+ current_snapshot = flow_graph.get_flowfile_data()
312
+ current_dict = current_snapshot.model_dump()
313
+ undo_entry = self._create_entry(
314
+ current_dict,
315
+ entry.action_type,
316
+ entry.description,
317
+ entry.node_id,
318
+ )
319
+ self._undo_stack.append(undo_entry)
320
+
321
+ # Decompress and restore the flow graph from the snapshot
322
+ snapshot_dict = entry.get_snapshot()
323
+ snapshot_data = FlowfileData.model_validate(snapshot_dict)
324
+ flow_graph.restore_from_snapshot(snapshot_data)
325
+
326
+ # Update last snapshot hash
327
+ self._last_snapshot_hash = entry.snapshot_hash
328
+
329
+ logger.info(f"Redo successful: {entry.description}")
330
+ return UndoRedoResult(
331
+ success=True,
332
+ action_description=entry.description,
333
+ )
334
+
335
+ except Exception as e:
336
+ logger.error(f"Redo failed: {e}")
337
+ return UndoRedoResult(
338
+ success=False,
339
+ error_message=str(e),
340
+ )
341
+
342
+ finally:
343
+ self._is_restoring = False
344
+
345
+ def get_state(self) -> HistoryState:
346
+ """Get the current state of the history system.
347
+
348
+ Returns:
349
+ HistoryState with information about available undo/redo operations.
350
+ """
351
+ can_undo = len(self._undo_stack) > 0
352
+ can_redo = len(self._redo_stack) > 0
353
+
354
+ undo_description = None
355
+ if can_undo:
356
+ undo_description = self._undo_stack[-1].description
357
+
358
+ redo_description = None
359
+ if can_redo:
360
+ redo_description = self._redo_stack[-1].description
361
+
362
+ return HistoryState(
363
+ can_undo=can_undo,
364
+ can_redo=can_redo,
365
+ undo_description=undo_description,
366
+ redo_description=redo_description,
367
+ undo_count=len(self._undo_stack),
368
+ redo_count=len(self._redo_stack),
369
+ )
370
+
371
+ def clear(self) -> None:
372
+ """Clear all history entries."""
373
+ self._undo_stack.clear()
374
+ self._redo_stack.clear()
375
+ self._last_snapshot_hash = None
376
+ logger.debug("History cleared")
377
+
378
+ def is_restoring(self) -> bool:
379
+ """Check if a restore operation is currently in progress.
380
+
381
+ Returns:
382
+ True if undo/redo is in progress.
383
+ """
384
+ return self._is_restoring
385
+
386
+ def get_memory_usage(self) -> dict:
387
+ """Get memory usage statistics for the history stacks.
388
+
389
+ Returns:
390
+ Dictionary with memory usage information.
391
+ """
392
+ undo_size = sum(e.compressed_size for e in self._undo_stack)
393
+ redo_size = sum(e.compressed_size for e in self._redo_stack)
394
+
395
+ return {
396
+ "undo_stack_entries": len(self._undo_stack),
397
+ "redo_stack_entries": len(self._redo_stack),
398
+ "undo_stack_bytes": undo_size,
399
+ "redo_stack_bytes": redo_size,
400
+ "total_bytes": undo_size + redo_size,
401
+ }
@@ -454,6 +454,15 @@ def ensure_flow_settings(flow_storage_obj: schemas.FlowInformation, flow_path: s
454
454
  if not hasattr(fs, "show_detailed_progress"):
455
455
  fs.show_detailed_progress = True
456
456
 
457
+ # For track_history, we need to handle legacy pickled objects that were
458
+ # serialized before this field existed. Use object.__setattr__ to bypass
459
+ # Pydantic validation which would reject adding a new field.
460
+ if "track_history" not in fs.__dict__:
461
+ object.__setattr__(fs, '__dict__', {**fs.__dict__, 'track_history': True})
462
+
463
+ if "max_parallel_workers" not in fs.__dict__ or fs.max_parallel_workers is None:
464
+ object.__setattr__(fs, '__dict__', {**fs.__dict__, 'max_parallel_workers': 4})
465
+
457
466
  return flow_storage_obj
458
467
 
459
468
 
@@ -173,6 +173,7 @@ def _flowfile_data_to_flow_information(flowfile_data: schemas.FlowfileData) -> s
173
173
  setting_data["pos_x"] = float(node.x_position or 0)
174
174
  setting_data["pos_y"] = float(node.y_position or 0)
175
175
  setting_data["description"] = node.description or ""
176
+ setting_data["node_reference"] = node.node_reference
176
177
  setting_data["is_setup"] = True
177
178
 
178
179
  if is_user_defined:
@@ -209,6 +210,7 @@ def _flowfile_data_to_flow_information(flowfile_data: schemas.FlowfileData) -> s
209
210
  type=node.type,
210
211
  is_setup=setting_input is not None,
211
212
  description=node.description,
213
+ node_reference=node.node_reference,
212
214
  x_position=node.x_position,
213
215
  y_position=node.y_position,
214
216
  left_input_id=node.left_input_id,
@@ -231,6 +233,7 @@ def _flowfile_data_to_flow_information(flowfile_data: schemas.FlowfileData) -> s
231
233
  execution_location=flowfile_data.flowfile_settings.execution_location,
232
234
  auto_save=flowfile_data.flowfile_settings.auto_save,
233
235
  show_detailed_progress=flowfile_data.flowfile_settings.show_detailed_progress,
236
+ max_parallel_workers=flowfile_data.flowfile_settings.max_parallel_workers,
234
237
  )
235
238
 
236
239
  return schemas.FlowInformation(
@@ -305,7 +308,6 @@ def open_flow(flow_path: Path) -> FlowGraph:
305
308
  ingestion_order = determine_insertion_order(flow_storage_obj)
306
309
  new_flow = FlowGraph(name=flow_storage_obj.flow_name, flow_settings=flow_storage_obj.flow_settings)
307
310
  # Create new FlowGraph
308
-
309
311
  # First pass: add node promises
310
312
  for node_id in ingestion_order:
311
313
  node_info: schemas.NodeInformation = flow_storage_obj.data[node_id]
@@ -1,8 +1,8 @@
1
1
  import base64
2
- from typing import Literal
2
+ from typing import Annotated, Any, Literal
3
3
 
4
4
  import polars as pl
5
- from pydantic import BaseModel
5
+ from pydantic import BaseModel, BeforeValidator, PlainSerializer
6
6
 
7
7
  from flowfile_core.schemas.input_schema import (
8
8
  DatabaseConnection,
@@ -12,6 +12,22 @@ from flowfile_core.schemas.input_schema import (
12
12
  )
13
13
 
14
14
 
15
+ # Custom type for bytes that serializes to/from base64 string in JSON
16
+ def _decode_bytes(v: Any) -> bytes:
17
+ if isinstance(v, bytes):
18
+ return v
19
+ if isinstance(v, str):
20
+ return base64.b64decode(v)
21
+ raise ValueError(f"Expected bytes or base64 string, got {type(v)}")
22
+
23
+
24
+ Base64Bytes = Annotated[
25
+ bytes,
26
+ BeforeValidator(_decode_bytes),
27
+ PlainSerializer(lambda x: base64.b64encode(x).decode('ascii'), return_type=str),
28
+ ]
29
+
30
+
15
31
  class ExtDatabaseConnection(DatabaseConnection):
16
32
  """Database connection configuration with password handling."""
17
33
 
@@ -26,7 +42,7 @@ class DatabaseExternalWriteSettings(BaseModel):
26
42
  if_exists: Literal["append", "replace", "fail"] | None = "append"
27
43
  flowfile_flow_id: int = 1
28
44
  flowfile_node_id: int | str = -1
29
- operation: str
45
+ operation: Base64Bytes # Accepts bytes or base64 string, serializes to base64
30
46
 
31
47
  @classmethod
32
48
  def create_from_from_node_database_writer(
@@ -60,7 +76,7 @@ class DatabaseExternalWriteSettings(BaseModel):
60
76
  if_exists=node_database_writer.database_write_settings.if_exists,
61
77
  flowfile_flow_id=node_database_writer.flow_id,
62
78
  flowfile_node_id=node_database_writer.node_id,
63
- operation=base64.b64encode(lf.serialize()).decode(),
79
+ operation=lf.serialize(), # Pass raw bytes, Base64Bytes handles encoding
64
80
  )
65
81
 
66
82
 
@@ -1,45 +1,90 @@
1
1
  from collections import defaultdict, deque
2
+ from dataclasses import dataclass
2
3
 
3
4
  from flowfile_core.configs import logger
4
5
  from flowfile_core.flowfile.flow_node.flow_node import FlowNode
5
6
  from flowfile_core.flowfile.util.node_skipper import determine_nodes_to_skip
6
7
 
7
8
 
8
- def compute_execution_plan(nodes: list[FlowNode], flow_starts: list[FlowNode] = None):
9
- """Computes the execution order after finding the nodes to skip on the execution step."""
9
+ @dataclass(frozen=True)
10
+ class ExecutionStage:
11
+ """A group of nodes with no mutual dependencies that can execute in parallel."""
12
+
13
+ nodes: list[FlowNode]
14
+
15
+ def __len__(self) -> int:
16
+ return len(self.nodes)
17
+
18
+ def __iter__(self):
19
+ return iter(self.nodes)
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class ExecutionPlan:
24
+ """Complete execution plan: nodes to skip and ordered stages of parallelizable nodes."""
25
+
26
+ skip_nodes: list[FlowNode]
27
+ stages: list[ExecutionStage]
28
+
29
+ @property
30
+ def all_nodes(self) -> list[FlowNode]:
31
+ """Flattened list of all nodes across all stages, preserving topological order."""
32
+ return [node for stage in self.stages for node in stage.nodes]
33
+
34
+ @property
35
+ def node_count(self) -> int:
36
+ return sum(len(stage) for stage in self.stages)
37
+
38
+
39
+ def compute_execution_plan(nodes: list[FlowNode], flow_starts: list[FlowNode] = None) -> ExecutionPlan:
40
+ """Computes the execution plan: nodes to skip and parallelizable execution stages.
41
+
42
+ Args:
43
+ nodes: All nodes in the flow.
44
+ flow_starts: Explicit starting nodes for the flow.
45
+
46
+ Returns:
47
+ An ExecutionPlan containing skip_nodes and ordered execution stages.
48
+ """
10
49
  skip_nodes = determine_nodes_to_skip(nodes=nodes)
11
- computed_execution_order = determine_execution_order(
50
+ stages = determine_execution_order(
12
51
  all_nodes=[node for node in nodes if node not in skip_nodes], flow_starts=flow_starts
13
52
  )
14
- return skip_nodes, computed_execution_order
53
+ return ExecutionPlan(skip_nodes=skip_nodes, stages=stages)
15
54
 
16
55
 
17
- def determine_execution_order(all_nodes: list[FlowNode], flow_starts: list[FlowNode] = None) -> list[FlowNode]:
56
+ def determine_execution_order(
57
+ all_nodes: list[FlowNode], flow_starts: list[FlowNode] = None
58
+ ) -> list[ExecutionStage]:
18
59
  """
19
60
  Determines the execution order of nodes using topological sorting based on node dependencies.
61
+ Returns stages of nodes where each stage contains nodes that can execute in parallel.
20
62
 
21
63
  Args:
22
- all_nodes (List[FlowNode]): A list of all nodes (steps) in the flow.
23
- flow_starts (List[FlowNode], optional): A list of starting nodes for the flow. If not provided, the function starts with nodes having zero in-degree.
64
+ all_nodes: A list of all nodes in the flow.
65
+ flow_starts: Starting nodes for the flow. If not provided, starts with zero in-degree nodes.
24
66
 
25
67
  Returns:
26
- List[FlowNode]: A list of nodes in the order they should be executed.
68
+ A list of ExecutionStage objects in dependency order. Nodes within a stage have no
69
+ mutual dependencies and can run concurrently.
27
70
 
28
71
  Raises:
29
- Exception: If a cycle is detected, meaning that a valid execution order cannot be determined.
72
+ Exception: If a cycle is detected in the graph.
30
73
  """
31
74
  node_map = build_node_map(all_nodes)
32
75
  in_degree, adjacency_list = compute_in_degrees_and_adjacency_list(all_nodes, node_map)
33
76
 
34
77
  queue, visited_nodes = initialize_queue(flow_starts, all_nodes, in_degree)
35
78
 
36
- execution_order = perform_topological_sort(queue, node_map, in_degree, adjacency_list, visited_nodes)
37
- if len(execution_order) != len(node_map):
79
+ stages = perform_topological_sort(queue, node_map, in_degree, adjacency_list, visited_nodes)
80
+ total_nodes = sum(len(stage) for stage in stages)
81
+ if total_nodes != len(node_map):
38
82
  raise Exception("Cycle detected in the graph. Execution order cannot be determined.")
39
83
 
40
- logger.info(f"execution order: \n {[node for node in execution_order if node.is_correct]}")
84
+ all_nodes_flat = [node for stage in stages for node in stage if node.is_correct]
85
+ logger.info(f"execution order: \n {all_nodes_flat}")
41
86
 
42
- return execution_order
87
+ return stages
43
88
 
44
89
 
45
90
  def build_node_map(all_nodes: list[FlowNode]) -> dict[str, FlowNode]:
@@ -124,35 +169,43 @@ def perform_topological_sort(
124
169
  in_degree: dict[str, int],
125
170
  adjacency_list: dict[str, list[str]],
126
171
  visited_nodes: set[str],
127
- ) -> list[FlowNode]:
172
+ ) -> list[ExecutionStage]:
128
173
  """
129
- Performs topological sorting to determine the execution order of nodes.
174
+ Performs a level-based topological sort, grouping nodes into execution stages.
175
+ Nodes within the same stage have no dependencies on each other and can run in parallel.
130
176
 
131
177
  Args:
132
- queue (deque): A deque containing nodes with zero in-degree.
133
- node_map (Dict[str, FlowNode]): A dictionary mapping node IDs to FlowNode objects.
134
- in_degree (Dict[str, int]): A dictionary mapping node IDs to their in-degree count.
135
- adjacency_list (Dict[str, List[str]]): A dictionary mapping node IDs to a list of their connected nodes (outgoing edges).
136
- visited_nodes (Set[str]): A set of visited node IDs.
178
+ queue: A deque containing node IDs with zero in-degree.
179
+ node_map: A dictionary mapping node IDs to FlowNode objects.
180
+ in_degree: A dictionary mapping node IDs to their in-degree count.
181
+ adjacency_list: A dictionary mapping node IDs to a list of their outgoing node IDs.
182
+ visited_nodes: A set of visited node IDs.
137
183
 
138
184
  Returns:
139
- List[FlowNode]: A list of nodes in the order they should be executed.
185
+ A list of ExecutionStage objects. Each stage contains nodes that can execute concurrently.
140
186
  """
141
- execution_order = []
187
+ stages: list[ExecutionStage] = []
142
188
  logger.info("Starting topological sort to determine execution order")
143
189
 
144
190
  while queue:
145
- current_node_id = queue.popleft()
146
- current_node = node_map.get(current_node_id)
147
- if current_node is None:
148
- logger.warning(f"Node with ID {current_node_id} not found in the node map.")
149
- continue
150
- execution_order.append(current_node)
151
-
152
- for next_node_id in adjacency_list.get(current_node_id, []):
153
- in_degree[next_node_id] -= 1
154
- if in_degree[next_node_id] == 0 and next_node_id not in visited_nodes:
155
- queue.append(next_node_id)
156
- visited_nodes.add(next_node_id)
157
-
158
- return execution_order
191
+ current_stage_ids = list(queue)
192
+ queue.clear()
193
+
194
+ stage_nodes: list[FlowNode] = []
195
+ for node_id in current_stage_ids:
196
+ node = node_map.get(node_id)
197
+ if node is None:
198
+ logger.warning(f"Node with ID {node_id} not found in the node map.")
199
+ continue
200
+ stage_nodes.append(node)
201
+
202
+ for next_node_id in adjacency_list.get(node_id, []):
203
+ in_degree[next_node_id] -= 1
204
+ if in_degree[next_node_id] == 0 and next_node_id not in visited_nodes:
205
+ queue.append(next_node_id)
206
+ visited_nodes.add(next_node_id)
207
+
208
+ if stage_nodes:
209
+ stages.append(ExecutionStage(nodes=stage_nodes))
210
+
211
+ return stages