Flowfile 0.5.1__py3-none-any.whl → 0.5.4__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 (346) hide show
  1. build_backends/main.py +25 -22
  2. build_backends/main_prd.py +10 -19
  3. flowfile/__init__.py +194 -74
  4. flowfile/__main__.py +10 -7
  5. flowfile/api.py +51 -57
  6. flowfile/web/__init__.py +14 -9
  7. flowfile/web/static/assets/AdminView-f53bad23.css +129 -0
  8. flowfile/web/static/assets/AdminView-f9847d67.js +713 -0
  9. flowfile/web/static/assets/CloudConnectionView-cf85f943.css +72 -0
  10. flowfile/web/static/assets/{CloudConnectionManager-0dfba9f2.js → CloudConnectionView-faace55b.js} +11 -11
  11. flowfile/web/static/assets/{CloudStorageReader-29d14fcc.css → CloudStorageReader-24c54524.css} +27 -27
  12. flowfile/web/static/assets/{CloudStorageReader-d5b1b6c9.js → CloudStorageReader-d86ecaa7.js} +10 -8
  13. flowfile/web/static/assets/{CloudStorageWriter-00d87aad.js → CloudStorageWriter-0f4d9a44.js} +10 -8
  14. flowfile/web/static/assets/{CloudStorageWriter-b0ee067f.css → CloudStorageWriter-60547855.css} +26 -26
  15. flowfile/web/static/assets/ColumnActionInput-c44b7aee.css +159 -0
  16. flowfile/web/static/assets/ColumnActionInput-f4189ae0.js +330 -0
  17. flowfile/web/static/assets/{ColumnSelector-47996a16.css → ColumnSelector-371637fb.css} +2 -2
  18. flowfile/web/static/assets/{ColumnSelector-4685e75d.js → ColumnSelector-e66b33da.js} +3 -5
  19. flowfile/web/static/assets/ContextMenu-49463352.js +9 -0
  20. flowfile/web/static/assets/ContextMenu-dd5f3f25.js +9 -0
  21. flowfile/web/static/assets/ContextMenu-f709b884.js +9 -0
  22. flowfile/web/static/assets/ContextMenu.vue_vue_type_script_setup_true_lang-a1bd6314.js +59 -0
  23. flowfile/web/static/assets/{CrossJoin-702a3edd.js → CrossJoin-24694b8f.js} +12 -10
  24. flowfile/web/static/assets/{CrossJoin-1119d18e.css → CrossJoin-71b4cc10.css} +20 -20
  25. flowfile/web/static/assets/{CustomNode-b1519993.js → CustomNode-569d45ff.js} +43 -24
  26. flowfile/web/static/assets/CustomNode-edb9b939.css +42 -0
  27. flowfile/web/static/assets/{DatabaseConnectionSettings-0c04b2e5.css → DatabaseConnectionSettings-c20a1e16.css} +23 -21
  28. flowfile/web/static/assets/{DatabaseConnectionSettings-6f3e4ea5.js → DatabaseConnectionSettings-cfc08938.js} +5 -4
  29. flowfile/web/static/assets/{DatabaseReader-ae61773c.css → DatabaseReader-5bf8c75b.css} +41 -46
  30. flowfile/web/static/assets/{DatabaseReader-d38c7295.js → DatabaseReader-701feabb.js} +25 -15
  31. flowfile/web/static/assets/{DatabaseManager-cf5ef661.js → DatabaseView-0482e5b5.js} +11 -11
  32. flowfile/web/static/assets/DatabaseView-6655afd6.css +57 -0
  33. flowfile/web/static/assets/{DatabaseWriter-b04ef46a.js → DatabaseWriter-16721989.js} +17 -10
  34. flowfile/web/static/assets/{DatabaseWriter-2f570e53.css → DatabaseWriter-bdcf2c8b.css} +29 -27
  35. flowfile/web/static/assets/{designer-8da3ba3a.css → DesignerView-49abb835.css} +783 -663
  36. flowfile/web/static/assets/{designer-9633482a.js → DesignerView-f64749fb.js} +1292 -3253
  37. flowfile/web/static/assets/{documentation-ca400224.js → DocumentationView-61bd2990.js} +5 -5
  38. flowfile/web/static/assets/{documentation-12216a74.css → DocumentationView-9ea6e871.css} +9 -9
  39. flowfile/web/static/assets/{ExploreData-2d0cf4db.css → ExploreData-10c5acc8.css} +13 -12
  40. flowfile/web/static/assets/{ExploreData-5fa10ed8.js → ExploreData-e2735b13.js} +18 -9
  41. flowfile/web/static/assets/{ExternalSource-d39af878.js → ExternalSource-2535c3b2.js} +9 -7
  42. flowfile/web/static/assets/{ExternalSource-e37b6275.css → ExternalSource-7ac7373f.css} +20 -20
  43. flowfile/web/static/assets/Filter-2cdbc93c.js +287 -0
  44. flowfile/web/static/assets/Filter-7494ea97.css +48 -0
  45. flowfile/web/static/assets/{Formula-bb96803d.css → Formula-53d58c43.css} +7 -7
  46. flowfile/web/static/assets/{Formula-6b04fb1d.js → Formula-fcda3c2c.js} +13 -11
  47. flowfile/web/static/assets/{FuzzyMatch-1010f966.css → FuzzyMatch-ad6361d6.css} +68 -69
  48. flowfile/web/static/assets/{FuzzyMatch-999521f4.js → FuzzyMatch-f8d3b7d3.js} +12 -10
  49. flowfile/web/static/assets/{Pivot-cf333e3d.css → GraphSolver-4b4d7db9.css} +5 -5
  50. flowfile/web/static/assets/{GraphSolver-17dd2198.js → GraphSolver-72eaa695.js} +14 -12
  51. flowfile/web/static/assets/GroupBy-5792782d.css +9 -0
  52. flowfile/web/static/assets/{GroupBy-6b039e18.js → GroupBy-8aa0598b.js} +9 -7
  53. flowfile/web/static/assets/{Join-fd79b451.css → Join-28b5e18f.css} +22 -22
  54. flowfile/web/static/assets/{Join-24d0f113.js → Join-e40f0ffa.js} +13 -11
  55. flowfile/web/static/assets/LoginView-5111c9ae.js +134 -0
  56. flowfile/web/static/assets/LoginView-d325d632.css +172 -0
  57. flowfile/web/static/assets/ManualInput-3702e677.css +293 -0
  58. flowfile/web/static/assets/{ManualInput-34639209.js → ManualInput-9b6f3224.js} +170 -116
  59. flowfile/web/static/assets/{MultiSelect-0e8724a3.js → MultiSelect-ef28e19e.js} +2 -2
  60. flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-b0e538c2.js → MultiSelect.vue_vue_type_script_setup_true_lang-83b3bbfd.js} +1 -1
  61. flowfile/web/static/assets/NodeDesigner-94cd4dd3.css +1429 -0
  62. flowfile/web/static/assets/NodeDesigner-d2b7ee2b.js +2712 -0
  63. flowfile/web/static/assets/{NumericInput-3d63a470.js → NumericInput-1d789794.js} +2 -2
  64. flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-e0edeccc.js → NumericInput.vue_vue_type_script_setup_true_lang-7775f83e.js} +5 -2
  65. flowfile/web/static/assets/Output-692dd25d.css +37 -0
  66. flowfile/web/static/assets/{Output-edea9802.js → Output-cefef801.js} +14 -10
  67. flowfile/web/static/assets/{GraphSolver-f0cb7bfb.css → Pivot-0eda81b4.css} +5 -5
  68. flowfile/web/static/assets/{Pivot-61d19301.js → Pivot-bab1b75b.js} +12 -10
  69. flowfile/web/static/assets/PivotValidation-0e905b1a.css +13 -0
  70. flowfile/web/static/assets/PivotValidation-41b57ad6.css +13 -0
  71. flowfile/web/static/assets/{PivotValidation-f97fec5b.js → PivotValidation-e7941f91.js} +3 -3
  72. flowfile/web/static/assets/{PivotValidation-de9f43fe.js → PivotValidation-fba09336.js} +3 -3
  73. flowfile/web/static/assets/{PolarsCode-650322d1.css → PolarsCode-2b1f1f23.css} +4 -4
  74. flowfile/web/static/assets/{PolarsCode-bc3c9984.js → PolarsCode-740e40fa.js} +18 -9
  75. flowfile/web/static/assets/PopOver-862d7e28.js +939 -0
  76. flowfile/web/static/assets/PopOver-d96599db.css +33 -0
  77. flowfile/web/static/assets/{Read-64a3f259.js → Read-225cc63f.js} +16 -12
  78. flowfile/web/static/assets/{Read-e808b239.css → Read-90f366bc.css} +15 -15
  79. flowfile/web/static/assets/{RecordCount-3d5039be.js → RecordCount-ffc71eca.js} +6 -4
  80. flowfile/web/static/assets/{RecordId-597510e0.js → RecordId-a70bb8df.js} +9 -7
  81. flowfile/web/static/assets/{SQLQueryComponent-df51adbe.js → SQLQueryComponent-15a421f5.js} +3 -3
  82. flowfile/web/static/assets/SQLQueryComponent-edb90b98.css +29 -0
  83. flowfile/web/static/assets/{Sample-4be0a507.js → Sample-6c26afc7.js} +6 -4
  84. flowfile/web/static/assets/SecretSelector-6329f743.css +43 -0
  85. flowfile/web/static/assets/SecretSelector-ceed9496.js +113 -0
  86. flowfile/web/static/assets/{SecretManager-4839be57.js → SecretsView-214d255a.js} +35 -36
  87. flowfile/web/static/assets/SecretsView-aa291340.css +38 -0
  88. flowfile/web/static/assets/{Select-9b72f201.js → Select-8fc29999.js} +9 -7
  89. flowfile/web/static/assets/{SettingsSection-71e6b7e3.css → SettingsSection-07fbbc39.css} +4 -4
  90. flowfile/web/static/assets/{SettingsSection-5c696bee.css → SettingsSection-26fe48d4.css} +4 -4
  91. flowfile/web/static/assets/{SettingsSection-7ded385d.js → SettingsSection-3f70e4c3.js} +3 -3
  92. flowfile/web/static/assets/{SettingsSection-f0f75a42.js → SettingsSection-83090218.js} +3 -3
  93. flowfile/web/static/assets/{SettingsSection-2e4d03c4.css → SettingsSection-8f980839.css} +4 -4
  94. flowfile/web/static/assets/{SettingsSection-e1e9c953.js → SettingsSection-9f0d1725.js} +3 -3
  95. flowfile/web/static/assets/SetupView-3fa0aa03.js +160 -0
  96. flowfile/web/static/assets/SetupView-e2da3442.css +230 -0
  97. flowfile/web/static/assets/{SingleSelect-6c777aac.js → SingleSelect-a4a568cb.js} +2 -2
  98. flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-33e3ff9b.js → SingleSelect.vue_vue_type_script_setup_true_lang-c8ebdd33.js} +1 -1
  99. flowfile/web/static/assets/{SliderInput-7cb93e62.js → SliderInput-be533e71.js} +7 -4
  100. flowfile/web/static/assets/SliderInput-f2e4f23c.css +4 -0
  101. flowfile/web/static/assets/{Sort-6cbde21a.js → Sort-154dad81.js} +9 -7
  102. flowfile/web/static/assets/Sort-4abb7fae.css +9 -0
  103. flowfile/web/static/assets/{TextInput-d9a40c11.js → TextInput-454e2bda.js} +2 -2
  104. flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-5896c375.js → TextInput.vue_vue_type_script_setup_true_lang-e86510d0.js} +5 -2
  105. flowfile/web/static/assets/{TextToRows-5d2c1190.css → TextToRows-12afb4f4.css} +10 -10
  106. flowfile/web/static/assets/{TextToRows-c4fcbf4d.js → TextToRows-ea73433d.js} +11 -10
  107. flowfile/web/static/assets/{ToggleSwitch-4ef91d19.js → ToggleSwitch-9d7b30f1.js} +2 -2
  108. flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-38478c20.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-00f2580e.js} +1 -1
  109. flowfile/web/static/assets/{UnavailableFields-5edd5322.css → UnavailableFields-394a1f78.css} +14 -14
  110. flowfile/web/static/assets/{UnavailableFields-a03f512c.js → UnavailableFields-b72a2c72.js} +4 -4
  111. flowfile/web/static/assets/{Union-bfe9b996.js → Union-1e44f263.js} +8 -6
  112. flowfile/web/static/assets/{Union-af6c3d9b.css → Union-d6a8d7d5.css} +7 -7
  113. flowfile/web/static/assets/Unique-2b705521.css +3 -0
  114. flowfile/web/static/assets/{Unique-5d023a27.js → Unique-a3bc6d0a.js} +13 -10
  115. flowfile/web/static/assets/{Unpivot-1e422df3.css → Unpivot-b6ad6427.css} +7 -7
  116. flowfile/web/static/assets/{Unpivot-91cc5354.js → Unpivot-e27935fc.js} +11 -9
  117. flowfile/web/static/assets/{UnpivotValidation-7ee2de44.js → UnpivotValidation-72497680.js} +3 -3
  118. flowfile/web/static/assets/UnpivotValidation-d5ca3b7b.css +13 -0
  119. flowfile/web/static/assets/{VueGraphicWalker-ed5ab88b.css → VueGraphicWalker-430f0b86.css} +1 -1
  120. flowfile/web/static/assets/{VueGraphicWalker-e51b9924.js → VueGraphicWalker-d9ab70a3.js} +4 -4
  121. flowfile/web/static/assets/{api-cf1221f0.js → api-a2102880.js} +1 -1
  122. flowfile/web/static/assets/{api-c1bad5ca.js → api-f75042b0.js} +1 -1
  123. flowfile/web/static/assets/{dropDown-35135ba8.css → dropDown-1d6acbd9.css} +41 -41
  124. flowfile/web/static/assets/{dropDown-614b998d.js → dropDown-2798a109.js} +3 -3
  125. flowfile/web/static/assets/{fullEditor-f7971590.js → fullEditor-cf7d7d93.js} +11 -10
  126. flowfile/web/static/assets/{fullEditor-178376bb.css → fullEditor-fe9f7e18.css} +77 -65
  127. flowfile/web/static/assets/{genericNodeSettings-4fe5f36b.js → genericNodeSettings-14eac1c3.js} +5 -5
  128. flowfile/web/static/assets/{genericNodeSettings-924759c7.css → genericNodeSettings-3b2507ea.css} +10 -10
  129. flowfile/web/static/assets/{index-5429bbf8.js → index-387a6f18.js} +41806 -40958
  130. flowfile/web/static/assets/index-6b367bb5.js +38 -0
  131. flowfile/web/static/assets/{index-50508d4d.css → index-e96ab018.css} +2184 -569
  132. flowfile/web/static/assets/index-f0a6e5a5.js +2696 -0
  133. flowfile/web/static/assets/node.types-2c15bb7e.js +82 -0
  134. flowfile/web/static/assets/nodeInput-ed2ae8d7.js +2 -0
  135. flowfile/web/static/assets/{outputCsv-076b85ab.js → outputCsv-3c1757e8.js} +3 -3
  136. flowfile/web/static/assets/outputCsv-b9a072af.css +2499 -0
  137. flowfile/web/static/assets/{outputExcel-0fd17dbe.js → outputExcel-686e1f48.js} +3 -3
  138. flowfile/web/static/assets/{outputExcel-b41305c0.css → outputExcel-f5d272b2.css} +26 -26
  139. flowfile/web/static/assets/outputParquet-54597c3c.css +4 -0
  140. flowfile/web/static/assets/{outputParquet-b61e0847.js → outputParquet-df28faa7.js} +4 -4
  141. flowfile/web/static/assets/{readCsv-c767cb37.css → readCsv-3bfac4c3.css} +15 -15
  142. flowfile/web/static/assets/{readCsv-a8bb8b61.js → readCsv-e37eee21.js} +3 -3
  143. flowfile/web/static/assets/{readExcel-806d2826.css → readExcel-3db6b763.css} +13 -13
  144. flowfile/web/static/assets/{readExcel-67b4aee0.js → readExcel-a13f14bb.js} +5 -5
  145. flowfile/web/static/assets/{readParquet-92ce1dbc.js → readParquet-344cf746.js} +3 -3
  146. flowfile/web/static/assets/{readParquet-48c81530.css → readParquet-c5244ad5.css} +4 -4
  147. flowfile/web/static/assets/secrets.api-ae198c5c.js +65 -0
  148. flowfile/web/static/assets/{selectDynamic-92e25ee3.js → selectDynamic-6b4b0767.js} +5 -5
  149. flowfile/web/static/assets/{selectDynamic-aa913ff4.css → selectDynamic-f2fb394f.css} +21 -20
  150. flowfile/web/static/assets/{vue-codemirror.esm-41b0e0d7.js → vue-codemirror.esm-31ba0e0b.js} +31 -640
  151. flowfile/web/static/assets/{vue-content-loader.es-2c8e608f.js → vue-content-loader.es-4469c8ff.js} +1 -1
  152. flowfile/web/static/index.html +2 -2
  153. {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/METADATA +3 -4
  154. flowfile-0.5.4.dist-info/RECORD +407 -0
  155. flowfile_core/__init__.py +13 -6
  156. flowfile_core/auth/jwt.py +51 -16
  157. flowfile_core/auth/models.py +32 -7
  158. flowfile_core/auth/password.py +89 -0
  159. flowfile_core/auth/secrets.py +64 -19
  160. flowfile_core/configs/__init__.py +9 -7
  161. flowfile_core/configs/flow_logger.py +15 -14
  162. flowfile_core/configs/node_store/__init__.py +72 -4
  163. flowfile_core/configs/node_store/nodes.py +155 -172
  164. flowfile_core/configs/node_store/user_defined_node_registry.py +108 -27
  165. flowfile_core/configs/settings.py +28 -15
  166. flowfile_core/database/connection.py +7 -6
  167. flowfile_core/database/init_db.py +96 -2
  168. flowfile_core/database/models.py +3 -1
  169. flowfile_core/fileExplorer/__init__.py +17 -0
  170. flowfile_core/fileExplorer/funcs.py +145 -57
  171. flowfile_core/fileExplorer/utils.py +10 -11
  172. flowfile_core/flowfile/_extensions/real_time_interface.py +10 -8
  173. flowfile_core/flowfile/analytics/analytics_processor.py +26 -24
  174. flowfile_core/flowfile/analytics/graphic_walker.py +11 -12
  175. flowfile_core/flowfile/analytics/utils.py +1 -1
  176. flowfile_core/flowfile/code_generator/__init__.py +11 -0
  177. flowfile_core/flowfile/code_generator/code_generator.py +706 -247
  178. flowfile_core/flowfile/connection_manager/_connection_manager.py +6 -5
  179. flowfile_core/flowfile/connection_manager/models.py +1 -1
  180. flowfile_core/flowfile/database_connection_manager/db_connections.py +60 -44
  181. flowfile_core/flowfile/database_connection_manager/models.py +1 -1
  182. flowfile_core/flowfile/extensions.py +17 -12
  183. flowfile_core/flowfile/flow_data_engine/cloud_storage_reader.py +34 -32
  184. flowfile_core/flowfile/flow_data_engine/create/funcs.py +115 -83
  185. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +493 -423
  186. flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +2 -2
  187. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +92 -52
  188. flowfile_core/flowfile/flow_data_engine/flow_file_column/polars_type.py +12 -11
  189. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +6 -6
  190. flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +26 -30
  191. flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +31 -20
  192. flowfile_core/flowfile/flow_data_engine/join/__init__.py +1 -1
  193. flowfile_core/flowfile/flow_data_engine/join/utils.py +11 -9
  194. flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +14 -15
  195. flowfile_core/flowfile/flow_data_engine/pivot_table.py +5 -7
  196. flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +95 -82
  197. flowfile_core/flowfile/flow_data_engine/read_excel_tables.py +66 -65
  198. flowfile_core/flowfile/flow_data_engine/sample_data.py +27 -21
  199. flowfile_core/flowfile/flow_data_engine/subprocess_operations/__init__.py +1 -1
  200. flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +13 -11
  201. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +190 -127
  202. flowfile_core/flowfile/flow_data_engine/threaded_processes.py +8 -8
  203. flowfile_core/flowfile/flow_data_engine/utils.py +99 -67
  204. flowfile_core/flowfile/flow_graph.py +920 -571
  205. flowfile_core/flowfile/flow_graph_utils.py +31 -49
  206. flowfile_core/flowfile/flow_node/flow_node.py +379 -258
  207. flowfile_core/flowfile/flow_node/models.py +53 -41
  208. flowfile_core/flowfile/flow_node/schema_callback.py +14 -19
  209. flowfile_core/flowfile/graph_tree/graph_tree.py +41 -41
  210. flowfile_core/flowfile/handler.py +80 -30
  211. flowfile_core/flowfile/manage/compatibility_enhancements.py +209 -126
  212. flowfile_core/flowfile/manage/io_flowfile.py +54 -57
  213. flowfile_core/flowfile/node_designer/__init__.py +19 -13
  214. flowfile_core/flowfile/node_designer/_type_registry.py +34 -37
  215. flowfile_core/flowfile/node_designer/custom_node.py +162 -36
  216. flowfile_core/flowfile/node_designer/ui_components.py +278 -34
  217. flowfile_core/flowfile/schema_callbacks.py +71 -51
  218. flowfile_core/flowfile/setting_generator/__init__.py +0 -1
  219. flowfile_core/flowfile/setting_generator/setting_generator.py +6 -5
  220. flowfile_core/flowfile/setting_generator/settings.py +64 -53
  221. flowfile_core/flowfile/sources/external_sources/base_class.py +12 -10
  222. flowfile_core/flowfile/sources/external_sources/custom_external_sources/external_source.py +27 -17
  223. flowfile_core/flowfile/sources/external_sources/custom_external_sources/sample_users.py +9 -9
  224. flowfile_core/flowfile/sources/external_sources/factory.py +0 -1
  225. flowfile_core/flowfile/sources/external_sources/sql_source/models.py +45 -31
  226. flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +198 -73
  227. flowfile_core/flowfile/sources/external_sources/sql_source/utils.py +250 -196
  228. flowfile_core/flowfile/util/calculate_layout.py +9 -13
  229. flowfile_core/flowfile/util/execution_orderer.py +25 -17
  230. flowfile_core/flowfile/util/node_skipper.py +4 -4
  231. flowfile_core/flowfile/utils.py +19 -21
  232. flowfile_core/main.py +26 -19
  233. flowfile_core/routes/auth.py +284 -11
  234. flowfile_core/routes/cloud_connections.py +25 -25
  235. flowfile_core/routes/logs.py +21 -29
  236. flowfile_core/routes/public.py +46 -4
  237. flowfile_core/routes/routes.py +70 -34
  238. flowfile_core/routes/secrets.py +25 -27
  239. flowfile_core/routes/user_defined_components.py +483 -4
  240. flowfile_core/run_lock.py +0 -1
  241. flowfile_core/schemas/__init__.py +4 -6
  242. flowfile_core/schemas/analysis_schemas/graphic_walker_schemas.py +55 -55
  243. flowfile_core/schemas/cloud_storage_schemas.py +96 -66
  244. flowfile_core/schemas/input_schema.py +231 -144
  245. flowfile_core/schemas/output_model.py +49 -34
  246. flowfile_core/schemas/schemas.py +116 -89
  247. flowfile_core/schemas/transform_schema.py +518 -263
  248. flowfile_core/schemas/yaml_types.py +21 -7
  249. flowfile_core/secret_manager/secret_manager.py +123 -18
  250. flowfile_core/types.py +29 -9
  251. flowfile_core/utils/arrow_reader.py +7 -6
  252. flowfile_core/utils/excel_file_manager.py +3 -3
  253. flowfile_core/utils/fileManager.py +7 -7
  254. flowfile_core/utils/fl_executor.py +8 -10
  255. flowfile_core/utils/utils.py +4 -4
  256. flowfile_core/utils/validate_setup.py +5 -4
  257. flowfile_frame/__init__.py +117 -51
  258. flowfile_frame/adapters.py +2 -9
  259. flowfile_frame/adding_expr.py +73 -32
  260. flowfile_frame/cloud_storage/frame_helpers.py +27 -23
  261. flowfile_frame/cloud_storage/secret_manager.py +12 -26
  262. flowfile_frame/config.py +2 -5
  263. flowfile_frame/database/__init__.py +36 -0
  264. flowfile_frame/database/connection_manager.py +205 -0
  265. flowfile_frame/database/frame_helpers.py +249 -0
  266. flowfile_frame/expr.py +311 -218
  267. flowfile_frame/expr.pyi +160 -159
  268. flowfile_frame/expr_name.py +23 -23
  269. flowfile_frame/flow_frame.py +571 -476
  270. flowfile_frame/flow_frame.pyi +123 -104
  271. flowfile_frame/flow_frame_methods.py +227 -246
  272. flowfile_frame/group_frame.py +50 -20
  273. flowfile_frame/join.py +2 -2
  274. flowfile_frame/lazy.py +129 -87
  275. flowfile_frame/lazy_methods.py +83 -30
  276. flowfile_frame/list_name_space.py +55 -50
  277. flowfile_frame/selectors.py +148 -68
  278. flowfile_frame/series.py +9 -7
  279. flowfile_frame/utils.py +19 -21
  280. flowfile_worker/__init__.py +12 -7
  281. flowfile_worker/configs.py +41 -33
  282. flowfile_worker/create/__init__.py +14 -9
  283. flowfile_worker/create/funcs.py +114 -77
  284. flowfile_worker/create/models.py +46 -43
  285. flowfile_worker/create/pl_types.py +14 -15
  286. flowfile_worker/create/read_excel_tables.py +34 -41
  287. flowfile_worker/create/utils.py +22 -19
  288. flowfile_worker/external_sources/s3_source/main.py +18 -51
  289. flowfile_worker/external_sources/s3_source/models.py +34 -27
  290. flowfile_worker/external_sources/sql_source/main.py +8 -5
  291. flowfile_worker/external_sources/sql_source/models.py +13 -9
  292. flowfile_worker/flow_logger.py +10 -8
  293. flowfile_worker/funcs.py +214 -155
  294. flowfile_worker/main.py +11 -17
  295. flowfile_worker/models.py +35 -28
  296. flowfile_worker/process_manager.py +2 -3
  297. flowfile_worker/routes.py +121 -90
  298. flowfile_worker/secrets.py +114 -21
  299. flowfile_worker/spawner.py +89 -54
  300. flowfile_worker/utils.py +3 -2
  301. shared/__init__.py +2 -7
  302. shared/storage_config.py +25 -13
  303. test_utils/postgres/commands.py +3 -2
  304. test_utils/postgres/fixtures.py +9 -9
  305. test_utils/s3/commands.py +1 -1
  306. test_utils/s3/data_generator.py +3 -4
  307. test_utils/s3/demo_data_generator.py +4 -7
  308. test_utils/s3/fixtures.py +7 -5
  309. tools/migrate/__init__.py +1 -1
  310. tools/migrate/__main__.py +16 -29
  311. tools/migrate/legacy_schemas.py +251 -190
  312. tools/migrate/migrate.py +193 -181
  313. tools/migrate/tests/conftest.py +1 -3
  314. tools/migrate/tests/test_migrate.py +36 -41
  315. tools/migrate/tests/test_migration_e2e.py +28 -29
  316. tools/migrate/tests/test_node_migrations.py +50 -20
  317. flowfile/web/static/assets/CloudConnectionManager-2dfdce2f.css +0 -86
  318. flowfile/web/static/assets/ContextMenu-23e909da.js +0 -41
  319. flowfile/web/static/assets/ContextMenu-4c74eef1.css +0 -26
  320. flowfile/web/static/assets/ContextMenu-63cfa99b.css +0 -26
  321. flowfile/web/static/assets/ContextMenu-70ae0c79.js +0 -41
  322. flowfile/web/static/assets/ContextMenu-c13f91d0.css +0 -26
  323. flowfile/web/static/assets/ContextMenu-f149cf7c.js +0 -41
  324. flowfile/web/static/assets/CustomNode-74a37f74.css +0 -32
  325. flowfile/web/static/assets/DatabaseManager-30fa27e5.css +0 -64
  326. flowfile/web/static/assets/Filter-9b6d08db.js +0 -164
  327. flowfile/web/static/assets/Filter-f62091b3.css +0 -20
  328. flowfile/web/static/assets/GroupBy-b9505323.css +0 -51
  329. flowfile/web/static/assets/ManualInput-3246a08d.css +0 -96
  330. flowfile/web/static/assets/Output-283fe388.css +0 -37
  331. flowfile/web/static/assets/PivotValidation-891ddfb0.css +0 -13
  332. flowfile/web/static/assets/PivotValidation-c46cd420.css +0 -13
  333. flowfile/web/static/assets/SQLQueryComponent-36cef432.css +0 -27
  334. flowfile/web/static/assets/SliderInput-b8fb6a8c.css +0 -4
  335. flowfile/web/static/assets/Sort-3643d625.css +0 -51
  336. flowfile/web/static/assets/Unique-f9fb0809.css +0 -51
  337. flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +0 -13
  338. flowfile/web/static/assets/nodeInput-5d0d6b79.js +0 -41
  339. flowfile/web/static/assets/outputCsv-9cc59e0b.css +0 -2499
  340. flowfile/web/static/assets/outputParquet-cf8cf3f2.css +0 -4
  341. flowfile/web/static/assets/secretApi-68435402.js +0 -46
  342. flowfile/web/static/assets/vue-codemirror-bccfde04.css +0 -32
  343. flowfile-0.5.1.dist-info/RECORD +0 -388
  344. {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/WHEEL +0 -0
  345. {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/entry_points.txt +0 -0
  346. {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/licenses/LICENSE +0 -0
@@ -1,22 +1,35 @@
1
- from typing import List, Union, Callable, Any, Optional, Generator, Literal
2
- from flowfile_core.configs import logger
3
- from flowfile_core.flowfile.flow_data_engine.flow_file_column.main import FlowfileColumn
4
- from flowfile_core.flowfile.flow_data_engine.flow_data_engine import FlowDataEngine
5
- from flowfile_core.utils.arrow_reader import get_read_top_n
6
- from flowfile_core.schemas import input_schema, schemas
7
- from flowfile_core.configs.flow_logger import NodeLogger
8
-
9
- from flowfile_core.schemas.output_model import TableExample, FileColumn, NodeData
10
- from flowfile_core.flowfile.utils import get_hash
11
- from flowfile_core.configs import node_store
12
- from flowfile_core.flowfile.setting_generator import setting_generator, setting_updator
1
+ import threading
2
+ from collections.abc import Callable, Generator
13
3
  from time import sleep
4
+ from typing import Any, Literal, Optional
5
+
6
+ from flowfile_core.configs import logger, node_store
7
+ from flowfile_core.configs.flow_logger import NodeLogger
8
+ from flowfile_core.flowfile.flow_data_engine.flow_data_engine import FlowDataEngine
9
+ from flowfile_core.flowfile.flow_data_engine.flow_file_column.main import FlowfileColumn
14
10
  from flowfile_core.flowfile.flow_data_engine.subprocess_operations import (
15
- ExternalDfFetcher, ExternalSampler, clear_task_from_worker, results_exists, get_external_df_result,
16
- ExternalDatabaseFetcher, ExternalDatabaseWriter, ExternalCloudWriter)
17
- from flowfile_core.flowfile.flow_node.models import (NodeStepSettings, NodeStepInputs, NodeSchemaInformation,
18
- NodeStepStats, NodeResults)
11
+ ExternalCloudWriter,
12
+ ExternalDatabaseFetcher,
13
+ ExternalDatabaseWriter,
14
+ ExternalDfFetcher,
15
+ ExternalSampler,
16
+ clear_task_from_worker,
17
+ get_external_df_result,
18
+ results_exists,
19
+ )
20
+ from flowfile_core.flowfile.flow_node.models import (
21
+ NodeResults,
22
+ NodeSchemaInformation,
23
+ NodeStepInputs,
24
+ NodeStepSettings,
25
+ NodeStepStats,
26
+ )
19
27
  from flowfile_core.flowfile.flow_node.schema_callback import SingleExecutionFuture
28
+ from flowfile_core.flowfile.setting_generator import setting_generator, setting_updator
29
+ from flowfile_core.flowfile.utils import get_hash
30
+ from flowfile_core.schemas import input_schema, schemas
31
+ from flowfile_core.schemas.output_model import FileColumn, NodeData, TableExample
32
+ from flowfile_core.utils.arrow_reader import get_read_top_n
20
33
 
21
34
 
22
35
  class FlowNode:
@@ -25,6 +38,7 @@ class FlowNode:
25
38
  This class manages the node's state, its data processing function,
26
39
  and its connections to other nodes within the graph.
27
40
  """
41
+
28
42
  parent_uuid: str
29
43
  node_type: str
30
44
  node_template: node_store.NodeTemplate
@@ -34,31 +48,38 @@ class FlowNode:
34
48
  node_stats: NodeStepStats
35
49
  node_settings: NodeStepSettings
36
50
  results: NodeResults
37
- node_information: Optional[schemas.NodeInformation] = None
38
- leads_to_nodes: List["FlowNode"] = [] # list with target flows, after execution the step will trigger those step(s)
39
- user_provided_schema_callback: Optional[Callable] = None # user provided callback function for schema calculation
51
+ node_information: schemas.NodeInformation | None = None
52
+ leads_to_nodes: list["FlowNode"] = [] # list with target flows, after execution the step will trigger those step(s)
53
+ user_provided_schema_callback: Callable | None = None # user provided callback function for schema calculation
40
54
  _setting_input: Any = None
41
- _hash: Optional[str] = None # host this for caching results
55
+ _hash: str | None = None # host this for caching results
42
56
  _function: Callable = None # the function that needs to be executed when triggered
43
57
  _name: str = None # name of the node, used for display
44
- _schema_callback: Optional[SingleExecutionFuture] = None # Function that calculates the schema without executing
58
+ _schema_callback: SingleExecutionFuture | None = None # Function that calculates the schema without executing
45
59
  _state_needs_reset: bool = False
46
- _fetch_cached_df: Optional[ExternalDfFetcher | ExternalDatabaseFetcher | ExternalDatabaseWriter | ExternalCloudWriter] = None
47
- _cache_progress: Optional[ExternalDfFetcher | ExternalDatabaseFetcher | ExternalDatabaseWriter | ExternalCloudWriter] = None
48
-
49
- def __init__(self, node_id: Union[str, int], function: Callable,
50
- parent_uuid: str,
51
- setting_input: Any,
52
- name: str,
53
- node_type: str,
54
- input_columns: List[str] = None,
55
- output_schema: List[FlowfileColumn] = None,
56
- drop_columns: List[str] = None,
57
- renew_schema: bool = True,
58
- pos_x: float = 0,
59
- pos_y: float = 0,
60
- schema_callback: Callable = None,
61
- ):
60
+ _fetch_cached_df: (
61
+ ExternalDfFetcher | ExternalDatabaseFetcher | ExternalDatabaseWriter | ExternalCloudWriter | None
62
+ ) = None
63
+ _cache_progress: (
64
+ ExternalDfFetcher | ExternalDatabaseFetcher | ExternalDatabaseWriter | ExternalCloudWriter | None
65
+ ) = None
66
+
67
+ def __init__(
68
+ self,
69
+ node_id: str | int,
70
+ function: Callable,
71
+ parent_uuid: str,
72
+ setting_input: Any,
73
+ name: str,
74
+ node_type: str,
75
+ input_columns: list[str] = None,
76
+ output_schema: list[FlowfileColumn] = None,
77
+ drop_columns: list[str] = None,
78
+ renew_schema: bool = True,
79
+ pos_x: float = 0,
80
+ pos_y: float = 0,
81
+ schema_callback: Callable = None,
82
+ ):
62
83
  """Initializes a FlowNode instance.
63
84
 
64
85
  Args:
@@ -83,16 +104,17 @@ class FlowNode:
83
104
  self.node_information.id = node_id
84
105
  self.node_type = node_type
85
106
  self.node_settings.renew_schema = renew_schema
86
- self.update_node(function=function,
87
- input_columns=input_columns,
88
- output_schema=output_schema,
89
- drop_columns=drop_columns,
90
- setting_input=setting_input,
91
- name=name,
92
- pos_x=pos_x,
93
- pos_y=pos_y,
94
- schema_callback=schema_callback,
95
- )
107
+ self.update_node(
108
+ function=function,
109
+ input_columns=input_columns,
110
+ output_schema=output_schema,
111
+ drop_columns=drop_columns,
112
+ setting_input=setting_input,
113
+ name=name,
114
+ pos_x=pos_x,
115
+ pos_y=pos_y,
116
+ schema_callback=schema_callback,
117
+ )
96
118
 
97
119
  def post_init(self):
98
120
  """Initializes or resets the node's attributes to their default states."""
@@ -107,6 +129,7 @@ class FlowNode:
107
129
  self._cache_progress = None
108
130
  self._schema_callback = None
109
131
  self._state_needs_reset = False
132
+ self._execution_lock = threading.RLock() # Protects concurrent access to get_resulting_data
110
133
 
111
134
  @property
112
135
  def state_needs_reset(self) -> bool:
@@ -126,23 +149,27 @@ class FlowNode:
126
149
  """
127
150
  self._state_needs_reset = v
128
151
 
129
- @staticmethod
130
- def create_schema_callback_from_function(f: Callable) -> Callable[[], List[FlowfileColumn]]:
152
+ def create_schema_callback_from_function(self, f: Callable) -> Callable[[], list[FlowfileColumn]]:
131
153
  """Wraps a node's function to create a schema callback that extracts the schema.
132
154
 
155
+ Thread-safe: uses _execution_lock to prevent concurrent execution with get_resulting_data.
156
+
133
157
  Args:
134
158
  f: The node's core function that returns a FlowDataEngine instance.
135
159
 
136
160
  Returns:
137
161
  A callable that, when executed, returns the output schema.
138
162
  """
139
- def schema_callback() -> List[FlowfileColumn]:
163
+
164
+ def schema_callback() -> list[FlowfileColumn]:
140
165
  try:
141
- logger.info('Executing the schema callback function based on the node function')
142
- return f().schema
166
+ logger.info("Executing the schema callback function based on the node function")
167
+ with self._execution_lock:
168
+ return f().schema
143
169
  except Exception as e:
144
- logger.warning(f'Error with the schema callback: {e}')
170
+ logger.warning(f"Error with the schema callback: {e}")
145
171
  return []
172
+
146
173
  return schema_callback
147
174
 
148
175
  @property
@@ -171,7 +198,7 @@ class FlowNode:
171
198
  if f is None:
172
199
  return
173
200
 
174
- def error_callback(e: Exception) -> List:
201
+ def error_callback(e: Exception) -> list:
175
202
  logger.warning(e)
176
203
 
177
204
  self.node_settings.setup_errors = True
@@ -190,7 +217,7 @@ class FlowNode:
190
217
  """
191
218
  return not self.has_input and self.node_template.input == 0
192
219
 
193
- def get_input_type(self, node_id: int) -> List:
220
+ def get_input_type(self, node_id: int) -> list:
194
221
  """Gets the type of connection ('main', 'left', 'right') for a given input node ID.
195
222
 
196
223
  Args:
@@ -201,24 +228,25 @@ class FlowNode:
201
228
  """
202
229
  relation_type = []
203
230
  if node_id in [n.node_id for n in self.node_inputs.main_inputs]:
204
- relation_type.append('main')
231
+ relation_type.append("main")
205
232
  if self.node_inputs.left_input is not None and node_id == self.node_inputs.left_input.node_id:
206
- relation_type.append('left')
233
+ relation_type.append("left")
207
234
  if self.node_inputs.right_input is not None and node_id == self.node_inputs.right_input.node_id:
208
- relation_type.append('right')
235
+ relation_type.append("right")
209
236
  return list(set(relation_type))
210
237
 
211
- def update_node(self,
212
- function: Callable,
213
- input_columns: List[str] = None,
214
- output_schema: List[FlowfileColumn] = None,
215
- drop_columns: List[str] = None,
216
- name: str = None,
217
- setting_input: Any = None,
218
- pos_x: float = 0,
219
- pos_y: float = 0,
220
- schema_callback: Callable = None,
221
- ):
238
+ def update_node(
239
+ self,
240
+ function: Callable,
241
+ input_columns: list[str] = None,
242
+ output_schema: list[FlowfileColumn] = None,
243
+ drop_columns: list[str] = None,
244
+ name: str = None,
245
+ setting_input: Any = None,
246
+ pos_x: float = 0,
247
+ pos_y: float = 0,
248
+ schema_callback: Callable = None,
249
+ ):
222
250
  """Updates the properties of the node.
223
251
 
224
252
  This is called during initialization and when settings are changed.
@@ -245,7 +273,7 @@ class FlowNode:
245
273
  self.node_schema.output_columns = [] if output_schema is None else output_schema
246
274
  self.node_schema.drop_columns = [] if drop_columns is None else drop_columns
247
275
  self.node_settings.renew_schema = True
248
- if hasattr(setting_input, 'cache_results'):
276
+ if hasattr(setting_input, "cache_results"):
249
277
  self.node_settings.cache_results = setting_input.cache_results
250
278
 
251
279
  self.results.errors = None
@@ -253,7 +281,7 @@ class FlowNode:
253
281
  _ = self.hash
254
282
  self.node_template = node_store.node_dict.get(self.node_type)
255
283
  if self.node_template is None:
256
- raise Exception(f'Node template {self.node_type} not found')
284
+ raise Exception(f"Node template {self.node_type} not found")
257
285
  self.node_default = node_store.node_defaults.get(self.node_type)
258
286
  self.setting_input = setting_input # wait until the end so that the hash is calculated correctly
259
287
 
@@ -292,10 +320,11 @@ class FlowNode:
292
320
  Args:
293
321
  setting_input: The new settings object.
294
322
  """
295
- is_manual_input = (self.node_type == 'manual_input' and
296
- isinstance(setting_input, input_schema.NodeManualInput) and
297
- isinstance(self._setting_input, input_schema.NodeManualInput)
298
- )
323
+ is_manual_input = (
324
+ self.node_type == "manual_input"
325
+ and isinstance(setting_input, input_schema.NodeManualInput)
326
+ and isinstance(self._setting_input, input_schema.NodeManualInput)
327
+ )
299
328
  if is_manual_input:
300
329
  _ = self.hash
301
330
  self._setting_input = setting_input
@@ -309,7 +338,7 @@ class FlowNode:
309
338
  self.reset()
310
339
 
311
340
  @property
312
- def node_id(self) -> Union[str, int]:
341
+ def node_id(self) -> str | int:
313
342
  """Gets the unique identifier of the node.
314
343
 
315
344
  Returns:
@@ -336,7 +365,7 @@ class FlowNode:
336
365
  return self.node_inputs.right_input
337
366
 
338
367
  @property
339
- def main_input(self) -> List["FlowNode"]:
368
+ def main_input(self) -> list["FlowNode"]:
340
369
  """Gets the list of nodes connected to the main input port(s).
341
370
 
342
371
  Returns:
@@ -353,24 +382,29 @@ class FlowNode:
353
382
  """
354
383
  if isinstance(self.setting_input, input_schema.NodePromise):
355
384
  return False
356
- return (self.node_template.input == len(self.node_inputs.get_all_inputs()) or
357
- (self.node_template.multi and len(self.node_inputs.get_all_inputs()) > 0) or
358
- (self.node_template.multi and self.node_template.can_be_start))
385
+ return (
386
+ self.node_template.input == len(self.node_inputs.get_all_inputs())
387
+ or (self.node_template.multi and len(self.node_inputs.get_all_inputs()) > 0)
388
+ or (self.node_template.multi and self.node_template.can_be_start)
389
+ )
359
390
 
360
391
  def set_node_information(self):
361
392
  """Populates the `node_information` attribute with the current state.
362
393
 
363
394
  This includes the node's connections, settings, and position.
364
395
  """
365
- logger.info('setting node information')
396
+ logger.info("setting node information")
366
397
  node_information = self.node_information
367
398
  node_information.left_input_id = self.node_inputs.left_input.node_id if self.left_input else None
368
399
  node_information.right_input_id = self.node_inputs.right_input.node_id if self.right_input else None
369
- node_information.input_ids = [mi.node_id for mi in
370
- self.node_inputs.main_inputs] if self.node_inputs.main_inputs is not None else None
400
+ node_information.input_ids = (
401
+ [mi.node_id for mi in self.node_inputs.main_inputs] if self.node_inputs.main_inputs is not None else None
402
+ )
371
403
  node_information.setting_input = self.setting_input
372
404
  node_information.outputs = [n.node_id for n in self.leads_to_nodes]
373
- node_information.description = self.setting_input.description if hasattr(self.setting_input, 'description') else ''
405
+ node_information.description = (
406
+ self.setting_input.description if hasattr(self.setting_input, "description") else ""
407
+ )
374
408
  node_information.is_setup = self.is_setup
375
409
  node_information.x_position = self.setting_input.pos_x
376
410
  node_information.y_position = self.setting_input.pos_y
@@ -404,7 +438,7 @@ class FlowNode:
404
438
  self._function = function
405
439
 
406
440
  @property
407
- def all_inputs(self) -> List["FlowNode"]:
441
+ def all_inputs(self) -> list["FlowNode"]:
408
442
  """Gets a list of all nodes connected to any input port.
409
443
 
410
444
  Returns:
@@ -436,8 +470,9 @@ class FlowNode:
436
470
  self._hash = self.calculate_hash(self.setting_input)
437
471
  return self._hash
438
472
 
439
- def add_node_connection(self, from_node: "FlowNode",
440
- insert_type: Literal['main', 'left', 'right'] = 'main') -> None:
473
+ def add_node_connection(
474
+ self, from_node: "FlowNode", insert_type: Literal["main", "left", "right"] = "main"
475
+ ) -> None:
441
476
  """Adds a connection from a source node to this node.
442
477
 
443
478
  Args:
@@ -448,19 +483,19 @@ class FlowNode:
448
483
  Exception: If the insert_type is invalid.
449
484
  """
450
485
  from_node.leads_to_nodes.append(self)
451
- if insert_type == 'main':
486
+ if insert_type == "main":
452
487
  if self.node_template.input <= 2 or self.node_inputs.main_inputs is None:
453
488
  self.node_inputs.main_inputs = [from_node]
454
489
  else:
455
490
  self.node_inputs.main_inputs.append(from_node)
456
- elif insert_type == 'right':
491
+ elif insert_type == "right":
457
492
  self.node_inputs.right_input = from_node
458
- elif insert_type == 'left':
493
+ elif insert_type == "left":
459
494
  self.node_inputs.left_input = from_node
460
495
  else:
461
- raise Exception('Cannot find the connection')
496
+ raise Exception("Cannot find the connection")
462
497
  if self.setting_input.is_setup:
463
- if hasattr(self.setting_input, 'depending_on_id') and insert_type == 'main':
498
+ if hasattr(self.setting_input, "depending_on_id") and insert_type == "main":
464
499
  self.setting_input.depending_on_id = from_node.node_id
465
500
  self.reset()
466
501
  from_node.reset()
@@ -472,7 +507,7 @@ class FlowNode:
472
507
  deep: If True, the reset propagates recursively through the entire downstream graph.
473
508
  """
474
509
  for node in self.leads_to_nodes:
475
- self.print(f'resetting node: {node.node_id}')
510
+ self.print(f"resetting node: {node.node_id}")
476
511
  node.reset(deep)
477
512
 
478
513
  def get_flow_file_column_schema(self, col_name: str) -> FlowfileColumn | None:
@@ -488,7 +523,7 @@ class FlowNode:
488
523
  if s.column_name == col_name:
489
524
  return s
490
525
 
491
- def get_predicted_schema(self, force: bool = False) -> List[FlowfileColumn] | None:
526
+ def get_predicted_schema(self, force: bool = False) -> list[FlowfileColumn] | None:
492
527
  """Predicts the output schema of the node without full execution.
493
528
 
494
529
  It uses the schema_callback or infers from predicted data.
@@ -503,18 +538,18 @@ class FlowNode:
503
538
  if self.node_schema.predicted_schema and not force:
504
539
  return self.node_schema.predicted_schema
505
540
  if self.schema_callback is not None and (self.node_schema.predicted_schema is None or force):
506
- self.print('Getting the data from a schema callback')
541
+ self.print("Getting the data from a schema callback")
507
542
  if force:
508
543
  # Force the schema callback to reset, so that it will be executed again
509
544
  self.schema_callback.reset()
510
545
  schema = self.schema_callback()
511
546
  if schema is not None and len(schema) > 0:
512
- self.print('Calculating the schema based on the schema callback')
547
+ self.print("Calculating the schema based on the schema callback")
513
548
  self.node_schema.predicted_schema = schema
514
549
  return self.node_schema.predicted_schema
515
550
  predicted_data = self._predicted_data_getter()
516
551
  if predicted_data is not None and predicted_data.schema is not None:
517
- self.print('Calculating the schema based on the predicted resulting data')
552
+ self.print("Calculating the schema based on the predicted resulting data")
518
553
  self.node_schema.predicted_schema = self._predicted_data_getter().schema
519
554
 
520
555
  return self.node_schema.predicted_schema
@@ -527,7 +562,7 @@ class FlowNode:
527
562
  True if the node is set up, False otherwise.
528
563
  """
529
564
  if not self.node_information.is_setup:
530
- if self.function.__name__ != 'placeholder':
565
+ if self.function.__name__ != "placeholder":
531
566
  self.node_information.is_setup = True
532
567
  self.setting_input.is_setup = True
533
568
  return self.node_information.is_setup
@@ -538,12 +573,13 @@ class FlowNode:
538
573
  Args:
539
574
  v: The message or value to log.
540
575
  """
541
- logger.info(f'{self.node_type}, node_id: {self.node_id}: {v}')
576
+ logger.info(f"{self.node_type}, node_id: {self.node_id}: {v}")
542
577
 
543
578
  def get_resulting_data(self) -> FlowDataEngine | None:
544
579
  """Executes the node's function to produce the actual output data.
545
580
 
546
581
  Handles both regular functions and external data sources.
582
+ Thread-safe: uses _execution_lock to prevent concurrent execution.
547
583
 
548
584
  Returns:
549
585
  A FlowDataEngine instance containing the result, or None on error.
@@ -552,30 +588,40 @@ class FlowNode:
552
588
  Exception: Propagates exceptions from the node's function execution.
553
589
  """
554
590
  if self.is_setup:
555
- if self.results.resulting_data is None and self.results.errors is None:
556
- self.print('getting resulting data')
557
- try:
558
- if isinstance(self.function, FlowDataEngine):
559
- fl: FlowDataEngine = self.function
560
- elif self.node_type == 'external_source':
561
- fl: FlowDataEngine = self.function()
562
- fl.collect_external()
563
- self.node_settings.streamable = False
564
- else:
565
- try:
566
- fl = self._function(*[v.get_resulting_data() for v in self.all_inputs])
567
- except Exception as e:
568
- raise e
569
- fl.set_streamable(self.node_settings.streamable)
570
- self.results.resulting_data = fl
571
- self.node_schema.result_schema = fl.schema
572
- except Exception as e:
573
- self.results.resulting_data = FlowDataEngine()
574
- self.results.errors = str(e)
575
- self.node_stats.has_run_with_current_setup = False
576
- self.node_stats.has_completed_last_run = False
577
- raise e
578
- return self.results.resulting_data
591
+ with self._execution_lock:
592
+ if self.results.resulting_data is None and self.results.errors is None:
593
+ self.print("getting resulting data")
594
+ try:
595
+ if isinstance(self.function, FlowDataEngine):
596
+ fl: FlowDataEngine = self.function
597
+ elif self.node_type == "external_source":
598
+ fl: FlowDataEngine = self.function()
599
+ fl.collect_external()
600
+ self.node_settings.streamable = False
601
+ else:
602
+ try:
603
+ self.print("Collecting input data from all inputs")
604
+ input_data = []
605
+ for i, v in enumerate(self.all_inputs):
606
+ self.print(f"Getting resulting data from input {i} (node {v.node_id})")
607
+ input_result = v.get_resulting_data()
608
+ self.print(f"Input {i} data type: {type(input_result)}, dataframe type: {type(input_result.data_frame) if input_result else 'None'}")
609
+ input_data.append(input_result)
610
+ self.print(f"All {len(input_data)} inputs collected, calling node function")
611
+ fl = self._function(*input_data)
612
+ self.print(f"Node function returned, result type: {type(fl)}")
613
+ except Exception as e:
614
+ raise e
615
+ fl.set_streamable(self.node_settings.streamable)
616
+ self.results.resulting_data = fl
617
+ self.node_schema.result_schema = fl.schema
618
+ except Exception as e:
619
+ self.results.resulting_data = FlowDataEngine()
620
+ self.results.errors = str(e)
621
+ self.node_stats.has_run_with_current_setup = False
622
+ self.node_stats.has_completed_last_run = False
623
+ raise e
624
+ return self.results.resulting_data
579
625
 
580
626
  def _predicted_data_getter(self) -> FlowDataEngine | None:
581
627
  """Internal helper to get a predicted data result.
@@ -590,14 +636,14 @@ class FlowNode:
590
636
  return fl
591
637
  except ValueError as e:
592
638
  if str(e) == "generator already executing":
593
- logger.info('Generator already executing, waiting for the result')
639
+ logger.info("Generator already executing, waiting for the result")
594
640
  sleep(1)
595
641
  return self._predicted_data_getter()
596
642
  fl = FlowDataEngine()
597
643
  return fl
598
644
 
599
645
  except Exception as e:
600
- logger.warning('there was an issue with the function, returning an empty Flowfile')
646
+ logger.warning("there was an issue with the function, returning an empty Flowfile")
601
647
  logger.warning(e)
602
648
 
603
649
  def get_predicted_resulting_data(self) -> FlowDataEngine:
@@ -609,7 +655,7 @@ class FlowNode:
609
655
  A FlowDataEngine instance with a schema but no data.
610
656
  """
611
657
  if self.needs_run(False) and self.schema_callback is not None or self.node_schema.result_schema is not None:
612
- self.print('Getting data based on the schema')
658
+ self.print("Getting data based on the schema")
613
659
 
614
660
  _s = self.schema_callback() if self.node_schema.result_schema is None else self.node_schema.result_schema
615
661
  return FlowDataEngine.create_from_schema(_s)
@@ -649,7 +695,7 @@ class FlowNode:
649
695
  yield n
650
696
 
651
697
  @property
652
- def schema(self) -> List[FlowfileColumn]:
698
+ def schema(self) -> list[FlowfileColumn]:
653
699
  """Gets the definitive output schema of the node.
654
700
 
655
701
  If not already run, it falls back to the predicted schema.
@@ -661,7 +707,7 @@ class FlowNode:
661
707
  if self.is_setup and self.results.errors is None:
662
708
  if self.node_schema.result_schema is not None and len(self.node_schema.result_schema) > 0:
663
709
  return self.node_schema.result_schema
664
- elif self.node_type == 'output':
710
+ elif self.node_type == "output":
665
711
  if len(self.node_inputs.main_inputs) > 0:
666
712
  self.node_schema.result_schema = self.node_inputs.main_inputs[0].schema
667
713
  else:
@@ -680,11 +726,15 @@ class FlowNode:
680
726
  """
681
727
 
682
728
  if results_exists(self.hash):
683
- logger.warning('Not implemented')
729
+ logger.warning("Not implemented")
684
730
  clear_task_from_worker(self.hash)
685
731
 
686
- def needs_run(self, performance_mode: bool, node_logger: NodeLogger = None,
687
- execution_location: schemas.ExecutionLocationsLiteral = "remote") -> bool:
732
+ def needs_run(
733
+ self,
734
+ performance_mode: bool,
735
+ node_logger: NodeLogger = None,
736
+ execution_location: schemas.ExecutionLocationsLiteral = "remote",
737
+ ) -> bool:
688
738
  """Determines if the node needs to be executed.
689
739
 
690
740
  The decision is based on its run state, caching settings, and execution mode.
@@ -703,7 +753,7 @@ class FlowNode:
703
753
  flow_logger = logger if node_logger is None else node_logger
704
754
  cache_result_exists = results_exists(self.hash)
705
755
  if not self.node_stats.has_run_with_current_setup:
706
- flow_logger.info('Node has not run, needs to run')
756
+ flow_logger.info("Node has not run, needs to run")
707
757
  return True
708
758
  if self.node_settings.cache_results and cache_result_exists:
709
759
  return False
@@ -737,7 +787,9 @@ class FlowNode:
737
787
  if example_data is None:
738
788
  example_data = resulting_data.get_sample(100).to_arrow()
739
789
  return example_data
790
+
740
791
  return get_example_data
792
+
741
793
  resulting_data = self.get_resulting_data()
742
794
 
743
795
  if not performance_mode:
@@ -759,8 +811,13 @@ class FlowNode:
759
811
  try:
760
812
  resulting_data = self.get_resulting_data()
761
813
  if not performance_mode:
762
- external_sampler = ExternalSampler(lf=resulting_data.data_frame, file_ref=self.hash,
763
- wait_on_completion=True, node_id=self.node_id, flow_id=flow_id)
814
+ external_sampler = ExternalSampler(
815
+ lf=resulting_data.data_frame,
816
+ file_ref=self.hash,
817
+ wait_on_completion=True,
818
+ node_id=self.node_id,
819
+ flow_id=flow_id,
820
+ )
764
821
  self.store_example_data_generator(external_sampler)
765
822
  if self.results.errors is None and not self.node_stats.is_canceled:
766
823
  self.node_stats.has_run_with_current_setup = True
@@ -790,48 +847,67 @@ class FlowNode:
790
847
  Exception: If the node_logger is not provided or if execution fails.
791
848
  """
792
849
  if node_logger is None:
793
- raise Exception('Node logger is not defined')
850
+ raise Exception("Node logger is not defined")
794
851
  if self.node_settings.cache_results and results_exists(self.hash):
795
852
  try:
796
853
  self.results.resulting_data = get_external_df_result(self.hash)
797
854
  self._cache_progress = None
798
855
  return
799
- except Exception as e:
800
- node_logger.warning('Failed to read the cache, rerunning the code')
801
- if self.node_type == 'output':
856
+ except Exception:
857
+ node_logger.warning("Failed to read the cache, rerunning the code")
858
+ if self.node_type == "output":
802
859
  self.results.resulting_data = self.get_resulting_data()
803
860
  self.node_stats.has_run_with_current_setup = True
804
861
  return
862
+
805
863
  try:
806
- self.get_resulting_data()
864
+ result_data = self.get_resulting_data()
865
+ # Use 'is not None' instead of truthiness check to avoid triggering __len__()
866
+ # which calls .collect() on the LazyFrame and can cause issues
867
+ if result_data is None:
868
+ self.results.errors = "Error with creating the lazy frame, most likely due to invalid graph"
869
+ raise Exception("get_resulting_data returned None")
807
870
  except Exception as e:
808
- self.results.errors = 'Error with creating the lazy frame, most likely due to invalid graph'
871
+ self.results.errors = "Error with creating the lazy frame, most likely due to invalid graph"
809
872
  raise e
873
+
810
874
  if not performance_mode:
811
- external_df_fetcher = ExternalDfFetcher(lf=self.get_resulting_data().data_frame,
812
- file_ref=self.hash, wait_on_completion=False,
813
- flow_id=node_logger.flow_id,
814
- node_id=self.node_id)
875
+ external_df_fetcher = ExternalDfFetcher(
876
+ lf=self.get_resulting_data().data_frame,
877
+ file_ref=self.hash,
878
+ wait_on_completion=False,
879
+ flow_id=node_logger.flow_id,
880
+ node_id=self.node_id,
881
+ )
815
882
  self._fetch_cached_df = external_df_fetcher
883
+
816
884
  try:
817
885
  lf = external_df_fetcher.get_result()
818
886
  self.results.resulting_data = FlowDataEngine(
819
- lf, number_of_records=ExternalDfFetcher(lf=lf, operation_type='calculate_number_of_records',
820
- flow_id=node_logger.flow_id, node_id=self.node_id).result
887
+ lf,
888
+ number_of_records=ExternalDfFetcher(
889
+ lf=lf,
890
+ operation_type="calculate_number_of_records",
891
+ flow_id=node_logger.flow_id,
892
+ node_id=self.node_id,
893
+ ).result,
821
894
  )
895
+
822
896
  if not performance_mode:
823
897
  self.store_example_data_generator(external_df_fetcher)
824
898
  self.node_stats.has_run_with_current_setup = True
825
899
 
826
900
  except Exception as e:
827
- node_logger.error('Error with external process')
901
+ node_logger.error("Error with external process")
828
902
  if external_df_fetcher.error_code == -1:
829
903
  try:
830
904
  self.results.resulting_data = self.get_resulting_data()
831
- self.results.warnings = ('Error with external process (unknown error), '
832
- 'likely the process was killed by the server because of memory constraints, '
833
- 'continue with the process. '
834
- 'We cannot display example data...')
905
+ self.results.warnings = (
906
+ "Error with external process (unknown error), "
907
+ "likely the process was killed by the server because of memory constraints, "
908
+ "continue with the process. "
909
+ "We cannot display example data..."
910
+ )
835
911
  except Exception as e:
836
912
  self.results.errors = str(e)
837
913
  raise e
@@ -858,15 +934,18 @@ class FlowNode:
858
934
  self._fetch_cached_df.cancel()
859
935
  self.node_stats.is_canceled = True
860
936
  else:
861
- logger.warning('No external process to cancel')
937
+ logger.warning("No external process to cancel")
862
938
  self.node_stats.is_canceled = True
863
939
 
864
- def execute_node(self, run_location: schemas.ExecutionLocationsLiteral,
865
- reset_cache: bool = False,
866
- performance_mode: bool = False,
867
- retry: bool = True,
868
- node_logger: NodeLogger = None,
869
- optimize_for_downstream: bool = True):
940
+ def execute_node(
941
+ self,
942
+ run_location: schemas.ExecutionLocationsLiteral,
943
+ reset_cache: bool = False,
944
+ performance_mode: bool = False,
945
+ retry: bool = True,
946
+ node_logger: NodeLogger = None,
947
+ optimize_for_downstream: bool = True,
948
+ ):
870
949
  """Orchestrates the execution, handling location, caching, and retries.
871
950
 
872
951
  Args:
@@ -882,7 +961,7 @@ class FlowNode:
882
961
  Exception: If the node_logger is not defined.
883
962
  """
884
963
  if node_logger is None:
885
- raise Exception('Flow logger is not defined')
964
+ raise Exception("Flow logger is not defined")
886
965
  # TODO: Simplify which route is being picked there are many duplicate checks
887
966
 
888
967
  if reset_cache:
@@ -891,63 +970,78 @@ class FlowNode:
891
970
  self.node_stats.has_completed_last_run = False
892
971
 
893
972
  if self.is_setup:
894
- node_logger.info(f'Starting to run {self.__name__}')
895
- if (self.needs_run(performance_mode, node_logger, run_location) or self.node_template.node_group == "output"
896
- and not (run_location == 'local')):
973
+ node_logger.info(f"Starting to run {self.__name__}")
974
+ if (
975
+ self.needs_run(performance_mode, node_logger, run_location)
976
+ or self.node_template.node_group == "output"
977
+ and not (run_location == "local")
978
+ ):
897
979
  self.clear_table_example()
898
980
  self.prepare_before_run()
899
981
  self.reset()
900
982
  try:
901
- if (((run_location == 'remote' or
902
- (self.node_default.transform_type == 'wide' and optimize_for_downstream) and
903
- not run_location == 'local'))
904
- or self.node_settings.cache_results):
905
- node_logger.info('Running the node remotely')
983
+ if (
984
+ run_location == "remote"
985
+ or (self.node_default.transform_type == "wide" and optimize_for_downstream)
986
+ and not run_location == "local"
987
+ ) or self.node_settings.cache_results:
988
+ node_logger.info("Running the node remotely")
906
989
  if self.node_settings.cache_results:
907
990
  performance_mode = False
908
- self.execute_remote(performance_mode=(performance_mode if not self.node_settings.cache_results
909
- else False),
910
- node_logger=node_logger
911
- )
991
+ self.execute_remote(
992
+ performance_mode=(performance_mode if not self.node_settings.cache_results else False),
993
+ node_logger=node_logger,
994
+ )
912
995
  else:
913
- node_logger.info('Running the node locally')
996
+ node_logger.info("Running the node locally")
914
997
  self.execute_local(performance_mode=performance_mode, flow_id=node_logger.flow_id)
915
998
  except Exception as e:
916
- if 'No such file or directory (os error' in str(e) and retry:
917
- logger.warning('Error with the input node, starting to rerun the input node...')
918
- all_inputs: List[FlowNode] = self.node_inputs.get_all_inputs()
999
+ if "No such file or directory (os error" in str(e) and retry:
1000
+ logger.warning("Error with the input node, starting to rerun the input node...")
1001
+ all_inputs: list[FlowNode] = self.node_inputs.get_all_inputs()
919
1002
  for node_input in all_inputs:
920
- node_input.execute_node(run_location=run_location,
921
- performance_mode=performance_mode, retry=True,
922
- reset_cache=True,
923
- node_logger=node_logger)
924
- self.execute_node(run_location=run_location,
925
- performance_mode=performance_mode, retry=False,
926
- node_logger=node_logger)
1003
+ node_input.execute_node(
1004
+ run_location=run_location,
1005
+ performance_mode=performance_mode,
1006
+ retry=True,
1007
+ reset_cache=True,
1008
+ node_logger=node_logger,
1009
+ )
1010
+ self.execute_node(
1011
+ run_location=run_location,
1012
+ performance_mode=performance_mode,
1013
+ retry=False,
1014
+ node_logger=node_logger,
1015
+ )
927
1016
  else:
928
1017
  self.results.errors = str(e)
929
1018
  if "Connection refused" in str(e) and "/submit_query/" in str(e):
930
- node_logger.warning("There was an issue connecting to the remote worker, "
931
- "ensure the worker process is running, "
932
- "or change the settings to, so it executes locally")
933
- node_logger.error("Could not execute in the remote worker. (Re)start the worker service, or change settings to local settings.")
1019
+ node_logger.warning(
1020
+ "There was an issue connecting to the remote worker, "
1021
+ "ensure the worker process is running, "
1022
+ "or change the settings to, so it executes locally"
1023
+ )
1024
+ node_logger.error(
1025
+ "Could not execute in the remote worker. (Re)start the worker service, or change settings to local settings."
1026
+ )
934
1027
  else:
935
- node_logger.error(f'Error with running the node: {e}')
936
- elif ((run_location == 'local') and
937
- (not self.node_stats.has_run_with_current_setup or self.node_template.node_group == "output")):
1028
+ node_logger.error(f"Error with running the node: {e}")
1029
+ elif (run_location == "local") and (
1030
+ not self.node_stats.has_run_with_current_setup or self.node_template.node_group == "output"
1031
+ ):
938
1032
  try:
939
- node_logger.info('Executing fully locally')
1033
+ node_logger.info("Executing fully locally")
940
1034
  self.execute_full_local(performance_mode)
941
1035
  except Exception as e:
942
1036
  self.results.errors = str(e)
943
- node_logger.error(f'Error with running the node: {e}')
1037
+ node_logger.error(f"Error with running the node: {e}")
944
1038
  self.node_stats.error = str(e)
945
1039
  self.node_stats.has_completed_last_run = False
946
1040
 
947
1041
  else:
948
- node_logger.info('Node has already run, not running the node')
1042
+ node_logger.info("Node has already run, not running the node")
949
1043
  else:
950
- node_logger.warning(f'Node {self.__name__} is not setup, cannot run the node')
1044
+ node_logger.warning(f"Node {self.__name__} is not setup, cannot run the node")
951
1045
 
952
1046
  def store_example_data_generator(self, external_df_fetcher: ExternalDfFetcher | ExternalSampler):
953
1047
  """Stores a generator function for fetching a sample of the result data.
@@ -960,7 +1054,7 @@ class FlowNode:
960
1054
  self.results.example_data_path = file_ref
961
1055
  self.results.example_data_generator = get_read_top_n(file_path=file_ref, n=100)
962
1056
  else:
963
- logger.error('Could not get the sample data, the external process is not ready')
1057
+ logger.error("Could not get the sample data, the external process is not ready")
964
1058
 
965
1059
  def needs_reset(self) -> bool:
966
1060
  """Checks if the node's hash has changed, indicating an outdated state.
@@ -980,7 +1074,7 @@ class FlowNode:
980
1074
  """
981
1075
  needs_reset = self.needs_reset() or deep
982
1076
  if needs_reset:
983
- logger.info(f'{self.node_id}: Node needs reset')
1077
+ logger.info(f"{self.node_id}: Node needs reset")
984
1078
  self.node_stats.has_run_with_current_setup = False
985
1079
  self.results.reset()
986
1080
  self.node_schema.result_schema = None
@@ -991,7 +1085,7 @@ class FlowNode:
991
1085
  if self.is_correct:
992
1086
  self._schema_callback = None # Ensure the schema callback is reset
993
1087
  if self.schema_callback:
994
- logger.info(f'{self.node_id}: Resetting the schema callback')
1088
+ logger.info(f"{self.node_id}: Resetting the schema callback")
995
1089
  self.schema_callback.start()
996
1090
  self.evaluate_nodes()
997
1091
  _ = self.hash # Recalculate the hash after reset
@@ -1005,17 +1099,18 @@ class FlowNode:
1005
1099
  Returns:
1006
1100
  True if the connection was found and removed, False otherwise.
1007
1101
  """
1008
- logger.info(f'Deleting lead to node: {node_id}')
1102
+ logger.info(f"Deleting lead to node: {node_id}")
1009
1103
  for i, lead_to_node in enumerate(self.leads_to_nodes):
1010
- logger.info(f'Checking lead to node: {lead_to_node.node_id}')
1104
+ logger.info(f"Checking lead to node: {lead_to_node.node_id}")
1011
1105
  if lead_to_node.node_id == node_id:
1012
- logger.info(f'Found the node to delete: {node_id}')
1106
+ logger.info(f"Found the node to delete: {node_id}")
1013
1107
  self.leads_to_nodes.pop(i)
1014
1108
  return True
1015
1109
  return False
1016
1110
 
1017
- def delete_input_node(self, node_id: int, connection_type: input_schema.InputConnectionClass = 'input-0',
1018
- complete: bool = False) -> bool:
1111
+ def delete_input_node(
1112
+ self, node_id: int, connection_type: input_schema.InputConnectionClass = "input-0", complete: bool = False
1113
+ ) -> bool:
1019
1114
  """Removes a connection from a specific input node.
1020
1115
 
1021
1116
  Args:
@@ -1027,23 +1122,23 @@ class FlowNode:
1027
1122
  True if a connection was found and removed, False otherwise.
1028
1123
  """
1029
1124
  deleted: bool = False
1030
- if connection_type == 'input-0':
1125
+ if connection_type == "input-0":
1031
1126
  for i, node in enumerate(self.node_inputs.main_inputs):
1032
1127
  if node.node_id == node_id:
1033
1128
  self.node_inputs.main_inputs.pop(i)
1034
1129
  deleted = True
1035
1130
  if not complete:
1036
1131
  continue
1037
- elif connection_type == 'input-1' or complete:
1132
+ elif connection_type == "input-1" or complete:
1038
1133
  if self.node_inputs.right_input is not None and self.node_inputs.right_input.node_id == node_id:
1039
1134
  self.node_inputs.right_input = None
1040
1135
  deleted = True
1041
- elif connection_type == 'input-2' or complete:
1136
+ elif connection_type == "input-2" or complete:
1042
1137
  if self.node_inputs.left_input is not None and self.node_inputs.right_input.node_id == node_id:
1043
1138
  self.node_inputs.left_input = None
1044
1139
  deleted = True
1045
1140
  else:
1046
- logger.warning('Could not find the connection to delete...')
1141
+ logger.warning("Could not find the connection to delete...")
1047
1142
  if deleted:
1048
1143
  self.reset()
1049
1144
  return deleted
@@ -1056,7 +1151,7 @@ class FlowNode:
1056
1151
  """
1057
1152
  return f"Node id: {self.node_id} ({self.node_type})"
1058
1153
 
1059
- def _get_readable_schema(self) -> List[dict] | None:
1154
+ def _get_readable_schema(self) -> list[dict] | None:
1060
1155
  """Helper to get a simplified, dictionary representation of the output schema.
1061
1156
 
1062
1157
  Returns:
@@ -1074,11 +1169,14 @@ class FlowNode:
1074
1169
  Returns:
1075
1170
  A dictionary containing key information about the node.
1076
1171
  """
1077
- return dict(FlowNode=
1078
- dict(node_id=self.node_id,
1079
- step_name=self.__name__,
1080
- output_columns=self.node_schema.output_columns,
1081
- output_schema=self._get_readable_schema()))
1172
+ return dict(
1173
+ FlowNode=dict(
1174
+ node_id=self.node_id,
1175
+ step_name=self.__name__,
1176
+ output_columns=self.node_schema.output_columns,
1177
+ output_schema=self._get_readable_schema(),
1178
+ )
1179
+ )
1082
1180
 
1083
1181
  @property
1084
1182
  def number_of_leads_to_nodes(self) -> int | None:
@@ -1149,14 +1247,13 @@ class FlowNode:
1149
1247
  Returns:
1150
1248
  A `TableExample` object, or None if the node is not set up.
1151
1249
  """
1152
- self.print('Getting a table example')
1250
+ self.print("Getting a table example")
1153
1251
  if self.is_setup and include_data and self.node_stats.has_completed_last_run:
1154
-
1155
- if self.node_template.node_group == 'output':
1156
- self.print('getting the table example')
1252
+ if self.node_template.node_group == "output":
1253
+ self.print("getting the table example")
1157
1254
  return self.main_input[0].get_table_example(include_data)
1158
1255
 
1159
- logger.info('getting the table example since the node has run')
1256
+ logger.info("getting the table example since the node has run")
1160
1257
  example_data_getter = self.results.example_data_generator
1161
1258
  if example_data_getter is not None:
1162
1259
  data = example_data_getter().to_pylist()
@@ -1168,26 +1265,34 @@ class FlowNode:
1168
1265
  fl = self.get_resulting_data()
1169
1266
  has_example_data = self.results.example_data_generator is not None
1170
1267
 
1171
- return TableExample(node_id=self.node_id,
1172
- name=str(self.node_id), number_of_records=999,
1173
- number_of_columns=fl.number_of_fields,
1174
- table_schema=schema, columns=fl.columns, data=data,
1175
- has_example_data=has_example_data,
1176
- has_run_with_current_setup=self.node_stats.has_run_with_current_setup
1177
- )
1268
+ return TableExample(
1269
+ node_id=self.node_id,
1270
+ name=str(self.node_id),
1271
+ number_of_records=999,
1272
+ number_of_columns=fl.number_of_fields,
1273
+ table_schema=schema,
1274
+ columns=fl.columns,
1275
+ data=data,
1276
+ has_example_data=has_example_data,
1277
+ has_run_with_current_setup=self.node_stats.has_run_with_current_setup,
1278
+ )
1178
1279
  else:
1179
- logger.warning('getting the table example but the node has not run')
1280
+ logger.warning("getting the table example but the node has not run")
1180
1281
  try:
1181
1282
  schema = [FileColumn.model_validate(c.get_column_repr()) for c in self.schema]
1182
1283
  except Exception as e:
1183
1284
  logger.warning(e)
1184
1285
  schema = []
1185
1286
  columns = [s.name for s in schema]
1186
- return TableExample(node_id=self.node_id,
1187
- name=str(self.node_id), number_of_records=0,
1188
- number_of_columns=len(columns),
1189
- table_schema=schema, columns=columns,
1190
- data=[])
1287
+ return TableExample(
1288
+ node_id=self.node_id,
1289
+ name=str(self.node_id),
1290
+ number_of_records=0,
1291
+ number_of_columns=len(columns),
1292
+ table_schema=schema,
1293
+ columns=columns,
1294
+ data=[],
1295
+ )
1191
1296
 
1192
1297
  def get_node_data(self, flow_id: int, include_example: bool = False) -> NodeData:
1193
1298
  """Gathers all necessary data for representing the node in the UI.
@@ -1199,11 +1304,13 @@ class FlowNode:
1199
1304
  Returns:
1200
1305
  A `NodeData` object.
1201
1306
  """
1202
- node = NodeData(flow_id=flow_id,
1203
- node_id=self.node_id,
1204
- has_run=self.node_stats.has_run_with_current_setup,
1205
- setting_input=self.setting_input,
1206
- flow_type=self.node_type)
1307
+ node = NodeData(
1308
+ flow_id=flow_id,
1309
+ node_id=self.node_id,
1310
+ has_run=self.node_stats.has_run_with_current_setup,
1311
+ setting_input=self.setting_input,
1312
+ flow_type=self.node_type,
1313
+ )
1207
1314
  if self.main_input:
1208
1315
  node.main_input = self.main_input[0].get_table_example()
1209
1316
  if self.left_input:
@@ -1215,6 +1322,9 @@ class FlowNode:
1215
1322
  node = setting_generator.get_setting_generator(self.node_type)(node)
1216
1323
 
1217
1324
  node = setting_updator.get_setting_updator(self.node_type)(node)
1325
+ # Save the updated settings back to the node so they persist across calls
1326
+ if node.setting_input is not None and not isinstance(node.setting_input, input_schema.NodePromise):
1327
+ self.setting_input = node.setting_input
1218
1328
  return node
1219
1329
 
1220
1330
  def get_output_data(self) -> TableExample:
@@ -1231,12 +1341,14 @@ class FlowNode:
1231
1341
  Returns:
1232
1342
  A `NodeInput` object.
1233
1343
  """
1234
- return schemas.NodeInput(pos_y=self.setting_input.pos_y,
1235
- pos_x=self.setting_input.pos_x,
1236
- id=self.node_id,
1237
- **self.node_template.__dict__)
1238
-
1239
- def get_edge_input(self) -> List[schemas.NodeEdge]:
1344
+ return schemas.NodeInput(
1345
+ pos_y=self.setting_input.pos_y,
1346
+ pos_x=self.setting_input.pos_x,
1347
+ id=self.node_id,
1348
+ **self.node_template.__dict__,
1349
+ )
1350
+
1351
+ def get_edge_input(self) -> list[schemas.NodeEdge]:
1240
1352
  """Generates `NodeEdge` objects for all input connections to this node.
1241
1353
 
1242
1354
  Returns:
@@ -1245,24 +1357,33 @@ class FlowNode:
1245
1357
  edges = []
1246
1358
  if self.node_inputs.main_inputs is not None:
1247
1359
  for i, main_input in enumerate(self.node_inputs.main_inputs):
1248
- edges.append(schemas.NodeEdge(id=f'{main_input.node_id}-{self.node_id}-{i}',
1249
- source=main_input.node_id,
1250
- target=self.node_id,
1251
- sourceHandle='output-0',
1252
- targetHandle='input-0',
1253
- ))
1360
+ edges.append(
1361
+ schemas.NodeEdge(
1362
+ id=f"{main_input.node_id}-{self.node_id}-{i}",
1363
+ source=main_input.node_id,
1364
+ target=self.node_id,
1365
+ sourceHandle="output-0",
1366
+ targetHandle="input-0",
1367
+ )
1368
+ )
1254
1369
  if self.node_inputs.left_input is not None:
1255
- edges.append(schemas.NodeEdge(id=f'{self.node_inputs.left_input.node_id}-{self.node_id}-right',
1256
- source=self.node_inputs.left_input.node_id,
1257
- target=self.node_id,
1258
- sourceHandle='output-0',
1259
- targetHandle='input-2',
1260
- ))
1370
+ edges.append(
1371
+ schemas.NodeEdge(
1372
+ id=f"{self.node_inputs.left_input.node_id}-{self.node_id}-right",
1373
+ source=self.node_inputs.left_input.node_id,
1374
+ target=self.node_id,
1375
+ sourceHandle="output-0",
1376
+ targetHandle="input-2",
1377
+ )
1378
+ )
1261
1379
  if self.node_inputs.right_input is not None:
1262
- edges.append(schemas.NodeEdge(id=f'{self.node_inputs.right_input.node_id}-{self.node_id}-left',
1263
- source=self.node_inputs.right_input.node_id,
1264
- target=self.node_id,
1265
- sourceHandle='output-0',
1266
- targetHandle='input-1',
1267
- ))
1380
+ edges.append(
1381
+ schemas.NodeEdge(
1382
+ id=f"{self.node_inputs.right_input.node_id}-{self.node_id}-left",
1383
+ source=self.node_inputs.right_input.node_id,
1384
+ target=self.node_id,
1385
+ sourceHandle="output-0",
1386
+ targetHandle="input-1",
1387
+ )
1388
+ )
1268
1389
  return edges