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