Flowfile 0.4.1__py3-none-any.whl → 0.5.3__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 (332) hide show
  1. build_backends/main.py +25 -22
  2. build_backends/main_prd.py +10 -19
  3. flowfile/__init__.py +179 -73
  4. flowfile/__main__.py +10 -7
  5. flowfile/api.py +52 -59
  6. flowfile/web/__init__.py +14 -9
  7. flowfile/web/static/assets/AdminView-49392a9a.js +713 -0
  8. flowfile/web/static/assets/AdminView-f53bad23.css +129 -0
  9. flowfile/web/static/assets/CloudConnectionView-36bcd6df.css +72 -0
  10. flowfile/web/static/assets/{CloudConnectionManager-d3248f8d.js → CloudConnectionView-f13f202b.js} +11 -11
  11. flowfile/web/static/assets/{CloudStorageReader-d65bf041.js → CloudStorageReader-0023d4a5.js} +10 -8
  12. flowfile/web/static/assets/{CloudStorageReader-29d14fcc.css → CloudStorageReader-24c54524.css} +27 -27
  13. flowfile/web/static/assets/{CloudStorageWriter-b0ee067f.css → CloudStorageWriter-60547855.css} +26 -26
  14. flowfile/web/static/assets/{CloudStorageWriter-e83be3ed.js → CloudStorageWriter-8e781e11.js} +10 -8
  15. flowfile/web/static/assets/{ColumnSelector-47996a16.css → ColumnSelector-371637fb.css} +2 -2
  16. flowfile/web/static/assets/{ColumnSelector-cce661cf.js → ColumnSelector-8ad68ea9.js} +3 -5
  17. flowfile/web/static/assets/{ContextMenu-c13f91d0.css → ContextMenu-26d4dd27.css} +6 -6
  18. flowfile/web/static/assets/{ContextMenu-11a4652a.js → ContextMenu-31ee57f0.js} +3 -3
  19. flowfile/web/static/assets/{ContextMenu-160afb08.js → ContextMenu-69a74055.js} +3 -3
  20. flowfile/web/static/assets/{ContextMenu-cf18d2cc.js → ContextMenu-8e2051c6.js} +3 -3
  21. flowfile/web/static/assets/{ContextMenu-4c74eef1.css → ContextMenu-8ec1729e.css} +6 -6
  22. flowfile/web/static/assets/{ContextMenu-63cfa99b.css → ContextMenu-9b310c60.css} +6 -6
  23. flowfile/web/static/assets/{CrossJoin-d395d38c.js → CrossJoin-03df6938.js} +12 -10
  24. flowfile/web/static/assets/{CrossJoin-1119d18e.css → CrossJoin-71b4cc10.css} +20 -20
  25. flowfile/web/static/assets/CustomNode-59e99a86.css +32 -0
  26. flowfile/web/static/assets/{CustomNode-b812dc0b.js → CustomNode-8479239b.js} +36 -24
  27. flowfile/web/static/assets/{DatabaseConnectionSettings-7000bf2c.js → DatabaseConnectionSettings-869e3efd.js} +5 -4
  28. flowfile/web/static/assets/{DatabaseConnectionSettings-0c04b2e5.css → DatabaseConnectionSettings-e91df89a.css} +13 -13
  29. flowfile/web/static/assets/{DatabaseReader-ae61773c.css → DatabaseReader-36898a00.css} +24 -24
  30. flowfile/web/static/assets/{DatabaseReader-4f035d0c.js → DatabaseReader-c58b9552.js} +25 -15
  31. flowfile/web/static/assets/DatabaseView-6655afd6.css +57 -0
  32. flowfile/web/static/assets/{DatabaseManager-9662ec5b.js → DatabaseView-d26a9140.js} +11 -11
  33. flowfile/web/static/assets/{DatabaseWriter-2f570e53.css → DatabaseWriter-217a99f1.css} +19 -19
  34. flowfile/web/static/assets/{DatabaseWriter-f65dcd54.js → DatabaseWriter-4d05ddc7.js} +17 -10
  35. flowfile/web/static/assets/{designer-e3c150ec.css → DesignerView-a6d0ee84.css} +629 -538
  36. flowfile/web/static/assets/{designer-f3656d8c.js → DesignerView-e6f5c0e8.js} +1214 -3209
  37. flowfile/web/static/assets/{documentation-52b241e7.js → DocumentationView-2e78ef1b.js} +5 -5
  38. flowfile/web/static/assets/{documentation-12216a74.css → DocumentationView-fd46c656.css} +7 -7
  39. flowfile/web/static/assets/{ExploreData-2d0cf4db.css → ExploreData-10c5acc8.css} +13 -12
  40. flowfile/web/static/assets/{ExploreData-94c43dfc.js → ExploreData-7b54caca.js} +18 -9
  41. flowfile/web/static/assets/{ExternalSource-ac04b3cc.js → ExternalSource-3fa399b2.js} +9 -7
  42. flowfile/web/static/assets/{ExternalSource-e37b6275.css → ExternalSource-47ab05a3.css} +17 -17
  43. flowfile/web/static/assets/Filter-7494ea97.css +48 -0
  44. flowfile/web/static/assets/Filter-8cbbdbf3.js +287 -0
  45. flowfile/web/static/assets/{Formula-bb96803d.css → Formula-53d58c43.css} +7 -7
  46. flowfile/web/static/assets/{Formula-71472193.js → Formula-aac42b1e.js} +13 -11
  47. flowfile/web/static/assets/{FuzzyMatch-1010f966.css → FuzzyMatch-ad6361d6.css} +68 -69
  48. flowfile/web/static/assets/{FuzzyMatch-b317f631.js → FuzzyMatch-cd9bbfca.js} +12 -10
  49. flowfile/web/static/assets/{Pivot-cf333e3d.css → GraphSolver-c24dec17.css} +5 -5
  50. flowfile/web/static/assets/{GraphSolver-754a234f.js → GraphSolver-c7e6780e.js} +13 -11
  51. flowfile/web/static/assets/{GroupBy-6c6f9802.js → GroupBy-93c5d22b.js} +9 -7
  52. flowfile/web/static/assets/{GroupBy-b9505323.css → GroupBy-be7ac0bf.css} +10 -10
  53. flowfile/web/static/assets/{Join-fd79b451.css → Join-28b5e18f.css} +22 -22
  54. flowfile/web/static/assets/{Join-a1b800be.js → Join-a19b2de2.js} +13 -11
  55. flowfile/web/static/assets/LoginView-0df4ed0a.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-a9640276.js → ManualInput-8d3374b2.js} +170 -116
  59. flowfile/web/static/assets/{MultiSelect-97213888.js → MultiSelect-ad1b6243.js} +2 -2
  60. flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-6ffe088a.js → MultiSelect.vue_vue_type_script_setup_true_lang-e278950d.js} +1 -1
  61. flowfile/web/static/assets/NodeDesigner-40b647c9.js +2610 -0
  62. flowfile/web/static/assets/NodeDesigner-5f53be3f.css +1429 -0
  63. flowfile/web/static/assets/{NumericInput-e638088a.js → NumericInput-7100234c.js} +2 -2
  64. flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-90eb2cba.js → NumericInput.vue_vue_type_script_setup_true_lang-5130219f.js} +5 -2
  65. flowfile/web/static/assets/{Output-ddc9079f.css → Output-35e97000.css} +6 -6
  66. flowfile/web/static/assets/{Output-76750610.js → Output-f5efd2aa.js} +60 -38
  67. flowfile/web/static/assets/{GraphSolver-f0cb7bfb.css → Pivot-0eda81b4.css} +5 -5
  68. flowfile/web/static/assets/{Pivot-7814803f.js → Pivot-d981d23c.js} +11 -9
  69. flowfile/web/static/assets/PivotValidation-0e905b1a.css +13 -0
  70. flowfile/web/static/assets/{PivotValidation-f92137d2.js → PivotValidation-39386e95.js} +3 -3
  71. flowfile/web/static/assets/PivotValidation-41b57ad6.css +13 -0
  72. flowfile/web/static/assets/{PivotValidation-76dd431a.js → PivotValidation-63de1f73.js} +3 -3
  73. flowfile/web/static/assets/{PolarsCode-650322d1.css → PolarsCode-2b1f1f23.css} +4 -4
  74. flowfile/web/static/assets/{PolarsCode-889c3008.js → PolarsCode-f9d69217.js} +18 -9
  75. flowfile/web/static/assets/PopOver-b22f049e.js +939 -0
  76. flowfile/web/static/assets/PopOver-d96599db.css +33 -0
  77. flowfile/web/static/assets/{Read-6b17491f.css → Read-36e7bd51.css} +12 -12
  78. flowfile/web/static/assets/{Read-637b72a7.js → Read-aec2e377.js} +83 -105
  79. flowfile/web/static/assets/{RecordCount-2b050c41.js → RecordCount-78ed6845.js} +6 -4
  80. flowfile/web/static/assets/{RecordId-81df7784.js → RecordId-2156e890.js} +8 -6
  81. flowfile/web/static/assets/{SQLQueryComponent-36cef432.css → SQLQueryComponent-1c2f26b4.css} +5 -5
  82. flowfile/web/static/assets/{SQLQueryComponent-88dcfe53.js → SQLQueryComponent-48c72f5b.js} +3 -3
  83. flowfile/web/static/assets/{Sample-258ad2a9.js → Sample-1352ca74.js} +6 -4
  84. flowfile/web/static/assets/SecretSelector-22b5ff89.js +113 -0
  85. flowfile/web/static/assets/SecretSelector-6329f743.css +43 -0
  86. flowfile/web/static/assets/{SecretManager-2a2cb7e2.js → SecretsView-17df66ee.js} +35 -36
  87. flowfile/web/static/assets/SecretsView-aa291340.css +38 -0
  88. flowfile/web/static/assets/{Select-850215fd.js → Select-0aee4c54.js} +9 -7
  89. flowfile/web/static/assets/{SettingsSection-55bae608.js → SettingsSection-0784e157.js} +3 -3
  90. flowfile/web/static/assets/{SettingsSection-71e6b7e3.css → SettingsSection-07fbbc39.css} +4 -4
  91. flowfile/web/static/assets/{SettingsSection-5c696bee.css → SettingsSection-26fe48d4.css} +4 -4
  92. flowfile/web/static/assets/{SettingsSection-2e4d03c4.css → SettingsSection-8f980839.css} +4 -4
  93. flowfile/web/static/assets/{SettingsSection-0e8d9123.js → SettingsSection-cd341bb6.js} +3 -3
  94. flowfile/web/static/assets/{SettingsSection-29b4fa6b.js → SettingsSection-f2002a6d.js} +3 -3
  95. flowfile/web/static/assets/{SingleSelect-bebd408b.js → SingleSelect-460cc0ea.js} +2 -2
  96. flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-6093741c.js → SingleSelect.vue_vue_type_script_setup_true_lang-30741bb2.js} +1 -1
  97. flowfile/web/static/assets/{SliderInput-6a05ab61.js → SliderInput-5d926864.js} +7 -4
  98. flowfile/web/static/assets/SliderInput-f2e4f23c.css +4 -0
  99. flowfile/web/static/assets/{Sort-10ab48ed.js → Sort-3cdc971b.js} +9 -7
  100. flowfile/web/static/assets/{Unique-f9fb0809.css → Sort-8a871341.css} +10 -10
  101. flowfile/web/static/assets/{TextInput-df9d6259.js → TextInput-a2d0bfbd.js} +2 -2
  102. flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-000e1178.js → TextInput.vue_vue_type_script_setup_true_lang-abad1ca2.js} +5 -2
  103. flowfile/web/static/assets/{TextToRows-5d2c1190.css → TextToRows-12afb4f4.css} +10 -10
  104. flowfile/web/static/assets/{TextToRows-6c2d93d8.js → TextToRows-918945f7.js} +11 -10
  105. flowfile/web/static/assets/{ToggleSwitch-0ff7ac52.js → ToggleSwitch-f0ef5196.js} +2 -2
  106. flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-c6dc3029.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-5605c793.js} +1 -1
  107. flowfile/web/static/assets/{UnavailableFields-5edd5322.css → UnavailableFields-54d2f518.css} +6 -6
  108. flowfile/web/static/assets/{UnavailableFields-1bab97cb.js → UnavailableFields-bdad6144.js} +4 -4
  109. flowfile/web/static/assets/{Union-af6c3d9b.css → Union-d6a8d7d5.css} +7 -7
  110. flowfile/web/static/assets/{Union-b563478a.js → Union-e8ab8c86.js} +8 -6
  111. flowfile/web/static/assets/{Unique-f90db5db.js → Unique-8cd4f976.js} +13 -22
  112. flowfile/web/static/assets/{Sort-3643d625.css → Unique-9fb2f567.css} +10 -10
  113. flowfile/web/static/assets/{Unpivot-1e422df3.css → Unpivot-710a2948.css} +7 -7
  114. flowfile/web/static/assets/{Unpivot-bcb0025f.js → Unpivot-8da14095.js} +10 -8
  115. flowfile/web/static/assets/{UnpivotValidation-c4e73b04.js → UnpivotValidation-6f7d89ff.js} +3 -3
  116. flowfile/web/static/assets/UnpivotValidation-d5ca3b7b.css +13 -0
  117. flowfile/web/static/assets/{VueGraphicWalker-bb8535e2.js → VueGraphicWalker-3fb312e1.js} +4 -4
  118. flowfile/web/static/assets/{VueGraphicWalker-ed5ab88b.css → VueGraphicWalker-430f0b86.css} +1 -1
  119. flowfile/web/static/assets/{api-4c8e3822.js → api-24483f0d.js} +1 -1
  120. flowfile/web/static/assets/{api-2d6adc4f.js → api-8b81fa73.js} +1 -1
  121. flowfile/web/static/assets/{dropDown-35135ba8.css → dropDown-3d8dc5fa.css} +40 -40
  122. flowfile/web/static/assets/{dropDown-1bca8a74.js → dropDown-ac0fda9d.js} +3 -3
  123. flowfile/web/static/assets/{fullEditor-2985687e.js → fullEditor-5497a84a.js} +11 -10
  124. flowfile/web/static/assets/{fullEditor-178376bb.css → fullEditor-a0be62b3.css} +74 -62
  125. flowfile/web/static/assets/{genericNodeSettings-924759c7.css → genericNodeSettings-3b2507ea.css} +10 -10
  126. flowfile/web/static/assets/{genericNodeSettings-0476ba4e.js → genericNodeSettings-99014e1d.js} +5 -5
  127. flowfile/web/static/assets/index-07dda503.js +38 -0
  128. flowfile/web/static/assets/index-3ba44389.js +2696 -0
  129. flowfile/web/static/assets/{index-50508d4d.css → index-e6289dd0.css} +1945 -569
  130. flowfile/web/static/assets/{index-246f201c.js → index-fb6493ae.js} +41626 -40869
  131. flowfile/web/static/assets/node.types-2c15bb7e.js +82 -0
  132. flowfile/web/static/assets/nodeInput-0eb13f1a.js +2 -0
  133. flowfile/web/static/assets/{outputCsv-d686eeaf.js → outputCsv-8f8ba42d.js} +3 -3
  134. flowfile/web/static/assets/outputCsv-b9a072af.css +2499 -0
  135. flowfile/web/static/assets/{outputExcel-8809ea2f.js → outputExcel-393f4fef.js} +3 -3
  136. flowfile/web/static/assets/{outputExcel-b41305c0.css → outputExcel-f5d272b2.css} +26 -26
  137. flowfile/web/static/assets/{outputParquet-53ba645a.js → outputParquet-07c81f65.js} +4 -4
  138. flowfile/web/static/assets/outputParquet-54597c3c.css +4 -0
  139. flowfile/web/static/assets/{readCsv-053bf97b.js → readCsv-07f6d9ad.js} +21 -20
  140. flowfile/web/static/assets/{readCsv-bca3ed53.css → readCsv-3bfac4c3.css} +15 -15
  141. flowfile/web/static/assets/{readExcel-e1b381ea.css → readExcel-3db6b763.css} +13 -13
  142. flowfile/web/static/assets/{readExcel-ad531eab.js → readExcel-ed69bc8f.js} +10 -12
  143. flowfile/web/static/assets/{readParquet-cee068e2.css → readParquet-c5244ad5.css} +4 -4
  144. flowfile/web/static/assets/{readParquet-58e899a1.js → readParquet-e3ed4528.js} +4 -7
  145. flowfile/web/static/assets/secrets.api-002e7d7e.js +65 -0
  146. flowfile/web/static/assets/{selectDynamic-b38de2ba.js → selectDynamic-80b92899.js} +5 -5
  147. flowfile/web/static/assets/{selectDynamic-aa913ff4.css → selectDynamic-f2fb394f.css} +21 -20
  148. flowfile/web/static/assets/{vue-codemirror.esm-db9b8936.js → vue-codemirror.esm-0965f39f.js} +31 -637
  149. flowfile/web/static/assets/{vue-content-loader.es-b5f3ac30.js → vue-content-loader.es-c506ad97.js} +1 -1
  150. flowfile/web/static/index.html +2 -2
  151. {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/METADATA +4 -4
  152. flowfile-0.5.3.dist-info/RECORD +402 -0
  153. {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/WHEEL +1 -1
  154. {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/entry_points.txt +1 -0
  155. flowfile_core/__init__.py +13 -3
  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 +8 -6
  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 +123 -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 +27 -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/code_generator.py +391 -279
  177. flowfile_core/flowfile/connection_manager/_connection_manager.py +6 -5
  178. flowfile_core/flowfile/connection_manager/models.py +1 -1
  179. flowfile_core/flowfile/database_connection_manager/db_connections.py +60 -44
  180. flowfile_core/flowfile/database_connection_manager/models.py +1 -1
  181. flowfile_core/flowfile/extensions.py +17 -12
  182. flowfile_core/flowfile/flow_data_engine/cloud_storage_reader.py +34 -32
  183. flowfile_core/flowfile/flow_data_engine/create/funcs.py +152 -103
  184. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +526 -477
  185. flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +2 -2
  186. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +92 -52
  187. flowfile_core/flowfile/flow_data_engine/flow_file_column/polars_type.py +12 -11
  188. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +6 -6
  189. flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +26 -30
  190. flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +43 -32
  191. flowfile_core/flowfile/flow_data_engine/join/__init__.py +1 -1
  192. flowfile_core/flowfile/flow_data_engine/join/utils.py +11 -9
  193. flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +15 -11
  194. flowfile_core/flowfile/flow_data_engine/pivot_table.py +5 -7
  195. flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +95 -82
  196. flowfile_core/flowfile/flow_data_engine/read_excel_tables.py +66 -65
  197. flowfile_core/flowfile/flow_data_engine/sample_data.py +27 -21
  198. flowfile_core/flowfile/flow_data_engine/subprocess_operations/__init__.py +1 -1
  199. flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +13 -11
  200. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +360 -191
  201. flowfile_core/flowfile/flow_data_engine/threaded_processes.py +8 -8
  202. flowfile_core/flowfile/flow_data_engine/utils.py +101 -67
  203. flowfile_core/flowfile/flow_graph.py +1011 -561
  204. flowfile_core/flowfile/flow_graph_utils.py +31 -49
  205. flowfile_core/flowfile/flow_node/flow_node.py +332 -232
  206. flowfile_core/flowfile/flow_node/models.py +54 -41
  207. flowfile_core/flowfile/flow_node/schema_callback.py +14 -19
  208. flowfile_core/flowfile/graph_tree/graph_tree.py +41 -41
  209. flowfile_core/flowfile/handler.py +82 -32
  210. flowfile_core/flowfile/manage/compatibility_enhancements.py +493 -47
  211. flowfile_core/flowfile/manage/io_flowfile.py +391 -0
  212. flowfile_core/flowfile/node_designer/__init__.py +15 -13
  213. flowfile_core/flowfile/node_designer/_type_registry.py +34 -37
  214. flowfile_core/flowfile/node_designer/custom_node.py +162 -36
  215. flowfile_core/flowfile/node_designer/ui_components.py +136 -35
  216. flowfile_core/flowfile/schema_callbacks.py +77 -54
  217. flowfile_core/flowfile/setting_generator/__init__.py +0 -1
  218. flowfile_core/flowfile/setting_generator/setting_generator.py +6 -5
  219. flowfile_core/flowfile/setting_generator/settings.py +72 -55
  220. flowfile_core/flowfile/sources/external_sources/base_class.py +12 -10
  221. flowfile_core/flowfile/sources/external_sources/custom_external_sources/external_source.py +27 -17
  222. flowfile_core/flowfile/sources/external_sources/custom_external_sources/sample_users.py +9 -9
  223. flowfile_core/flowfile/sources/external_sources/factory.py +0 -1
  224. flowfile_core/flowfile/sources/external_sources/sql_source/models.py +45 -31
  225. flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +198 -73
  226. flowfile_core/flowfile/sources/external_sources/sql_source/utils.py +250 -196
  227. flowfile_core/flowfile/util/calculate_layout.py +9 -13
  228. flowfile_core/flowfile/util/execution_orderer.py +25 -17
  229. flowfile_core/flowfile/util/node_skipper.py +4 -4
  230. flowfile_core/flowfile/utils.py +19 -21
  231. flowfile_core/main.py +26 -19
  232. flowfile_core/routes/auth.py +284 -11
  233. flowfile_core/routes/cloud_connections.py +25 -25
  234. flowfile_core/routes/logs.py +21 -29
  235. flowfile_core/routes/public.py +3 -3
  236. flowfile_core/routes/routes.py +77 -43
  237. flowfile_core/routes/secrets.py +25 -27
  238. flowfile_core/routes/user_defined_components.py +483 -4
  239. flowfile_core/run_lock.py +0 -1
  240. flowfile_core/schemas/__init__.py +4 -6
  241. flowfile_core/schemas/analysis_schemas/graphic_walker_schemas.py +55 -55
  242. flowfile_core/schemas/cloud_storage_schemas.py +59 -55
  243. flowfile_core/schemas/input_schema.py +398 -154
  244. flowfile_core/schemas/output_model.py +50 -35
  245. flowfile_core/schemas/schemas.py +207 -67
  246. flowfile_core/schemas/transform_schema.py +1360 -435
  247. flowfile_core/schemas/yaml_types.py +117 -0
  248. flowfile_core/secret_manager/secret_manager.py +17 -13
  249. flowfile_core/{flowfile/node_designer/data_types.py → types.py} +33 -3
  250. flowfile_core/utils/arrow_reader.py +7 -6
  251. flowfile_core/utils/excel_file_manager.py +3 -3
  252. flowfile_core/utils/fileManager.py +7 -7
  253. flowfile_core/utils/fl_executor.py +8 -10
  254. flowfile_core/utils/utils.py +4 -4
  255. flowfile_core/utils/validate_setup.py +5 -4
  256. flowfile_frame/__init__.py +107 -50
  257. flowfile_frame/adapters.py +2 -9
  258. flowfile_frame/adding_expr.py +73 -32
  259. flowfile_frame/cloud_storage/frame_helpers.py +27 -23
  260. flowfile_frame/cloud_storage/secret_manager.py +12 -26
  261. flowfile_frame/config.py +2 -5
  262. flowfile_frame/expr.py +311 -218
  263. flowfile_frame/expr.pyi +160 -159
  264. flowfile_frame/expr_name.py +23 -23
  265. flowfile_frame/flow_frame.py +581 -489
  266. flowfile_frame/flow_frame.pyi +123 -104
  267. flowfile_frame/flow_frame_methods.py +236 -252
  268. flowfile_frame/group_frame.py +50 -20
  269. flowfile_frame/join.py +2 -2
  270. flowfile_frame/lazy.py +129 -87
  271. flowfile_frame/lazy_methods.py +83 -30
  272. flowfile_frame/list_name_space.py +55 -50
  273. flowfile_frame/selectors.py +148 -68
  274. flowfile_frame/series.py +9 -7
  275. flowfile_frame/utils.py +19 -21
  276. flowfile_worker/__init__.py +12 -4
  277. flowfile_worker/configs.py +11 -19
  278. flowfile_worker/create/__init__.py +14 -27
  279. flowfile_worker/create/funcs.py +143 -94
  280. flowfile_worker/create/models.py +139 -68
  281. flowfile_worker/create/pl_types.py +14 -15
  282. flowfile_worker/create/read_excel_tables.py +34 -41
  283. flowfile_worker/create/utils.py +22 -19
  284. flowfile_worker/external_sources/s3_source/main.py +18 -51
  285. flowfile_worker/external_sources/s3_source/models.py +34 -27
  286. flowfile_worker/external_sources/sql_source/main.py +8 -5
  287. flowfile_worker/external_sources/sql_source/models.py +13 -9
  288. flowfile_worker/flow_logger.py +10 -8
  289. flowfile_worker/funcs.py +214 -155
  290. flowfile_worker/main.py +11 -17
  291. flowfile_worker/models.py +35 -28
  292. flowfile_worker/process_manager.py +2 -3
  293. flowfile_worker/routes.py +121 -93
  294. flowfile_worker/secrets.py +9 -6
  295. flowfile_worker/spawner.py +80 -49
  296. flowfile_worker/utils.py +3 -2
  297. shared/__init__.py +2 -7
  298. shared/storage_config.py +25 -13
  299. test_utils/postgres/commands.py +3 -2
  300. test_utils/postgres/fixtures.py +9 -9
  301. test_utils/s3/commands.py +1 -1
  302. test_utils/s3/data_generator.py +3 -4
  303. test_utils/s3/demo_data_generator.py +4 -7
  304. test_utils/s3/fixtures.py +7 -5
  305. tools/migrate/README.md +56 -0
  306. tools/migrate/__init__.py +12 -0
  307. tools/migrate/__main__.py +118 -0
  308. tools/migrate/legacy_schemas.py +682 -0
  309. tools/migrate/migrate.py +610 -0
  310. tools/migrate/tests/__init__.py +0 -0
  311. tools/migrate/tests/conftest.py +21 -0
  312. tools/migrate/tests/test_migrate.py +622 -0
  313. tools/migrate/tests/test_migration_e2e.py +1009 -0
  314. tools/migrate/tests/test_node_migrations.py +843 -0
  315. flowfile/web/static/assets/CloudConnectionManager-2dfdce2f.css +0 -86
  316. flowfile/web/static/assets/CustomNode-74a37f74.css +0 -32
  317. flowfile/web/static/assets/DatabaseManager-30fa27e5.css +0 -64
  318. flowfile/web/static/assets/Filter-812dcbca.js +0 -164
  319. flowfile/web/static/assets/Filter-f62091b3.css +0 -20
  320. flowfile/web/static/assets/ManualInput-3246a08d.css +0 -96
  321. flowfile/web/static/assets/PivotValidation-891ddfb0.css +0 -13
  322. flowfile/web/static/assets/PivotValidation-c46cd420.css +0 -13
  323. flowfile/web/static/assets/SliderInput-b8fb6a8c.css +0 -4
  324. flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +0 -13
  325. flowfile/web/static/assets/outputCsv-9cc59e0b.css +0 -2499
  326. flowfile/web/static/assets/outputParquet-cf8cf3f2.css +0 -4
  327. flowfile/web/static/assets/secretApi-538058f3.js +0 -46
  328. flowfile/web/static/assets/vue-codemirror-bccfde04.css +0 -32
  329. flowfile-0.4.1.dist-info/RECORD +0 -376
  330. flowfile_core/flowfile/manage/open_flowfile.py +0 -143
  331. {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/licenses/LICENSE +0 -0
  332. /flowfile_core/flowfile/manage/manage_flowfile.py → /tools/__init__.py +0 -0
flowfile_core/auth/jwt.py CHANGED
@@ -3,15 +3,14 @@
3
3
  import os
4
4
  import secrets
5
5
  from datetime import datetime, timedelta
6
- from typing import Optional
7
6
 
8
- from fastapi import APIRouter, Depends, HTTPException, status, Query
7
+ from fastapi import APIRouter, Depends, HTTPException, Query, status
9
8
  from fastapi.security import OAuth2PasswordBearer
10
9
  from jose import JWTError, jwt
11
10
  from sqlalchemy.orm import Session
12
- from flowfile_core.auth.secrets import get_password, set_password
13
11
 
14
- from flowfile_core.auth.models import User, TokenData
12
+ from flowfile_core.auth.models import TokenData, User
13
+ from flowfile_core.auth.secrets import get_password, set_password
15
14
  from flowfile_core.database import models as db_models
16
15
  from flowfile_core.database.connection import get_db
17
16
 
@@ -59,10 +58,10 @@ def get_current_user_sync(token: str, db: Session):
59
58
  except JWTError:
60
59
  raise credentials_exception
61
60
 
62
- # In Electron mode, if token is valid, return default user
61
+ # In Electron mode, if token is valid, return default user (always admin in electron mode)
63
62
  if os.environ.get("FLOWFILE_MODE") == "electron":
64
63
  if token_data.username == "local_user":
65
- electron_user = User(username="local_user", id=1, disabled=False)
64
+ electron_user = User(username="local_user", id=1, disabled=False, is_admin=True, must_change_password=False)
66
65
  return electron_user
67
66
  else:
68
67
  # Invalid username in token
@@ -74,7 +73,16 @@ def get_current_user_sync(token: str, db: Session):
74
73
  raise credentials_exception
75
74
  if user.disabled:
76
75
  raise HTTPException(status_code=400, detail="Inactive user")
77
- return user
76
+ # Convert to Pydantic User model
77
+ return User(
78
+ username=user.username,
79
+ id=user.id,
80
+ email=user.email,
81
+ full_name=user.full_name,
82
+ disabled=user.disabled,
83
+ is_admin=user.is_admin,
84
+ must_change_password=user.must_change_password
85
+ )
78
86
 
79
87
 
80
88
  async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
@@ -98,10 +106,10 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De
98
106
  except JWTError:
99
107
  raise credentials_exception
100
108
 
101
- # In Electron mode, if token is valid, return default user
109
+ # In Electron mode, if token is valid, return default user (always admin in electron mode)
102
110
  if os.environ.get("FLOWFILE_MODE") == "electron":
103
111
  if token_data.username == "local_user":
104
- electron_user = User(username="local_user", id=1, disabled=False)
112
+ electron_user = User(username="local_user", id=1, disabled=False, is_admin=True, must_change_password=False)
105
113
  return electron_user
106
114
  else:
107
115
  # Invalid username in token
@@ -113,16 +121,25 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De
113
121
  raise credentials_exception
114
122
  if user.disabled:
115
123
  raise HTTPException(status_code=400, detail="Inactive user")
116
- return user
124
+ # Convert to Pydantic User model
125
+ return User(
126
+ username=user.username,
127
+ id=user.id,
128
+ email=user.email,
129
+ full_name=user.full_name,
130
+ disabled=user.disabled,
131
+ is_admin=user.is_admin,
132
+ must_change_password=user.must_change_password
133
+ )
117
134
 
118
135
 
119
136
  def get_current_active_user(current_user=Depends(get_current_user)):
120
- if hasattr(current_user, 'disabled') and current_user.disabled:
137
+ if hasattr(current_user, "disabled") and current_user.disabled:
121
138
  raise HTTPException(status_code=400, detail="Inactive user")
122
139
  return current_user
123
140
 
124
141
 
125
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
142
+ def create_access_token(data: dict, expires_delta: timedelta | None = None):
126
143
  to_encode = data.copy()
127
144
 
128
145
  if expires_delta:
@@ -136,8 +153,7 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
136
153
 
137
154
 
138
155
  async def get_current_user_from_query(
139
- access_token: str = Query(..., description="JWT access token"),
140
- db: Session = Depends(get_db)
156
+ access_token: str = Query(..., description="JWT access token"), db: Session = Depends(get_db)
141
157
  ):
142
158
  """
143
159
  Authenticate user using only the query parameter token.
@@ -165,7 +181,7 @@ async def get_current_user_from_query(
165
181
  # Handle authentication based on deployment mode (same as your existing logic)
166
182
  if os.environ.get("FLOWFILE_MODE") == "electron":
167
183
  if token_data.username == "local_user":
168
- electron_user = User(username="local_user", id=1, disabled=False)
184
+ electron_user = User(username="local_user", id=1, disabled=False, is_admin=True, must_change_password=False)
169
185
  return electron_user
170
186
  else:
171
187
  raise credentials_exception
@@ -176,4 +192,23 @@ async def get_current_user_from_query(
176
192
  raise credentials_exception
177
193
  if user.disabled:
178
194
  raise HTTPException(status_code=400, detail="Inactive user")
179
- return user
195
+ # Convert to Pydantic User model
196
+ return User(
197
+ username=user.username,
198
+ id=user.id,
199
+ email=user.email,
200
+ full_name=user.full_name,
201
+ disabled=user.disabled,
202
+ is_admin=user.is_admin,
203
+ must_change_password=user.must_change_password
204
+ )
205
+
206
+
207
+ async def get_current_admin_user(current_user: User = Depends(get_current_user)):
208
+ """Dependency that requires the current user to be an admin"""
209
+ if not current_user.is_admin:
210
+ raise HTTPException(
211
+ status_code=status.HTTP_403_FORBIDDEN,
212
+ detail="Admin privileges required"
213
+ )
214
+ return current_user
@@ -1,6 +1,4 @@
1
-
2
1
  from pydantic import BaseModel, SecretStr
3
- from typing import Optional, List
4
2
 
5
3
 
6
4
  class Token(BaseModel):
@@ -9,21 +7,48 @@ class Token(BaseModel):
9
7
 
10
8
 
11
9
  class TokenData(BaseModel):
12
- username: Optional[str] = None
10
+ username: str | None = None
13
11
 
14
12
 
15
13
  class User(BaseModel):
16
14
  username: str
17
- id: Optional[int] = None
18
- email: Optional[str] = None
19
- full_name: Optional[str] = None
20
- disabled: Optional[bool] = False
15
+ id: int | None = None
16
+ email: str | None = None
17
+ full_name: str | None = None
18
+ disabled: bool | None = False
19
+ is_admin: bool | None = False
20
+ must_change_password: bool | None = False
21
21
 
22
22
 
23
23
  class UserInDB(User):
24
24
  hashed_password: str
25
25
 
26
26
 
27
+ class UserCreate(BaseModel):
28
+ """Model for creating a new user (admin only)"""
29
+ username: str
30
+ password: str
31
+ email: str | None = None
32
+ full_name: str | None = None
33
+ is_admin: bool = False
34
+
35
+
36
+ class UserUpdate(BaseModel):
37
+ """Model for updating a user (admin only)"""
38
+ email: str | None = None
39
+ full_name: str | None = None
40
+ disabled: bool | None = None
41
+ is_admin: bool | None = None
42
+ password: str | None = None # Optional password change
43
+ must_change_password: bool | None = None
44
+
45
+
46
+ class ChangePassword(BaseModel):
47
+ """Model for user changing their own password"""
48
+ current_password: str
49
+ new_password: str
50
+
51
+
27
52
  class SecretInput(BaseModel):
28
53
  name: str
29
54
  value: SecretStr
@@ -0,0 +1,89 @@
1
+ """Password hashing and verification utilities."""
2
+
3
+ import re
4
+ from passlib.context import CryptContext
5
+
6
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
7
+
8
+ # Password requirements
9
+ PASSWORD_MIN_LENGTH = 8
10
+ PASSWORD_REQUIREMENTS = {
11
+ "min_length": PASSWORD_MIN_LENGTH,
12
+ "require_number": True,
13
+ "require_special": True,
14
+ "special_chars": "!@#$%^&*()_+-=[]{}|;:,.<>?"
15
+ }
16
+
17
+
18
+ class PasswordValidationError(Exception):
19
+ """Raised when password doesn't meet requirements."""
20
+ pass
21
+
22
+
23
+ def validate_password(password: str) -> tuple[bool, str]:
24
+ """
25
+ Validate password against security requirements.
26
+
27
+ Requirements:
28
+ - Minimum 8 characters
29
+ - At least one number
30
+ - At least one special character
31
+
32
+ Args:
33
+ password: The plain text password to validate
34
+
35
+ Returns:
36
+ Tuple of (is_valid, error_message)
37
+ """
38
+ if len(password) < PASSWORD_MIN_LENGTH:
39
+ return False, f"Password must be at least {PASSWORD_MIN_LENGTH} characters long"
40
+
41
+ if not re.search(r'\d', password):
42
+ return False, "Password must contain at least one number"
43
+
44
+ if not re.search(r'[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]', password):
45
+ return False, "Password must contain at least one special character (!@#$%^&*()_+-=[]{}|;:,.<>?)"
46
+
47
+ return True, ""
48
+
49
+
50
+ def validate_password_or_raise(password: str) -> None:
51
+ """
52
+ Validate password and raise exception if invalid.
53
+
54
+ Args:
55
+ password: The plain text password to validate
56
+
57
+ Raises:
58
+ PasswordValidationError: If password doesn't meet requirements
59
+ """
60
+ is_valid, error_message = validate_password(password)
61
+ if not is_valid:
62
+ raise PasswordValidationError(error_message)
63
+
64
+
65
+ def verify_password(plain: str, hashed: str) -> bool:
66
+ """
67
+ Verify a plain password against a hashed password.
68
+
69
+ Args:
70
+ plain: The plain text password
71
+ hashed: The hashed password to verify against
72
+
73
+ Returns:
74
+ True if the password matches, False otherwise
75
+ """
76
+ return pwd_context.verify(plain, hashed)
77
+
78
+
79
+ def get_password_hash(password: str) -> str:
80
+ """
81
+ Hash a plain text password.
82
+
83
+ Args:
84
+ password: The plain text password to hash
85
+
86
+ Returns:
87
+ The hashed password
88
+ """
89
+ return pwd_context.hash(password)
@@ -1,11 +1,13 @@
1
1
  """
2
2
  Secure storage module for FlowFile credentials and secrets.
3
3
  """
4
- from cryptography.fernet import Fernet
5
- import os
6
- from pathlib import Path
4
+
7
5
  import json
8
6
  import logging
7
+ import os
8
+ from pathlib import Path
9
+
10
+ from cryptography.fernet import Fernet
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
@@ -15,7 +17,7 @@ class SecureStorage:
15
17
 
16
18
  def __init__(self):
17
19
  env = os.environ.get("FLOWFILE_MODE")
18
- logger.debug(f'Using secure storage in {env} mode')
20
+ logger.debug(f"Using secure storage in {env} mode")
19
21
  if os.environ.get("FLOWFILE_MODE") == "electron":
20
22
  app_data = os.environ.get("APPDATA") or os.path.expanduser("~/.config")
21
23
  self.storage_path = Path(app_data) / "flowfile"
@@ -148,7 +150,7 @@ def get_docker_secret_key():
148
150
  secret_path = "/run/secrets/flowfile_master_key"
149
151
  if os.path.exists(secret_path):
150
152
  try:
151
- with open(secret_path, "r") as f:
153
+ with open(secret_path) as f:
152
154
  return f.read().strip()
153
155
  except Exception as e:
154
156
  logger.error(f"Failed to read master key from Docker secret: {e}")
@@ -168,7 +170,7 @@ def get_master_key():
168
170
  Returns:
169
171
  str: The master encryption key
170
172
  """
171
- if os.environ.get("RUNNING_IN_DOCKER") == "true":
173
+ if os.environ.get("FLOWFILE_MODE") == "docker":
172
174
  return get_docker_secret_key()
173
175
 
174
176
  key = get_password("flowfile", "master_key")
@@ -1,13 +1,14 @@
1
1
  # flowfile_core/flowfile_core/configs/__init__.py
2
2
  import logging
3
+ import os
3
4
  import sys
4
5
  from pathlib import Path
5
- import os
6
6
 
7
- os.environ["FLOWFILE_MODE"] = "electron"
7
+ if "FLOWFILE_MODE" not in os.environ:
8
+ os.environ["FLOWFILE_MODE"] = "electron"
8
9
 
9
10
  # Create and configure the logger
10
- logger = logging.getLogger('PipelineHandler')
11
+ logger = logging.getLogger("PipelineHandler")
11
12
  logger.setLevel(logging.INFO)
12
13
  logger.propagate = False
13
14
 
@@ -17,9 +18,9 @@ if logger.hasHandlers():
17
18
 
18
19
  # Try to determine the best output stream
19
20
  output_stream = None
20
- if hasattr(sys.stdout, 'isatty') and sys.stdout.isatty():
21
+ if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
21
22
  output_stream = sys.stdout
22
- elif hasattr(sys.stderr, 'isatty') and sys.stderr.isatty():
23
+ elif hasattr(sys.stderr, "isatty") and sys.stderr.isatty():
23
24
  output_stream = sys.stderr
24
25
  else:
25
26
  # Use __stdout__ for debugger environments (PyDev, PyCharm, etc.)
@@ -29,7 +30,7 @@ console_handler = logging.StreamHandler(output_stream)
29
30
  console_handler.setLevel(logging.INFO)
30
31
 
31
32
  # Create formatter
32
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
33
+ formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
33
34
  console_handler.setFormatter(formatter)
34
35
 
35
36
  logger.addHandler(console_handler)
@@ -37,10 +38,11 @@ logger.addHandler(console_handler)
37
38
  # Create logs directory in temp at startup
38
39
  try:
39
40
  from tempfile import gettempdir
41
+
40
42
  log_dir = Path(gettempdir()) / "flowfile_logs"
41
43
  log_dir.mkdir(exist_ok=True)
42
44
  except Exception as e:
43
45
  logger.warning(f"Failed to create logs directory: {e}")
44
46
 
45
47
  # Initialize vault
46
- logger.info("Logging system initialized")
48
+ logger.info("Logging system initialized")
@@ -1,14 +1,15 @@
1
1
  import logging
2
- from pathlib import Path
3
- from datetime import datetime
4
- import os
5
2
  import logging.handlers
3
+ import os
6
4
  import queue
7
5
  import threading
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
8
9
  from shared.storage_config import storage
9
10
 
10
11
  _process_safe_queue = queue.Queue(-1)
11
- main_logger = logging.getLogger('PipelineHandler')
12
+ main_logger = logging.getLogger("PipelineHandler")
12
13
 
13
14
 
14
15
  class NodeLogger:
@@ -38,6 +39,7 @@ class NodeLogger:
38
39
 
39
40
  class FlowLogger:
40
41
  """Thread-safe logger for flow execution"""
42
+
41
43
  _instances = {}
42
44
  _instances_lock = threading.RLock()
43
45
  _queue_listener = None
@@ -47,7 +49,7 @@ class FlowLogger:
47
49
  def handle_extra_log_info(flow_id: int, extra: dict = None) -> dict:
48
50
  if extra is None:
49
51
  extra = {}
50
- extra['flow_id'] = flow_id
52
+ extra["flow_id"] = flow_id
51
53
  return extra
52
54
 
53
55
  def __new__(cls, flow_id: int, clear_existing_logs: bool = False):
@@ -75,7 +77,7 @@ class FlowLogger:
75
77
 
76
78
  def _setup_new_logger(self):
77
79
  """Creates a new logger instance with appropriate handlers"""
78
- logger_name = f'FlowExecution.{self.flow_id}'
80
+ logger_name = f"FlowExecution.{self.flow_id}"
79
81
  self._logger = logging.getLogger(logger_name)
80
82
  self._logger.setLevel(logging.INFO)
81
83
  self.setup_logging()
@@ -131,7 +133,7 @@ class FlowLogger:
131
133
 
132
134
  try:
133
135
  # Create an empty file
134
- with open(self.log_file_path, 'w') as f:
136
+ with open(self.log_file_path, "w") as f:
135
137
  pass
136
138
 
137
139
  # Re-setup the logger
@@ -154,9 +156,7 @@ class FlowLogger:
154
156
  """Start the queue listener for asynchronous logging"""
155
157
  queue_handler = logging.handlers.QueueHandler(_process_safe_queue)
156
158
  cls._queue_listener = logging.handlers.QueueListener(
157
- _process_safe_queue,
158
- queue_handler,
159
- respect_handler_level=True
159
+ _process_safe_queue, queue_handler, respect_handler_level=True
160
160
  )
161
161
  cls._queue_listener.start()
162
162
 
@@ -193,7 +193,7 @@ class FlowLogger:
193
193
 
194
194
  # Add file handler
195
195
  file_handler = logging.FileHandler(self.log_file_path)
196
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
196
+ formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
197
197
  file_handler.setFormatter(formatter)
198
198
  self._logger.addHandler(file_handler)
199
199
 
@@ -214,7 +214,8 @@ class FlowLogger:
214
214
  else:
215
215
  # If still can't get lock, proceed anyway
216
216
  main_logger.warning(
217
- f"Could not acquire lock for flow {self.flow_id}, proceeding with file clearing anyway")
217
+ f"Could not acquire lock for flow {self.flow_id}, proceeding with file clearing anyway"
218
+ )
218
219
  self._clear_log_impl()
219
220
 
220
221
  def _clear_log_impl(self):
@@ -223,7 +224,7 @@ class FlowLogger:
223
224
  # Ensure parent directory exists
224
225
  self.refresh_logger_if_needed()
225
226
  # Truncate file
226
- with open(self.log_file_path, 'w') as f:
227
+ with open(self.log_file_path, "w") as f:
227
228
  pass
228
229
  main_logger.info(f"Log file cleared for flow {self.flow_id}")
229
230
  except Exception as e:
@@ -409,7 +410,7 @@ def read_log_from_line(log_file_path: Path, start_line: int = 0):
409
410
  """Read log file content starting from a specific line"""
410
411
  lines = []
411
412
  try:
412
- with open(log_file_path, "r") as file:
413
+ with open(log_file_path) as file:
413
414
  # Skip lines efficiently if needed
414
415
  if start_line > 0:
415
416
  for _ in range(start_line):
@@ -1,10 +1,17 @@
1
- from flowfile_core.configs.node_store.user_defined_node_registry import get_all_nodes_from_standard_location
1
+ import logging
2
+
2
3
  from flowfile_core.configs.node_store.nodes import get_all_standard_nodes
3
- from flowfile_core.schemas.schemas import NodeTemplate
4
+ from flowfile_core.configs.node_store.user_defined_node_registry import (
5
+ get_all_nodes_from_standard_location,
6
+ load_single_node_from_file,
7
+ unload_node_by_name,
8
+ )
4
9
  from flowfile_core.flowfile.node_designer.custom_node import CustomNodeBase
10
+ from flowfile_core.schemas.schemas import NodeTemplate
5
11
 
12
+ logger = logging.getLogger(__name__)
6
13
 
7
- nodes_with_defaults = {'sample', 'sort', 'union', 'select', 'record_count'}
14
+ nodes_with_defaults = {"sample", "sort", "union", "select", "record_count"}
8
15
 
9
16
 
10
17
  def register_custom_node(node: NodeTemplate):
@@ -18,6 +25,68 @@ def add_to_custom_node_store(custom_node: type[CustomNodeBase]):
18
25
  register_custom_node(custom_node().to_node_template())
19
26
 
20
27
 
28
+ def remove_from_custom_node_store(node_key: str, file_stem: str = None) -> bool:
29
+ """
30
+ Remove a custom node from both CUSTOM_NODE_STORE and node registries.
31
+
32
+ Args:
33
+ node_key: The key/item name of the node to remove
34
+ file_stem: Optional file name stem (without .py) to use as fallback for matching
35
+
36
+ Returns:
37
+ True if the node was found and removed, False otherwise
38
+ """
39
+ removed = False
40
+
41
+ logger.info(f"Attempting to remove node with key: '{node_key}' (file_stem: '{file_stem}')")
42
+ logger.info(f"Current CUSTOM_NODE_STORE keys: {list(CUSTOM_NODE_STORE.keys())}")
43
+ logger.info(f"Current nodes_list items: {[n.item for n in nodes_list if hasattr(n, 'item')]}")
44
+
45
+ # Try to find the key - use exact match first, then fallback to file_stem
46
+ actual_key = None
47
+ if node_key in CUSTOM_NODE_STORE:
48
+ actual_key = node_key
49
+ elif file_stem and file_stem in CUSTOM_NODE_STORE:
50
+ actual_key = file_stem
51
+ logger.info(f"Using file_stem '{file_stem}' as key instead of '{node_key}'")
52
+
53
+ # Remove from CUSTOM_NODE_STORE
54
+ if actual_key and actual_key in CUSTOM_NODE_STORE:
55
+ del CUSTOM_NODE_STORE[actual_key]
56
+ logger.info(f"Removed '{actual_key}' from CUSTOM_NODE_STORE")
57
+ removed = True
58
+ else:
59
+ logger.warning(f"Key '{node_key}' (or file_stem '{file_stem}') not found in CUSTOM_NODE_STORE")
60
+
61
+ # Remove from node_dict - try both keys
62
+ key_to_use = actual_key or node_key
63
+ if key_to_use in node_dict:
64
+ del node_dict[key_to_use]
65
+ logger.info(f"Removed '{key_to_use}' from node_dict")
66
+ elif file_stem and file_stem in node_dict:
67
+ del node_dict[file_stem]
68
+ logger.info(f"Removed '{file_stem}' from node_dict")
69
+
70
+ # Remove from nodes_list - try both keys
71
+ removed_from_list = False
72
+ for i, node in enumerate(nodes_list):
73
+ if node.item == key_to_use or (file_stem and node.item == file_stem):
74
+ nodes_list.pop(i)
75
+ logger.info(f"Removed '{node.item}' from nodes_list at index {i}")
76
+ removed_from_list = True
77
+ break
78
+
79
+ if not removed_from_list:
80
+ logger.warning(f"Key '{node_key}' not found in nodes_list")
81
+
82
+ # Clean up module cache
83
+ unload_node_by_name(node_key)
84
+ if file_stem and file_stem != node_key:
85
+ unload_node_by_name(file_stem)
86
+
87
+ return removed
88
+
89
+
21
90
  CUSTOM_NODE_STORE = get_all_nodes_from_standard_location()
22
91
  nodes_list, node_dict, node_defaults = get_all_standard_nodes()
23
92
 
@@ -26,5 +95,4 @@ for custom_node in CUSTOM_NODE_STORE.values():
26
95
 
27
96
 
28
97
  def check_if_has_default_setting(node_item: str):
29
-
30
98
  return node_item in nodes_with_defaults