Flowfile 0.4.1__py3-none-any.whl → 0.5.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- build_backends/main.py +25 -22
- build_backends/main_prd.py +10 -19
- flowfile/__init__.py +179 -73
- flowfile/__main__.py +10 -7
- flowfile/api.py +52 -59
- flowfile/web/__init__.py +14 -9
- flowfile/web/static/assets/AdminView-49392a9a.js +713 -0
- flowfile/web/static/assets/AdminView-f53bad23.css +129 -0
- flowfile/web/static/assets/CloudConnectionView-36bcd6df.css +72 -0
- flowfile/web/static/assets/{CloudConnectionManager-d3248f8d.js → CloudConnectionView-f13f202b.js} +11 -11
- flowfile/web/static/assets/{CloudStorageReader-d65bf041.js → CloudStorageReader-0023d4a5.js} +10 -8
- flowfile/web/static/assets/{CloudStorageReader-29d14fcc.css → CloudStorageReader-24c54524.css} +27 -27
- flowfile/web/static/assets/{CloudStorageWriter-b0ee067f.css → CloudStorageWriter-60547855.css} +26 -26
- flowfile/web/static/assets/{CloudStorageWriter-e83be3ed.js → CloudStorageWriter-8e781e11.js} +10 -8
- flowfile/web/static/assets/{ColumnSelector-47996a16.css → ColumnSelector-371637fb.css} +2 -2
- flowfile/web/static/assets/{ColumnSelector-cce661cf.js → ColumnSelector-8ad68ea9.js} +3 -5
- flowfile/web/static/assets/{ContextMenu-c13f91d0.css → ContextMenu-26d4dd27.css} +6 -6
- flowfile/web/static/assets/{ContextMenu-11a4652a.js → ContextMenu-31ee57f0.js} +3 -3
- flowfile/web/static/assets/{ContextMenu-160afb08.js → ContextMenu-69a74055.js} +3 -3
- flowfile/web/static/assets/{ContextMenu-cf18d2cc.js → ContextMenu-8e2051c6.js} +3 -3
- flowfile/web/static/assets/{ContextMenu-4c74eef1.css → ContextMenu-8ec1729e.css} +6 -6
- flowfile/web/static/assets/{ContextMenu-63cfa99b.css → ContextMenu-9b310c60.css} +6 -6
- flowfile/web/static/assets/{CrossJoin-d395d38c.js → CrossJoin-03df6938.js} +12 -10
- flowfile/web/static/assets/{CrossJoin-1119d18e.css → CrossJoin-71b4cc10.css} +20 -20
- flowfile/web/static/assets/CustomNode-59e99a86.css +32 -0
- flowfile/web/static/assets/{CustomNode-b812dc0b.js → CustomNode-8479239b.js} +36 -24
- flowfile/web/static/assets/{DatabaseConnectionSettings-7000bf2c.js → DatabaseConnectionSettings-869e3efd.js} +5 -4
- flowfile/web/static/assets/{DatabaseConnectionSettings-0c04b2e5.css → DatabaseConnectionSettings-e91df89a.css} +13 -13
- flowfile/web/static/assets/{DatabaseReader-ae61773c.css → DatabaseReader-36898a00.css} +24 -24
- flowfile/web/static/assets/{DatabaseReader-4f035d0c.js → DatabaseReader-c58b9552.js} +25 -15
- flowfile/web/static/assets/DatabaseView-6655afd6.css +57 -0
- flowfile/web/static/assets/{DatabaseManager-9662ec5b.js → DatabaseView-d26a9140.js} +11 -11
- flowfile/web/static/assets/{DatabaseWriter-2f570e53.css → DatabaseWriter-217a99f1.css} +19 -19
- flowfile/web/static/assets/{DatabaseWriter-f65dcd54.js → DatabaseWriter-4d05ddc7.js} +17 -10
- flowfile/web/static/assets/{designer-e3c150ec.css → DesignerView-a6d0ee84.css} +629 -538
- flowfile/web/static/assets/{designer-f3656d8c.js → DesignerView-e6f5c0e8.js} +1214 -3209
- flowfile/web/static/assets/{documentation-52b241e7.js → DocumentationView-2e78ef1b.js} +5 -5
- flowfile/web/static/assets/{documentation-12216a74.css → DocumentationView-fd46c656.css} +7 -7
- flowfile/web/static/assets/{ExploreData-2d0cf4db.css → ExploreData-10c5acc8.css} +13 -12
- flowfile/web/static/assets/{ExploreData-94c43dfc.js → ExploreData-7b54caca.js} +18 -9
- flowfile/web/static/assets/{ExternalSource-ac04b3cc.js → ExternalSource-3fa399b2.js} +9 -7
- flowfile/web/static/assets/{ExternalSource-e37b6275.css → ExternalSource-47ab05a3.css} +17 -17
- flowfile/web/static/assets/Filter-7494ea97.css +48 -0
- flowfile/web/static/assets/Filter-8cbbdbf3.js +287 -0
- flowfile/web/static/assets/{Formula-bb96803d.css → Formula-53d58c43.css} +7 -7
- flowfile/web/static/assets/{Formula-71472193.js → Formula-aac42b1e.js} +13 -11
- flowfile/web/static/assets/{FuzzyMatch-1010f966.css → FuzzyMatch-ad6361d6.css} +68 -69
- flowfile/web/static/assets/{FuzzyMatch-b317f631.js → FuzzyMatch-cd9bbfca.js} +12 -10
- flowfile/web/static/assets/{Pivot-cf333e3d.css → GraphSolver-c24dec17.css} +5 -5
- flowfile/web/static/assets/{GraphSolver-754a234f.js → GraphSolver-c7e6780e.js} +13 -11
- flowfile/web/static/assets/{GroupBy-6c6f9802.js → GroupBy-93c5d22b.js} +9 -7
- flowfile/web/static/assets/{GroupBy-b9505323.css → GroupBy-be7ac0bf.css} +10 -10
- flowfile/web/static/assets/{Join-fd79b451.css → Join-28b5e18f.css} +22 -22
- flowfile/web/static/assets/{Join-a1b800be.js → Join-a19b2de2.js} +13 -11
- flowfile/web/static/assets/LoginView-0df4ed0a.js +134 -0
- flowfile/web/static/assets/LoginView-d325d632.css +172 -0
- flowfile/web/static/assets/ManualInput-3702e677.css +293 -0
- flowfile/web/static/assets/{ManualInput-a9640276.js → ManualInput-8d3374b2.js} +170 -116
- flowfile/web/static/assets/{MultiSelect-97213888.js → MultiSelect-ad1b6243.js} +2 -2
- flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-6ffe088a.js → MultiSelect.vue_vue_type_script_setup_true_lang-e278950d.js} +1 -1
- flowfile/web/static/assets/NodeDesigner-40b647c9.js +2610 -0
- flowfile/web/static/assets/NodeDesigner-5f53be3f.css +1429 -0
- flowfile/web/static/assets/{NumericInput-e638088a.js → NumericInput-7100234c.js} +2 -2
- flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-90eb2cba.js → NumericInput.vue_vue_type_script_setup_true_lang-5130219f.js} +5 -2
- flowfile/web/static/assets/{Output-ddc9079f.css → Output-35e97000.css} +6 -6
- flowfile/web/static/assets/{Output-76750610.js → Output-f5efd2aa.js} +60 -38
- flowfile/web/static/assets/{GraphSolver-f0cb7bfb.css → Pivot-0eda81b4.css} +5 -5
- flowfile/web/static/assets/{Pivot-7814803f.js → Pivot-d981d23c.js} +11 -9
- flowfile/web/static/assets/PivotValidation-0e905b1a.css +13 -0
- flowfile/web/static/assets/{PivotValidation-f92137d2.js → PivotValidation-39386e95.js} +3 -3
- flowfile/web/static/assets/PivotValidation-41b57ad6.css +13 -0
- flowfile/web/static/assets/{PivotValidation-76dd431a.js → PivotValidation-63de1f73.js} +3 -3
- flowfile/web/static/assets/{PolarsCode-650322d1.css → PolarsCode-2b1f1f23.css} +4 -4
- flowfile/web/static/assets/{PolarsCode-889c3008.js → PolarsCode-f9d69217.js} +18 -9
- flowfile/web/static/assets/PopOver-b22f049e.js +939 -0
- flowfile/web/static/assets/PopOver-d96599db.css +33 -0
- flowfile/web/static/assets/{Read-6b17491f.css → Read-36e7bd51.css} +12 -12
- flowfile/web/static/assets/{Read-637b72a7.js → Read-aec2e377.js} +83 -105
- flowfile/web/static/assets/{RecordCount-2b050c41.js → RecordCount-78ed6845.js} +6 -4
- flowfile/web/static/assets/{RecordId-81df7784.js → RecordId-2156e890.js} +8 -6
- flowfile/web/static/assets/{SQLQueryComponent-36cef432.css → SQLQueryComponent-1c2f26b4.css} +5 -5
- flowfile/web/static/assets/{SQLQueryComponent-88dcfe53.js → SQLQueryComponent-48c72f5b.js} +3 -3
- flowfile/web/static/assets/{Sample-258ad2a9.js → Sample-1352ca74.js} +6 -4
- flowfile/web/static/assets/SecretSelector-22b5ff89.js +113 -0
- flowfile/web/static/assets/SecretSelector-6329f743.css +43 -0
- flowfile/web/static/assets/{SecretManager-2a2cb7e2.js → SecretsView-17df66ee.js} +35 -36
- flowfile/web/static/assets/SecretsView-aa291340.css +38 -0
- flowfile/web/static/assets/{Select-850215fd.js → Select-0aee4c54.js} +9 -7
- flowfile/web/static/assets/{SettingsSection-55bae608.js → SettingsSection-0784e157.js} +3 -3
- flowfile/web/static/assets/{SettingsSection-71e6b7e3.css → SettingsSection-07fbbc39.css} +4 -4
- flowfile/web/static/assets/{SettingsSection-5c696bee.css → SettingsSection-26fe48d4.css} +4 -4
- flowfile/web/static/assets/{SettingsSection-2e4d03c4.css → SettingsSection-8f980839.css} +4 -4
- flowfile/web/static/assets/{SettingsSection-0e8d9123.js → SettingsSection-cd341bb6.js} +3 -3
- flowfile/web/static/assets/{SettingsSection-29b4fa6b.js → SettingsSection-f2002a6d.js} +3 -3
- flowfile/web/static/assets/{SingleSelect-bebd408b.js → SingleSelect-460cc0ea.js} +2 -2
- flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-6093741c.js → SingleSelect.vue_vue_type_script_setup_true_lang-30741bb2.js} +1 -1
- flowfile/web/static/assets/{SliderInput-6a05ab61.js → SliderInput-5d926864.js} +7 -4
- flowfile/web/static/assets/SliderInput-f2e4f23c.css +4 -0
- flowfile/web/static/assets/{Sort-10ab48ed.js → Sort-3cdc971b.js} +9 -7
- flowfile/web/static/assets/{Unique-f9fb0809.css → Sort-8a871341.css} +10 -10
- flowfile/web/static/assets/{TextInput-df9d6259.js → TextInput-a2d0bfbd.js} +2 -2
- flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-000e1178.js → TextInput.vue_vue_type_script_setup_true_lang-abad1ca2.js} +5 -2
- flowfile/web/static/assets/{TextToRows-5d2c1190.css → TextToRows-12afb4f4.css} +10 -10
- flowfile/web/static/assets/{TextToRows-6c2d93d8.js → TextToRows-918945f7.js} +11 -10
- flowfile/web/static/assets/{ToggleSwitch-0ff7ac52.js → ToggleSwitch-f0ef5196.js} +2 -2
- flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-c6dc3029.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-5605c793.js} +1 -1
- flowfile/web/static/assets/{UnavailableFields-5edd5322.css → UnavailableFields-54d2f518.css} +6 -6
- flowfile/web/static/assets/{UnavailableFields-1bab97cb.js → UnavailableFields-bdad6144.js} +4 -4
- flowfile/web/static/assets/{Union-af6c3d9b.css → Union-d6a8d7d5.css} +7 -7
- flowfile/web/static/assets/{Union-b563478a.js → Union-e8ab8c86.js} +8 -6
- flowfile/web/static/assets/{Unique-f90db5db.js → Unique-8cd4f976.js} +13 -22
- flowfile/web/static/assets/{Sort-3643d625.css → Unique-9fb2f567.css} +10 -10
- flowfile/web/static/assets/{Unpivot-1e422df3.css → Unpivot-710a2948.css} +7 -7
- flowfile/web/static/assets/{Unpivot-bcb0025f.js → Unpivot-8da14095.js} +10 -8
- flowfile/web/static/assets/{UnpivotValidation-c4e73b04.js → UnpivotValidation-6f7d89ff.js} +3 -3
- flowfile/web/static/assets/UnpivotValidation-d5ca3b7b.css +13 -0
- flowfile/web/static/assets/{VueGraphicWalker-bb8535e2.js → VueGraphicWalker-3fb312e1.js} +4 -4
- flowfile/web/static/assets/{VueGraphicWalker-ed5ab88b.css → VueGraphicWalker-430f0b86.css} +1 -1
- flowfile/web/static/assets/{api-4c8e3822.js → api-24483f0d.js} +1 -1
- flowfile/web/static/assets/{api-2d6adc4f.js → api-8b81fa73.js} +1 -1
- flowfile/web/static/assets/{dropDown-35135ba8.css → dropDown-3d8dc5fa.css} +40 -40
- flowfile/web/static/assets/{dropDown-1bca8a74.js → dropDown-ac0fda9d.js} +3 -3
- flowfile/web/static/assets/{fullEditor-2985687e.js → fullEditor-5497a84a.js} +11 -10
- flowfile/web/static/assets/{fullEditor-178376bb.css → fullEditor-a0be62b3.css} +74 -62
- flowfile/web/static/assets/{genericNodeSettings-924759c7.css → genericNodeSettings-3b2507ea.css} +10 -10
- flowfile/web/static/assets/{genericNodeSettings-0476ba4e.js → genericNodeSettings-99014e1d.js} +5 -5
- flowfile/web/static/assets/index-07dda503.js +38 -0
- flowfile/web/static/assets/index-3ba44389.js +2696 -0
- flowfile/web/static/assets/{index-50508d4d.css → index-e6289dd0.css} +1945 -569
- flowfile/web/static/assets/{index-246f201c.js → index-fb6493ae.js} +41626 -40869
- flowfile/web/static/assets/node.types-2c15bb7e.js +82 -0
- flowfile/web/static/assets/nodeInput-0eb13f1a.js +2 -0
- flowfile/web/static/assets/{outputCsv-d686eeaf.js → outputCsv-8f8ba42d.js} +3 -3
- flowfile/web/static/assets/outputCsv-b9a072af.css +2499 -0
- flowfile/web/static/assets/{outputExcel-8809ea2f.js → outputExcel-393f4fef.js} +3 -3
- flowfile/web/static/assets/{outputExcel-b41305c0.css → outputExcel-f5d272b2.css} +26 -26
- flowfile/web/static/assets/{outputParquet-53ba645a.js → outputParquet-07c81f65.js} +4 -4
- flowfile/web/static/assets/outputParquet-54597c3c.css +4 -0
- flowfile/web/static/assets/{readCsv-053bf97b.js → readCsv-07f6d9ad.js} +21 -20
- flowfile/web/static/assets/{readCsv-bca3ed53.css → readCsv-3bfac4c3.css} +15 -15
- flowfile/web/static/assets/{readExcel-e1b381ea.css → readExcel-3db6b763.css} +13 -13
- flowfile/web/static/assets/{readExcel-ad531eab.js → readExcel-ed69bc8f.js} +10 -12
- flowfile/web/static/assets/{readParquet-cee068e2.css → readParquet-c5244ad5.css} +4 -4
- flowfile/web/static/assets/{readParquet-58e899a1.js → readParquet-e3ed4528.js} +4 -7
- flowfile/web/static/assets/secrets.api-002e7d7e.js +65 -0
- flowfile/web/static/assets/{selectDynamic-b38de2ba.js → selectDynamic-80b92899.js} +5 -5
- flowfile/web/static/assets/{selectDynamic-aa913ff4.css → selectDynamic-f2fb394f.css} +21 -20
- flowfile/web/static/assets/{vue-codemirror.esm-db9b8936.js → vue-codemirror.esm-0965f39f.js} +31 -637
- flowfile/web/static/assets/{vue-content-loader.es-b5f3ac30.js → vue-content-loader.es-c506ad97.js} +1 -1
- flowfile/web/static/index.html +2 -2
- {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/METADATA +4 -4
- flowfile-0.5.3.dist-info/RECORD +402 -0
- {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/WHEEL +1 -1
- {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/entry_points.txt +1 -0
- flowfile_core/__init__.py +13 -3
- flowfile_core/auth/jwt.py +51 -16
- flowfile_core/auth/models.py +32 -7
- flowfile_core/auth/password.py +89 -0
- flowfile_core/auth/secrets.py +8 -6
- flowfile_core/configs/__init__.py +9 -7
- flowfile_core/configs/flow_logger.py +15 -14
- flowfile_core/configs/node_store/__init__.py +72 -4
- flowfile_core/configs/node_store/nodes.py +155 -172
- flowfile_core/configs/node_store/user_defined_node_registry.py +108 -27
- flowfile_core/configs/settings.py +28 -15
- flowfile_core/database/connection.py +7 -6
- flowfile_core/database/init_db.py +96 -2
- flowfile_core/database/models.py +3 -1
- flowfile_core/fileExplorer/__init__.py +17 -0
- flowfile_core/fileExplorer/funcs.py +123 -57
- flowfile_core/fileExplorer/utils.py +10 -11
- flowfile_core/flowfile/_extensions/real_time_interface.py +10 -8
- flowfile_core/flowfile/analytics/analytics_processor.py +27 -24
- flowfile_core/flowfile/analytics/graphic_walker.py +11 -12
- flowfile_core/flowfile/analytics/utils.py +1 -1
- flowfile_core/flowfile/code_generator/code_generator.py +391 -279
- flowfile_core/flowfile/connection_manager/_connection_manager.py +6 -5
- flowfile_core/flowfile/connection_manager/models.py +1 -1
- flowfile_core/flowfile/database_connection_manager/db_connections.py +60 -44
- flowfile_core/flowfile/database_connection_manager/models.py +1 -1
- flowfile_core/flowfile/extensions.py +17 -12
- flowfile_core/flowfile/flow_data_engine/cloud_storage_reader.py +34 -32
- flowfile_core/flowfile/flow_data_engine/create/funcs.py +152 -103
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +526 -477
- flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +2 -2
- flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +92 -52
- flowfile_core/flowfile/flow_data_engine/flow_file_column/polars_type.py +12 -11
- flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +6 -6
- flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +26 -30
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +43 -32
- flowfile_core/flowfile/flow_data_engine/join/__init__.py +1 -1
- flowfile_core/flowfile/flow_data_engine/join/utils.py +11 -9
- flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +15 -11
- flowfile_core/flowfile/flow_data_engine/pivot_table.py +5 -7
- flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +95 -82
- flowfile_core/flowfile/flow_data_engine/read_excel_tables.py +66 -65
- flowfile_core/flowfile/flow_data_engine/sample_data.py +27 -21
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/__init__.py +1 -1
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +13 -11
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +360 -191
- flowfile_core/flowfile/flow_data_engine/threaded_processes.py +8 -8
- flowfile_core/flowfile/flow_data_engine/utils.py +101 -67
- flowfile_core/flowfile/flow_graph.py +1011 -561
- flowfile_core/flowfile/flow_graph_utils.py +31 -49
- flowfile_core/flowfile/flow_node/flow_node.py +332 -232
- flowfile_core/flowfile/flow_node/models.py +54 -41
- flowfile_core/flowfile/flow_node/schema_callback.py +14 -19
- flowfile_core/flowfile/graph_tree/graph_tree.py +41 -41
- flowfile_core/flowfile/handler.py +82 -32
- flowfile_core/flowfile/manage/compatibility_enhancements.py +493 -47
- flowfile_core/flowfile/manage/io_flowfile.py +391 -0
- flowfile_core/flowfile/node_designer/__init__.py +15 -13
- flowfile_core/flowfile/node_designer/_type_registry.py +34 -37
- flowfile_core/flowfile/node_designer/custom_node.py +162 -36
- flowfile_core/flowfile/node_designer/ui_components.py +136 -35
- flowfile_core/flowfile/schema_callbacks.py +77 -54
- flowfile_core/flowfile/setting_generator/__init__.py +0 -1
- flowfile_core/flowfile/setting_generator/setting_generator.py +6 -5
- flowfile_core/flowfile/setting_generator/settings.py +72 -55
- flowfile_core/flowfile/sources/external_sources/base_class.py +12 -10
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/external_source.py +27 -17
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/sample_users.py +9 -9
- flowfile_core/flowfile/sources/external_sources/factory.py +0 -1
- flowfile_core/flowfile/sources/external_sources/sql_source/models.py +45 -31
- flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +198 -73
- flowfile_core/flowfile/sources/external_sources/sql_source/utils.py +250 -196
- flowfile_core/flowfile/util/calculate_layout.py +9 -13
- flowfile_core/flowfile/util/execution_orderer.py +25 -17
- flowfile_core/flowfile/util/node_skipper.py +4 -4
- flowfile_core/flowfile/utils.py +19 -21
- flowfile_core/main.py +26 -19
- flowfile_core/routes/auth.py +284 -11
- flowfile_core/routes/cloud_connections.py +25 -25
- flowfile_core/routes/logs.py +21 -29
- flowfile_core/routes/public.py +3 -3
- flowfile_core/routes/routes.py +77 -43
- flowfile_core/routes/secrets.py +25 -27
- flowfile_core/routes/user_defined_components.py +483 -4
- flowfile_core/run_lock.py +0 -1
- flowfile_core/schemas/__init__.py +4 -6
- flowfile_core/schemas/analysis_schemas/graphic_walker_schemas.py +55 -55
- flowfile_core/schemas/cloud_storage_schemas.py +59 -55
- flowfile_core/schemas/input_schema.py +398 -154
- flowfile_core/schemas/output_model.py +50 -35
- flowfile_core/schemas/schemas.py +207 -67
- flowfile_core/schemas/transform_schema.py +1360 -435
- flowfile_core/schemas/yaml_types.py +117 -0
- flowfile_core/secret_manager/secret_manager.py +17 -13
- flowfile_core/{flowfile/node_designer/data_types.py → types.py} +33 -3
- flowfile_core/utils/arrow_reader.py +7 -6
- flowfile_core/utils/excel_file_manager.py +3 -3
- flowfile_core/utils/fileManager.py +7 -7
- flowfile_core/utils/fl_executor.py +8 -10
- flowfile_core/utils/utils.py +4 -4
- flowfile_core/utils/validate_setup.py +5 -4
- flowfile_frame/__init__.py +107 -50
- flowfile_frame/adapters.py +2 -9
- flowfile_frame/adding_expr.py +73 -32
- flowfile_frame/cloud_storage/frame_helpers.py +27 -23
- flowfile_frame/cloud_storage/secret_manager.py +12 -26
- flowfile_frame/config.py +2 -5
- flowfile_frame/expr.py +311 -218
- flowfile_frame/expr.pyi +160 -159
- flowfile_frame/expr_name.py +23 -23
- flowfile_frame/flow_frame.py +581 -489
- flowfile_frame/flow_frame.pyi +123 -104
- flowfile_frame/flow_frame_methods.py +236 -252
- flowfile_frame/group_frame.py +50 -20
- flowfile_frame/join.py +2 -2
- flowfile_frame/lazy.py +129 -87
- flowfile_frame/lazy_methods.py +83 -30
- flowfile_frame/list_name_space.py +55 -50
- flowfile_frame/selectors.py +148 -68
- flowfile_frame/series.py +9 -7
- flowfile_frame/utils.py +19 -21
- flowfile_worker/__init__.py +12 -4
- flowfile_worker/configs.py +11 -19
- flowfile_worker/create/__init__.py +14 -27
- flowfile_worker/create/funcs.py +143 -94
- flowfile_worker/create/models.py +139 -68
- flowfile_worker/create/pl_types.py +14 -15
- flowfile_worker/create/read_excel_tables.py +34 -41
- flowfile_worker/create/utils.py +22 -19
- flowfile_worker/external_sources/s3_source/main.py +18 -51
- flowfile_worker/external_sources/s3_source/models.py +34 -27
- flowfile_worker/external_sources/sql_source/main.py +8 -5
- flowfile_worker/external_sources/sql_source/models.py +13 -9
- flowfile_worker/flow_logger.py +10 -8
- flowfile_worker/funcs.py +214 -155
- flowfile_worker/main.py +11 -17
- flowfile_worker/models.py +35 -28
- flowfile_worker/process_manager.py +2 -3
- flowfile_worker/routes.py +121 -93
- flowfile_worker/secrets.py +9 -6
- flowfile_worker/spawner.py +80 -49
- flowfile_worker/utils.py +3 -2
- shared/__init__.py +2 -7
- shared/storage_config.py +25 -13
- test_utils/postgres/commands.py +3 -2
- test_utils/postgres/fixtures.py +9 -9
- test_utils/s3/commands.py +1 -1
- test_utils/s3/data_generator.py +3 -4
- test_utils/s3/demo_data_generator.py +4 -7
- test_utils/s3/fixtures.py +7 -5
- tools/migrate/README.md +56 -0
- tools/migrate/__init__.py +12 -0
- tools/migrate/__main__.py +118 -0
- tools/migrate/legacy_schemas.py +682 -0
- tools/migrate/migrate.py +610 -0
- tools/migrate/tests/__init__.py +0 -0
- tools/migrate/tests/conftest.py +21 -0
- tools/migrate/tests/test_migrate.py +622 -0
- tools/migrate/tests/test_migration_e2e.py +1009 -0
- tools/migrate/tests/test_node_migrations.py +843 -0
- flowfile/web/static/assets/CloudConnectionManager-2dfdce2f.css +0 -86
- flowfile/web/static/assets/CustomNode-74a37f74.css +0 -32
- flowfile/web/static/assets/DatabaseManager-30fa27e5.css +0 -64
- flowfile/web/static/assets/Filter-812dcbca.js +0 -164
- flowfile/web/static/assets/Filter-f62091b3.css +0 -20
- flowfile/web/static/assets/ManualInput-3246a08d.css +0 -96
- flowfile/web/static/assets/PivotValidation-891ddfb0.css +0 -13
- flowfile/web/static/assets/PivotValidation-c46cd420.css +0 -13
- flowfile/web/static/assets/SliderInput-b8fb6a8c.css +0 -4
- flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +0 -13
- flowfile/web/static/assets/outputCsv-9cc59e0b.css +0 -2499
- flowfile/web/static/assets/outputParquet-cf8cf3f2.css +0 -4
- flowfile/web/static/assets/secretApi-538058f3.js +0 -46
- flowfile/web/static/assets/vue-codemirror-bccfde04.css +0 -32
- flowfile-0.4.1.dist-info/RECORD +0 -376
- flowfile_core/flowfile/manage/open_flowfile.py +0 -143
- {flowfile-0.4.1.dist-info → flowfile-0.5.3.dist-info}/licenses/LICENSE +0 -0
- /flowfile_core/flowfile/manage/manage_flowfile.py → /tools/__init__.py +0 -0
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
import ast
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
from pathlib import Path
|
|
3
6
|
|
|
4
|
-
from fastapi import APIRouter, HTTPException, Depends
|
|
7
|
+
from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
from pydantic import BaseModel
|
|
5
10
|
|
|
6
11
|
from flowfile_core import flow_file_handler
|
|
7
12
|
# Core modules
|
|
8
13
|
from flowfile_core.auth.jwt import get_current_active_user
|
|
9
14
|
from flowfile_core.configs import logger
|
|
10
|
-
from flowfile_core.configs.node_store import
|
|
15
|
+
from flowfile_core.configs.node_store import (
|
|
16
|
+
CUSTOM_NODE_STORE,
|
|
17
|
+
add_to_custom_node_store,
|
|
18
|
+
remove_from_custom_node_store,
|
|
19
|
+
load_single_node_from_file,
|
|
20
|
+
)
|
|
11
21
|
# File handling
|
|
12
22
|
from flowfile_core.schemas import input_schema
|
|
13
23
|
from flowfile_core.utils.utils import camel_case_to_snake_case
|
|
24
|
+
from shared import storage
|
|
14
25
|
|
|
15
26
|
# External dependencies
|
|
16
27
|
|
|
@@ -18,6 +29,22 @@ from flowfile_core.utils.utils import camel_case_to_snake_case
|
|
|
18
29
|
router = APIRouter()
|
|
19
30
|
|
|
20
31
|
|
|
32
|
+
class CustomNodeInfo(BaseModel):
|
|
33
|
+
"""Info about a custom node file."""
|
|
34
|
+
file_name: str
|
|
35
|
+
node_name: str = ""
|
|
36
|
+
node_category: str = ""
|
|
37
|
+
title: str = ""
|
|
38
|
+
intro: str = ""
|
|
39
|
+
node_icon: str = "user-defined-icon.png"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SaveCustomNodeRequest(BaseModel):
|
|
43
|
+
"""Request model for saving a custom node."""
|
|
44
|
+
file_name: str
|
|
45
|
+
code: str
|
|
46
|
+
|
|
47
|
+
|
|
21
48
|
@router.get("/custom-node-schema", summary="Get a simple UI schema")
|
|
22
49
|
def get_simple_custom_object(flow_id: int, node_id: int):
|
|
23
50
|
"""
|
|
@@ -48,8 +75,460 @@ def update_user_defined_node(input_data: Dict[str, Any], node_type: str, current
|
|
|
48
75
|
user_defined_model = CUSTOM_NODE_STORE.get(node_type)
|
|
49
76
|
if not user_defined_model:
|
|
50
77
|
raise HTTPException(status_code=404, detail=f"Node type '{node_type}' not found")
|
|
51
|
-
|
|
78
|
+
print('adding user defined node')
|
|
79
|
+
print(input_data)
|
|
80
|
+
print('-----')
|
|
52
81
|
user_defined_node_settings = input_schema.UserDefinedNode.model_validate(input_data)
|
|
53
82
|
initialized_model = user_defined_model.from_settings(user_defined_node_settings.settings)
|
|
54
83
|
|
|
55
84
|
flow.add_user_defined_node(custom_node=initialized_model, user_defined_node_settings=user_defined_node_settings)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@router.post("/save-custom-node", summary="Save a custom node definition")
|
|
88
|
+
def save_custom_node(request: SaveCustomNodeRequest):
|
|
89
|
+
"""
|
|
90
|
+
Save a custom node Python file to the user-defined nodes directory.
|
|
91
|
+
|
|
92
|
+
This endpoint:
|
|
93
|
+
1. Validates the Python syntax
|
|
94
|
+
2. Ensures the file name is safe
|
|
95
|
+
3. Writes the file to the user_defined_nodes directory
|
|
96
|
+
4. Attempts to load and register the new node
|
|
97
|
+
"""
|
|
98
|
+
# Validate file name
|
|
99
|
+
file_name = request.file_name
|
|
100
|
+
if not file_name.endswith('.py'):
|
|
101
|
+
file_name += '.py'
|
|
102
|
+
|
|
103
|
+
# Sanitize file name - only allow alphanumeric, underscore, and .py extension
|
|
104
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_]', '_', file_name[:-3]) + '.py'
|
|
105
|
+
if not safe_name or safe_name == '.py':
|
|
106
|
+
raise HTTPException(status_code=400, detail="Invalid file name")
|
|
107
|
+
|
|
108
|
+
# Validate Python syntax
|
|
109
|
+
try:
|
|
110
|
+
ast.parse(request.code)
|
|
111
|
+
except SyntaxError as e:
|
|
112
|
+
raise HTTPException(
|
|
113
|
+
status_code=400,
|
|
114
|
+
detail=f"Python syntax error at line {e.lineno}: {e.msg}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Get the directory path
|
|
118
|
+
nodes_dir = storage.user_defined_nodes_directory
|
|
119
|
+
file_path = nodes_dir / safe_name
|
|
120
|
+
|
|
121
|
+
# Write the file
|
|
122
|
+
try:
|
|
123
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
124
|
+
f.write(request.code)
|
|
125
|
+
logger.info(f"Saved custom node to {file_path}")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error(f"Failed to save custom node: {e}")
|
|
128
|
+
raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}")
|
|
129
|
+
|
|
130
|
+
# Try to load and register the node using the centralized loader
|
|
131
|
+
try:
|
|
132
|
+
node_class = load_single_node_from_file(file_path)
|
|
133
|
+
if node_class:
|
|
134
|
+
add_to_custom_node_store(node_class)
|
|
135
|
+
logger.info(f"Registered custom node: {node_class().node_name}")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.warning(f"Node saved but failed to load: {e}")
|
|
138
|
+
# Don't fail the request - the file is saved, it just couldn't be loaded yet
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
"success": True,
|
|
142
|
+
"file_name": safe_name,
|
|
143
|
+
"message": f"Node saved successfully to {safe_name}"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _extract_node_info_from_file(file_path: Path) -> CustomNodeInfo:
|
|
148
|
+
"""Extract node metadata from a Python file by parsing its AST."""
|
|
149
|
+
info = CustomNodeInfo(file_name=file_path.name)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
153
|
+
content = f.read()
|
|
154
|
+
|
|
155
|
+
tree = ast.parse(content)
|
|
156
|
+
|
|
157
|
+
# Find class definitions that might be custom nodes
|
|
158
|
+
for node in ast.walk(tree):
|
|
159
|
+
if isinstance(node, ast.ClassDef):
|
|
160
|
+
# Look for class attributes (both annotated and simple assignments)
|
|
161
|
+
for item in node.body:
|
|
162
|
+
attr_name = None
|
|
163
|
+
value = None
|
|
164
|
+
|
|
165
|
+
# Handle annotated assignments: node_name: str = "value"
|
|
166
|
+
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
167
|
+
attr_name = item.target.id
|
|
168
|
+
if item.value and isinstance(item.value, ast.Constant) and isinstance(item.value.value, str):
|
|
169
|
+
value = item.value.value
|
|
170
|
+
# Handle simple assignments: node_name = "value"
|
|
171
|
+
elif isinstance(item, ast.Assign):
|
|
172
|
+
for target in item.targets:
|
|
173
|
+
if isinstance(target, ast.Name):
|
|
174
|
+
attr_name = target.id
|
|
175
|
+
if isinstance(item.value, ast.Constant) and isinstance(item.value.value, str):
|
|
176
|
+
value = item.value.value
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
# Map attribute names to info fields
|
|
180
|
+
if attr_name and value:
|
|
181
|
+
if attr_name == "node_name":
|
|
182
|
+
info.node_name = value
|
|
183
|
+
elif attr_name == "node_category":
|
|
184
|
+
info.node_category = value
|
|
185
|
+
elif attr_name == "title":
|
|
186
|
+
info.title = value
|
|
187
|
+
elif attr_name == "intro":
|
|
188
|
+
info.intro = value
|
|
189
|
+
elif attr_name == "node_icon":
|
|
190
|
+
info.node_icon = value
|
|
191
|
+
|
|
192
|
+
# If we found a node_name, this is likely a custom node class
|
|
193
|
+
if info.node_name:
|
|
194
|
+
break
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning(f"Failed to parse node info from {file_path}: {e}")
|
|
198
|
+
|
|
199
|
+
return info
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@router.get("/list-custom-nodes", summary="List all custom nodes", response_model=List[CustomNodeInfo])
|
|
203
|
+
def list_custom_nodes() -> List[CustomNodeInfo]:
|
|
204
|
+
"""
|
|
205
|
+
List all custom node Python files in the user-defined nodes directory.
|
|
206
|
+
Returns basic metadata extracted from each file.
|
|
207
|
+
"""
|
|
208
|
+
nodes_dir = storage.user_defined_nodes_directory
|
|
209
|
+
nodes: List[CustomNodeInfo] = []
|
|
210
|
+
|
|
211
|
+
if not nodes_dir.exists():
|
|
212
|
+
return nodes
|
|
213
|
+
|
|
214
|
+
for file_path in nodes_dir.glob("*.py"):
|
|
215
|
+
if file_path.name.startswith("_"):
|
|
216
|
+
continue # Skip private files
|
|
217
|
+
info = _extract_node_info_from_file(file_path)
|
|
218
|
+
nodes.append(info)
|
|
219
|
+
|
|
220
|
+
# Sort by node name
|
|
221
|
+
nodes.sort(key=lambda x: x.node_name or x.file_name)
|
|
222
|
+
return nodes
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@router.get("/get-custom-node/{file_name}", summary="Get custom node details")
|
|
226
|
+
def get_custom_node(file_name: str) -> Dict[str, Any]:
|
|
227
|
+
"""
|
|
228
|
+
Get the full content and parsed metadata of a custom node file.
|
|
229
|
+
This endpoint is used by the Node Designer to load an existing node for editing.
|
|
230
|
+
"""
|
|
231
|
+
# Sanitize file name
|
|
232
|
+
if not file_name.endswith('.py'):
|
|
233
|
+
file_name += '.py'
|
|
234
|
+
|
|
235
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_.]', '_', file_name)
|
|
236
|
+
file_path = storage.user_defined_nodes_directory / safe_name
|
|
237
|
+
|
|
238
|
+
if not file_path.exists():
|
|
239
|
+
raise HTTPException(status_code=404, detail=f"Node file '{safe_name}' not found")
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
243
|
+
content = f.read()
|
|
244
|
+
except Exception as e:
|
|
245
|
+
raise HTTPException(status_code=500, detail=f"Failed to read file: {str(e)}")
|
|
246
|
+
|
|
247
|
+
# Parse the file to extract metadata and sections
|
|
248
|
+
result = {
|
|
249
|
+
"file_name": safe_name,
|
|
250
|
+
"content": content,
|
|
251
|
+
"metadata": {},
|
|
252
|
+
"sections": [],
|
|
253
|
+
"processCode": ""
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
tree = ast.parse(content)
|
|
258
|
+
|
|
259
|
+
for node in ast.walk(tree):
|
|
260
|
+
if isinstance(node, ast.ClassDef):
|
|
261
|
+
# Check if this looks like a custom node class (has node_name attribute)
|
|
262
|
+
is_custom_node = False
|
|
263
|
+
for item in node.body:
|
|
264
|
+
# Check annotated assignments: node_name: str = "value"
|
|
265
|
+
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
266
|
+
if item.target.id == "node_name":
|
|
267
|
+
is_custom_node = True
|
|
268
|
+
break
|
|
269
|
+
# Check simple assignments: node_name = "value"
|
|
270
|
+
elif isinstance(item, ast.Assign):
|
|
271
|
+
for target in item.targets:
|
|
272
|
+
if isinstance(target, ast.Name) and target.id == "node_name":
|
|
273
|
+
is_custom_node = True
|
|
274
|
+
break
|
|
275
|
+
|
|
276
|
+
if is_custom_node:
|
|
277
|
+
# Extract metadata from both annotated and simple assignments
|
|
278
|
+
for item in node.body:
|
|
279
|
+
attr_name = None
|
|
280
|
+
value = None
|
|
281
|
+
|
|
282
|
+
if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
|
|
283
|
+
attr_name = item.target.id
|
|
284
|
+
if item.value and isinstance(item.value, ast.Constant):
|
|
285
|
+
value = item.value.value
|
|
286
|
+
elif isinstance(item, ast.Assign):
|
|
287
|
+
for target in item.targets:
|
|
288
|
+
if isinstance(target, ast.Name):
|
|
289
|
+
attr_name = target.id
|
|
290
|
+
if isinstance(item.value, ast.Constant):
|
|
291
|
+
value = item.value.value
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
if attr_name and value is not None:
|
|
295
|
+
if attr_name in ["node_name", "node_category", "title", "intro", "node_icon"]:
|
|
296
|
+
result["metadata"][attr_name] = value
|
|
297
|
+
elif attr_name == "number_of_inputs":
|
|
298
|
+
result["metadata"]["number_of_inputs"] = value
|
|
299
|
+
elif attr_name == "number_of_outputs":
|
|
300
|
+
result["metadata"]["number_of_outputs"] = value
|
|
301
|
+
|
|
302
|
+
# Extract process method
|
|
303
|
+
for item in node.body:
|
|
304
|
+
if isinstance(item, ast.FunctionDef) and item.name == "process":
|
|
305
|
+
# Get the source code of the process method
|
|
306
|
+
start_line = item.lineno - 1
|
|
307
|
+
end_line = item.end_lineno if hasattr(item, 'end_lineno') else start_line + 20
|
|
308
|
+
lines = content.split('\n')
|
|
309
|
+
process_lines = lines[start_line:end_line]
|
|
310
|
+
result["processCode"] = '\n'.join(process_lines)
|
|
311
|
+
break
|
|
312
|
+
|
|
313
|
+
break
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
logger.warning(f"Failed to parse custom node file: {e}")
|
|
317
|
+
# Return the raw content even if parsing fails
|
|
318
|
+
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@router.delete("/delete-custom-node/{file_name}", summary="Delete a custom node")
|
|
323
|
+
def delete_custom_node(file_name: str) -> Dict[str, Any]:
|
|
324
|
+
"""
|
|
325
|
+
Delete a custom node Python file from the user-defined nodes directory.
|
|
326
|
+
This also attempts to unregister the node from the node store.
|
|
327
|
+
"""
|
|
328
|
+
# Sanitize file name
|
|
329
|
+
if not file_name.endswith('.py'):
|
|
330
|
+
file_name += '.py'
|
|
331
|
+
|
|
332
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_.]', '_', file_name)
|
|
333
|
+
file_path = storage.user_defined_nodes_directory / safe_name
|
|
334
|
+
|
|
335
|
+
if not file_path.exists():
|
|
336
|
+
raise HTTPException(status_code=404, detail=f"Node file '{safe_name}' not found")
|
|
337
|
+
|
|
338
|
+
# Try to find and unregister the node from all stores
|
|
339
|
+
try:
|
|
340
|
+
info = _extract_node_info_from_file(file_path)
|
|
341
|
+
file_stem = file_path.stem # filename without .py extension
|
|
342
|
+
logger.info(f"Extracted node info: node_name='{info.node_name}', file_name='{info.file_name}', file_stem='{file_stem}'")
|
|
343
|
+
|
|
344
|
+
# Use the centralized remove function which cleans up all stores
|
|
345
|
+
# Pass both the computed key from node_name and the file_stem as fallback
|
|
346
|
+
if info.node_name:
|
|
347
|
+
node_type_key = info.node_name.lower().replace(' ', '_')
|
|
348
|
+
logger.info(f"Computed node_type_key: '{node_type_key}'")
|
|
349
|
+
else:
|
|
350
|
+
node_type_key = file_stem
|
|
351
|
+
logger.info(f"Using file_stem as node_type_key: '{node_type_key}'")
|
|
352
|
+
|
|
353
|
+
if remove_from_custom_node_store(node_type_key, file_stem=file_stem):
|
|
354
|
+
logger.info(f"Unregistered custom node: {info.node_name or file_stem}")
|
|
355
|
+
else:
|
|
356
|
+
logger.warning(f"Node '{node_type_key}' was not found in stores during unregister")
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.warning(f"Could not unregister node: {e}")
|
|
359
|
+
|
|
360
|
+
# Delete the file
|
|
361
|
+
try:
|
|
362
|
+
file_path.unlink()
|
|
363
|
+
logger.info(f"Deleted custom node file: {file_path}")
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.error(f"Failed to delete custom node file: {e}")
|
|
366
|
+
raise HTTPException(status_code=500, detail=f"Failed to delete file: {str(e)}")
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
"success": True,
|
|
370
|
+
"file_name": safe_name,
|
|
371
|
+
"message": f"Node '{safe_name}' deleted successfully"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
# ==================== Custom Icon Endpoints ====================
|
|
376
|
+
|
|
377
|
+
class IconInfo(BaseModel):
|
|
378
|
+
"""Info about a custom icon file."""
|
|
379
|
+
file_name: str
|
|
380
|
+
is_custom: bool = True
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
ALLOWED_ICON_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.svg', '.gif', '.webp'}
|
|
384
|
+
MAX_ICON_SIZE = 5 * 1024 * 1024 # 5MB
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
@router.get("/list-icons", summary="List all available icons", response_model=List[IconInfo])
|
|
388
|
+
def list_icons() -> List[IconInfo]:
|
|
389
|
+
"""
|
|
390
|
+
List all icon files available for custom nodes.
|
|
391
|
+
Returns icons from the user_defined_nodes/icons directory.
|
|
392
|
+
"""
|
|
393
|
+
icons_dir = storage.user_defined_nodes_icons
|
|
394
|
+
icons: List[IconInfo] = []
|
|
395
|
+
|
|
396
|
+
if not icons_dir.exists():
|
|
397
|
+
return icons
|
|
398
|
+
|
|
399
|
+
for file_path in icons_dir.iterdir():
|
|
400
|
+
if file_path.is_file() and file_path.suffix.lower() in ALLOWED_ICON_EXTENSIONS:
|
|
401
|
+
icons.append(IconInfo(file_name=file_path.name, is_custom=True))
|
|
402
|
+
|
|
403
|
+
# Sort by file name
|
|
404
|
+
icons.sort(key=lambda x: x.file_name.lower())
|
|
405
|
+
return icons
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@router.post("/upload-icon", summary="Upload a custom icon")
|
|
409
|
+
async def upload_icon(file: UploadFile = File(...)) -> Dict[str, Any]:
|
|
410
|
+
"""
|
|
411
|
+
Upload a new icon file to the user_defined_nodes/icons directory.
|
|
412
|
+
|
|
413
|
+
Accepts PNG, JPG, JPEG, SVG, GIF, and WebP files up to 5MB.
|
|
414
|
+
"""
|
|
415
|
+
if not file.filename:
|
|
416
|
+
raise HTTPException(status_code=400, detail="No file provided")
|
|
417
|
+
|
|
418
|
+
# Validate file extension
|
|
419
|
+
file_ext = Path(file.filename).suffix.lower()
|
|
420
|
+
if file_ext not in ALLOWED_ICON_EXTENSIONS:
|
|
421
|
+
raise HTTPException(
|
|
422
|
+
status_code=400,
|
|
423
|
+
detail=f"Invalid file type. Allowed types: {', '.join(ALLOWED_ICON_EXTENSIONS)}"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Read file content
|
|
427
|
+
content = await file.read()
|
|
428
|
+
|
|
429
|
+
# Validate file size
|
|
430
|
+
if len(content) > MAX_ICON_SIZE:
|
|
431
|
+
raise HTTPException(
|
|
432
|
+
status_code=400,
|
|
433
|
+
detail=f"File too large. Maximum size is {MAX_ICON_SIZE // (1024 * 1024)}MB"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Sanitize filename - preserve hyphens, dots, and underscores
|
|
437
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_.\-]', '_', file.filename)
|
|
438
|
+
if not safe_name:
|
|
439
|
+
raise HTTPException(status_code=400, detail="Invalid file name")
|
|
440
|
+
|
|
441
|
+
icons_dir = storage.user_defined_nodes_icons
|
|
442
|
+
|
|
443
|
+
# Ensure the icons directory exists
|
|
444
|
+
icons_dir.mkdir(parents=True, exist_ok=True)
|
|
445
|
+
|
|
446
|
+
file_path = icons_dir / safe_name
|
|
447
|
+
|
|
448
|
+
# Write the file
|
|
449
|
+
try:
|
|
450
|
+
with open(file_path, 'wb') as f:
|
|
451
|
+
f.write(content)
|
|
452
|
+
logger.info(f"Uploaded icon: {file_path} (size: {len(content)} bytes)")
|
|
453
|
+
except Exception as e:
|
|
454
|
+
logger.error(f"Failed to save icon: {e}")
|
|
455
|
+
raise HTTPException(status_code=500, detail=f"Failed to save icon: {str(e)}")
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
"success": True,
|
|
459
|
+
"file_name": safe_name,
|
|
460
|
+
"path": str(file_path),
|
|
461
|
+
"message": f"Icon '{safe_name}' uploaded successfully"
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@router.get("/icon/{file_name}", summary="Get a custom icon file")
|
|
466
|
+
def get_icon(file_name: str) -> FileResponse:
|
|
467
|
+
"""
|
|
468
|
+
Retrieve a custom icon file by name.
|
|
469
|
+
Returns the icon file for display in the UI.
|
|
470
|
+
"""
|
|
471
|
+
# Sanitize file name - preserve hyphens, dots, and underscores
|
|
472
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_.\-]', '_', file_name)
|
|
473
|
+
|
|
474
|
+
icons_dir = storage.user_defined_nodes_icons
|
|
475
|
+
file_path = icons_dir / safe_name
|
|
476
|
+
|
|
477
|
+
logger.debug(f"Attempting to serve icon: {file_path}")
|
|
478
|
+
|
|
479
|
+
if not file_path.exists():
|
|
480
|
+
logger.warning(f"Icon not found: {file_path} (icons_dir exists: {icons_dir.exists()})")
|
|
481
|
+
# List available icons for debugging
|
|
482
|
+
if icons_dir.exists():
|
|
483
|
+
available = list(icons_dir.iterdir())
|
|
484
|
+
logger.debug(f"Available icons: {[f.name for f in available]}")
|
|
485
|
+
raise HTTPException(status_code=404, detail=f"Icon '{safe_name}' not found at {file_path}")
|
|
486
|
+
|
|
487
|
+
# Validate it's actually an icon file
|
|
488
|
+
if file_path.suffix.lower() not in ALLOWED_ICON_EXTENSIONS:
|
|
489
|
+
raise HTTPException(status_code=400, detail="Invalid file type")
|
|
490
|
+
|
|
491
|
+
# Determine content type
|
|
492
|
+
content_type_map = {
|
|
493
|
+
'.png': 'image/png',
|
|
494
|
+
'.jpg': 'image/jpeg',
|
|
495
|
+
'.jpeg': 'image/jpeg',
|
|
496
|
+
'.svg': 'image/svg+xml',
|
|
497
|
+
'.gif': 'image/gif',
|
|
498
|
+
'.webp': 'image/webp',
|
|
499
|
+
}
|
|
500
|
+
content_type = content_type_map.get(file_path.suffix.lower(), 'application/octet-stream')
|
|
501
|
+
|
|
502
|
+
return FileResponse(
|
|
503
|
+
path=file_path,
|
|
504
|
+
media_type=content_type,
|
|
505
|
+
filename=safe_name
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@router.delete("/delete-icon/{file_name}", summary="Delete a custom icon")
|
|
510
|
+
def delete_icon(file_name: str) -> Dict[str, Any]:
|
|
511
|
+
"""
|
|
512
|
+
Delete a custom icon file from the icons directory.
|
|
513
|
+
"""
|
|
514
|
+
# Sanitize file name - preserve hyphens, dots, and underscores
|
|
515
|
+
safe_name = re.sub(r'[^a-zA-Z0-9_.\-]', '_', file_name)
|
|
516
|
+
|
|
517
|
+
icons_dir = storage.user_defined_nodes_icons
|
|
518
|
+
file_path = icons_dir / safe_name
|
|
519
|
+
|
|
520
|
+
if not file_path.exists():
|
|
521
|
+
raise HTTPException(status_code=404, detail=f"Icon '{safe_name}' not found")
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
file_path.unlink()
|
|
525
|
+
logger.info(f"Deleted icon: {file_path}")
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.error(f"Failed to delete icon: {e}")
|
|
528
|
+
raise HTTPException(status_code=500, detail=f"Failed to delete icon: {str(e)}")
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
"success": True,
|
|
532
|
+
"file_name": safe_name,
|
|
533
|
+
"message": f"Icon '{safe_name}' deleted successfully"
|
|
534
|
+
}
|
flowfile_core/run_lock.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
from flowfile_core.schemas import input_schema as node_interface
|
|
2
|
-
from flowfile_core.schemas
|
|
1
|
+
from flowfile_core.schemas import input_schema as node_interface
|
|
2
|
+
from flowfile_core.schemas import transform_schema as transformation_settings
|
|
3
3
|
from flowfile_core.schemas.input_schema import RawData
|
|
4
|
+
from flowfile_core.schemas.schemas import FlowInformation, FlowSettings
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"transformation_settings", "node_interface", "FlowSettings", "FlowInformation", "RawData"
|
|
8
|
-
]
|
|
6
|
+
__all__ = ["transformation_settings", "node_interface", "FlowSettings", "FlowInformation", "RawData"]
|