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