Flowfile 0.5.1__py3-none-any.whl → 0.5.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- build_backends/main.py +25 -22
- build_backends/main_prd.py +10 -19
- flowfile/__init__.py +194 -74
- flowfile/__main__.py +10 -7
- flowfile/api.py +51 -57
- flowfile/web/__init__.py +14 -9
- flowfile/web/static/assets/AdminView-f53bad23.css +129 -0
- flowfile/web/static/assets/AdminView-f9847d67.js +713 -0
- flowfile/web/static/assets/CloudConnectionView-cf85f943.css +72 -0
- flowfile/web/static/assets/{CloudConnectionManager-0dfba9f2.js → CloudConnectionView-faace55b.js} +11 -11
- flowfile/web/static/assets/{CloudStorageReader-29d14fcc.css → CloudStorageReader-24c54524.css} +27 -27
- flowfile/web/static/assets/{CloudStorageReader-d5b1b6c9.js → CloudStorageReader-d86ecaa7.js} +10 -8
- flowfile/web/static/assets/{CloudStorageWriter-00d87aad.js → CloudStorageWriter-0f4d9a44.js} +10 -8
- flowfile/web/static/assets/{CloudStorageWriter-b0ee067f.css → CloudStorageWriter-60547855.css} +26 -26
- flowfile/web/static/assets/ColumnActionInput-c44b7aee.css +159 -0
- flowfile/web/static/assets/ColumnActionInput-f4189ae0.js +330 -0
- flowfile/web/static/assets/{ColumnSelector-47996a16.css → ColumnSelector-371637fb.css} +2 -2
- flowfile/web/static/assets/{ColumnSelector-4685e75d.js → ColumnSelector-e66b33da.js} +3 -5
- flowfile/web/static/assets/ContextMenu-49463352.js +9 -0
- flowfile/web/static/assets/ContextMenu-dd5f3f25.js +9 -0
- flowfile/web/static/assets/ContextMenu-f709b884.js +9 -0
- flowfile/web/static/assets/ContextMenu.vue_vue_type_script_setup_true_lang-a1bd6314.js +59 -0
- flowfile/web/static/assets/{CrossJoin-702a3edd.js → CrossJoin-24694b8f.js} +12 -10
- flowfile/web/static/assets/{CrossJoin-1119d18e.css → CrossJoin-71b4cc10.css} +20 -20
- flowfile/web/static/assets/{CustomNode-b1519993.js → CustomNode-569d45ff.js} +43 -24
- flowfile/web/static/assets/CustomNode-edb9b939.css +42 -0
- flowfile/web/static/assets/{DatabaseConnectionSettings-0c04b2e5.css → DatabaseConnectionSettings-c20a1e16.css} +23 -21
- flowfile/web/static/assets/{DatabaseConnectionSettings-6f3e4ea5.js → DatabaseConnectionSettings-cfc08938.js} +5 -4
- flowfile/web/static/assets/{DatabaseReader-ae61773c.css → DatabaseReader-5bf8c75b.css} +41 -46
- flowfile/web/static/assets/{DatabaseReader-d38c7295.js → DatabaseReader-701feabb.js} +25 -15
- flowfile/web/static/assets/{DatabaseManager-cf5ef661.js → DatabaseView-0482e5b5.js} +11 -11
- flowfile/web/static/assets/DatabaseView-6655afd6.css +57 -0
- flowfile/web/static/assets/{DatabaseWriter-b04ef46a.js → DatabaseWriter-16721989.js} +17 -10
- flowfile/web/static/assets/{DatabaseWriter-2f570e53.css → DatabaseWriter-bdcf2c8b.css} +29 -27
- flowfile/web/static/assets/{designer-8da3ba3a.css → DesignerView-49abb835.css} +783 -663
- flowfile/web/static/assets/{designer-9633482a.js → DesignerView-f64749fb.js} +1292 -3253
- flowfile/web/static/assets/{documentation-ca400224.js → DocumentationView-61bd2990.js} +5 -5
- flowfile/web/static/assets/{documentation-12216a74.css → DocumentationView-9ea6e871.css} +9 -9
- flowfile/web/static/assets/{ExploreData-2d0cf4db.css → ExploreData-10c5acc8.css} +13 -12
- flowfile/web/static/assets/{ExploreData-5fa10ed8.js → ExploreData-e2735b13.js} +18 -9
- flowfile/web/static/assets/{ExternalSource-d39af878.js → ExternalSource-2535c3b2.js} +9 -7
- flowfile/web/static/assets/{ExternalSource-e37b6275.css → ExternalSource-7ac7373f.css} +20 -20
- flowfile/web/static/assets/Filter-2cdbc93c.js +287 -0
- flowfile/web/static/assets/Filter-7494ea97.css +48 -0
- flowfile/web/static/assets/{Formula-bb96803d.css → Formula-53d58c43.css} +7 -7
- flowfile/web/static/assets/{Formula-6b04fb1d.js → Formula-fcda3c2c.js} +13 -11
- flowfile/web/static/assets/{FuzzyMatch-1010f966.css → FuzzyMatch-ad6361d6.css} +68 -69
- flowfile/web/static/assets/{FuzzyMatch-999521f4.js → FuzzyMatch-f8d3b7d3.js} +12 -10
- flowfile/web/static/assets/{Pivot-cf333e3d.css → GraphSolver-4b4d7db9.css} +5 -5
- flowfile/web/static/assets/{GraphSolver-17dd2198.js → GraphSolver-72eaa695.js} +14 -12
- flowfile/web/static/assets/GroupBy-5792782d.css +9 -0
- flowfile/web/static/assets/{GroupBy-6b039e18.js → GroupBy-8aa0598b.js} +9 -7
- flowfile/web/static/assets/{Join-fd79b451.css → Join-28b5e18f.css} +22 -22
- flowfile/web/static/assets/{Join-24d0f113.js → Join-e40f0ffa.js} +13 -11
- flowfile/web/static/assets/LoginView-5111c9ae.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-34639209.js → ManualInput-9b6f3224.js} +170 -116
- flowfile/web/static/assets/{MultiSelect-0e8724a3.js → MultiSelect-ef28e19e.js} +2 -2
- flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-b0e538c2.js → MultiSelect.vue_vue_type_script_setup_true_lang-83b3bbfd.js} +1 -1
- flowfile/web/static/assets/NodeDesigner-94cd4dd3.css +1429 -0
- flowfile/web/static/assets/NodeDesigner-d2b7ee2b.js +2712 -0
- flowfile/web/static/assets/{NumericInput-3d63a470.js → NumericInput-1d789794.js} +2 -2
- flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-e0edeccc.js → NumericInput.vue_vue_type_script_setup_true_lang-7775f83e.js} +5 -2
- flowfile/web/static/assets/Output-692dd25d.css +37 -0
- flowfile/web/static/assets/{Output-edea9802.js → Output-cefef801.js} +14 -10
- flowfile/web/static/assets/{GraphSolver-f0cb7bfb.css → Pivot-0eda81b4.css} +5 -5
- flowfile/web/static/assets/{Pivot-61d19301.js → Pivot-bab1b75b.js} +12 -10
- flowfile/web/static/assets/PivotValidation-0e905b1a.css +13 -0
- flowfile/web/static/assets/PivotValidation-41b57ad6.css +13 -0
- flowfile/web/static/assets/{PivotValidation-f97fec5b.js → PivotValidation-e7941f91.js} +3 -3
- flowfile/web/static/assets/{PivotValidation-de9f43fe.js → PivotValidation-fba09336.js} +3 -3
- flowfile/web/static/assets/{PolarsCode-650322d1.css → PolarsCode-2b1f1f23.css} +4 -4
- flowfile/web/static/assets/{PolarsCode-bc3c9984.js → PolarsCode-740e40fa.js} +18 -9
- flowfile/web/static/assets/PopOver-862d7e28.js +939 -0
- flowfile/web/static/assets/PopOver-d96599db.css +33 -0
- flowfile/web/static/assets/{Read-64a3f259.js → Read-225cc63f.js} +16 -12
- flowfile/web/static/assets/{Read-e808b239.css → Read-90f366bc.css} +15 -15
- flowfile/web/static/assets/{RecordCount-3d5039be.js → RecordCount-ffc71eca.js} +6 -4
- flowfile/web/static/assets/{RecordId-597510e0.js → RecordId-a70bb8df.js} +9 -7
- flowfile/web/static/assets/{SQLQueryComponent-df51adbe.js → SQLQueryComponent-15a421f5.js} +3 -3
- flowfile/web/static/assets/SQLQueryComponent-edb90b98.css +29 -0
- flowfile/web/static/assets/{Sample-4be0a507.js → Sample-6c26afc7.js} +6 -4
- flowfile/web/static/assets/SecretSelector-6329f743.css +43 -0
- flowfile/web/static/assets/SecretSelector-ceed9496.js +113 -0
- flowfile/web/static/assets/{SecretManager-4839be57.js → SecretsView-214d255a.js} +35 -36
- flowfile/web/static/assets/SecretsView-aa291340.css +38 -0
- flowfile/web/static/assets/{Select-9b72f201.js → Select-8fc29999.js} +9 -7
- 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-7ded385d.js → SettingsSection-3f70e4c3.js} +3 -3
- flowfile/web/static/assets/{SettingsSection-f0f75a42.js → SettingsSection-83090218.js} +3 -3
- flowfile/web/static/assets/{SettingsSection-2e4d03c4.css → SettingsSection-8f980839.css} +4 -4
- flowfile/web/static/assets/{SettingsSection-e1e9c953.js → SettingsSection-9f0d1725.js} +3 -3
- flowfile/web/static/assets/SetupView-3fa0aa03.js +160 -0
- flowfile/web/static/assets/SetupView-e2da3442.css +230 -0
- flowfile/web/static/assets/{SingleSelect-6c777aac.js → SingleSelect-a4a568cb.js} +2 -2
- flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-33e3ff9b.js → SingleSelect.vue_vue_type_script_setup_true_lang-c8ebdd33.js} +1 -1
- flowfile/web/static/assets/{SliderInput-7cb93e62.js → SliderInput-be533e71.js} +7 -4
- flowfile/web/static/assets/SliderInput-f2e4f23c.css +4 -0
- flowfile/web/static/assets/{Sort-6cbde21a.js → Sort-154dad81.js} +9 -7
- flowfile/web/static/assets/Sort-4abb7fae.css +9 -0
- flowfile/web/static/assets/{TextInput-d9a40c11.js → TextInput-454e2bda.js} +2 -2
- flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-5896c375.js → TextInput.vue_vue_type_script_setup_true_lang-e86510d0.js} +5 -2
- flowfile/web/static/assets/{TextToRows-5d2c1190.css → TextToRows-12afb4f4.css} +10 -10
- flowfile/web/static/assets/{TextToRows-c4fcbf4d.js → TextToRows-ea73433d.js} +11 -10
- flowfile/web/static/assets/{ToggleSwitch-4ef91d19.js → ToggleSwitch-9d7b30f1.js} +2 -2
- flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-38478c20.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-00f2580e.js} +1 -1
- flowfile/web/static/assets/{UnavailableFields-5edd5322.css → UnavailableFields-394a1f78.css} +14 -14
- flowfile/web/static/assets/{UnavailableFields-a03f512c.js → UnavailableFields-b72a2c72.js} +4 -4
- flowfile/web/static/assets/{Union-bfe9b996.js → Union-1e44f263.js} +8 -6
- flowfile/web/static/assets/{Union-af6c3d9b.css → Union-d6a8d7d5.css} +7 -7
- flowfile/web/static/assets/Unique-2b705521.css +3 -0
- flowfile/web/static/assets/{Unique-5d023a27.js → Unique-a3bc6d0a.js} +13 -10
- flowfile/web/static/assets/{Unpivot-1e422df3.css → Unpivot-b6ad6427.css} +7 -7
- flowfile/web/static/assets/{Unpivot-91cc5354.js → Unpivot-e27935fc.js} +11 -9
- flowfile/web/static/assets/{UnpivotValidation-7ee2de44.js → UnpivotValidation-72497680.js} +3 -3
- flowfile/web/static/assets/UnpivotValidation-d5ca3b7b.css +13 -0
- flowfile/web/static/assets/{VueGraphicWalker-ed5ab88b.css → VueGraphicWalker-430f0b86.css} +1 -1
- flowfile/web/static/assets/{VueGraphicWalker-e51b9924.js → VueGraphicWalker-d9ab70a3.js} +4 -4
- flowfile/web/static/assets/{api-cf1221f0.js → api-a2102880.js} +1 -1
- flowfile/web/static/assets/{api-c1bad5ca.js → api-f75042b0.js} +1 -1
- flowfile/web/static/assets/{dropDown-35135ba8.css → dropDown-1d6acbd9.css} +41 -41
- flowfile/web/static/assets/{dropDown-614b998d.js → dropDown-2798a109.js} +3 -3
- flowfile/web/static/assets/{fullEditor-f7971590.js → fullEditor-cf7d7d93.js} +11 -10
- flowfile/web/static/assets/{fullEditor-178376bb.css → fullEditor-fe9f7e18.css} +77 -65
- flowfile/web/static/assets/{genericNodeSettings-4fe5f36b.js → genericNodeSettings-14eac1c3.js} +5 -5
- flowfile/web/static/assets/{genericNodeSettings-924759c7.css → genericNodeSettings-3b2507ea.css} +10 -10
- flowfile/web/static/assets/{index-5429bbf8.js → index-387a6f18.js} +41806 -40958
- flowfile/web/static/assets/index-6b367bb5.js +38 -0
- flowfile/web/static/assets/{index-50508d4d.css → index-e96ab018.css} +2184 -569
- flowfile/web/static/assets/index-f0a6e5a5.js +2696 -0
- flowfile/web/static/assets/node.types-2c15bb7e.js +82 -0
- flowfile/web/static/assets/nodeInput-ed2ae8d7.js +2 -0
- flowfile/web/static/assets/{outputCsv-076b85ab.js → outputCsv-3c1757e8.js} +3 -3
- flowfile/web/static/assets/outputCsv-b9a072af.css +2499 -0
- flowfile/web/static/assets/{outputExcel-0fd17dbe.js → outputExcel-686e1f48.js} +3 -3
- flowfile/web/static/assets/{outputExcel-b41305c0.css → outputExcel-f5d272b2.css} +26 -26
- flowfile/web/static/assets/outputParquet-54597c3c.css +4 -0
- flowfile/web/static/assets/{outputParquet-b61e0847.js → outputParquet-df28faa7.js} +4 -4
- flowfile/web/static/assets/{readCsv-c767cb37.css → readCsv-3bfac4c3.css} +15 -15
- flowfile/web/static/assets/{readCsv-a8bb8b61.js → readCsv-e37eee21.js} +3 -3
- flowfile/web/static/assets/{readExcel-806d2826.css → readExcel-3db6b763.css} +13 -13
- flowfile/web/static/assets/{readExcel-67b4aee0.js → readExcel-a13f14bb.js} +5 -5
- flowfile/web/static/assets/{readParquet-92ce1dbc.js → readParquet-344cf746.js} +3 -3
- flowfile/web/static/assets/{readParquet-48c81530.css → readParquet-c5244ad5.css} +4 -4
- flowfile/web/static/assets/secrets.api-ae198c5c.js +65 -0
- flowfile/web/static/assets/{selectDynamic-92e25ee3.js → selectDynamic-6b4b0767.js} +5 -5
- flowfile/web/static/assets/{selectDynamic-aa913ff4.css → selectDynamic-f2fb394f.css} +21 -20
- flowfile/web/static/assets/{vue-codemirror.esm-41b0e0d7.js → vue-codemirror.esm-31ba0e0b.js} +31 -640
- flowfile/web/static/assets/{vue-content-loader.es-2c8e608f.js → vue-content-loader.es-4469c8ff.js} +1 -1
- flowfile/web/static/index.html +2 -2
- {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/METADATA +3 -4
- flowfile-0.5.4.dist-info/RECORD +407 -0
- flowfile_core/__init__.py +13 -6
- 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 +64 -19
- 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 +145 -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 +26 -24
- flowfile_core/flowfile/analytics/graphic_walker.py +11 -12
- flowfile_core/flowfile/analytics/utils.py +1 -1
- flowfile_core/flowfile/code_generator/__init__.py +11 -0
- flowfile_core/flowfile/code_generator/code_generator.py +706 -247
- 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 +115 -83
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +493 -423
- 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 +31 -20
- 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 +14 -15
- 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 +190 -127
- flowfile_core/flowfile/flow_data_engine/threaded_processes.py +8 -8
- flowfile_core/flowfile/flow_data_engine/utils.py +99 -67
- flowfile_core/flowfile/flow_graph.py +920 -571
- flowfile_core/flowfile/flow_graph_utils.py +31 -49
- flowfile_core/flowfile/flow_node/flow_node.py +379 -258
- flowfile_core/flowfile/flow_node/models.py +53 -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 +80 -30
- flowfile_core/flowfile/manage/compatibility_enhancements.py +209 -126
- flowfile_core/flowfile/manage/io_flowfile.py +54 -57
- flowfile_core/flowfile/node_designer/__init__.py +19 -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 +278 -34
- flowfile_core/flowfile/schema_callbacks.py +71 -51
- 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 +64 -53
- 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 +46 -4
- flowfile_core/routes/routes.py +70 -34
- 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 +96 -66
- flowfile_core/schemas/input_schema.py +231 -144
- flowfile_core/schemas/output_model.py +49 -34
- flowfile_core/schemas/schemas.py +116 -89
- flowfile_core/schemas/transform_schema.py +518 -263
- flowfile_core/schemas/yaml_types.py +21 -7
- flowfile_core/secret_manager/secret_manager.py +123 -18
- flowfile_core/types.py +29 -9
- 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 +117 -51
- 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/database/__init__.py +36 -0
- flowfile_frame/database/connection_manager.py +205 -0
- flowfile_frame/database/frame_helpers.py +249 -0
- flowfile_frame/expr.py +311 -218
- flowfile_frame/expr.pyi +160 -159
- flowfile_frame/expr_name.py +23 -23
- flowfile_frame/flow_frame.py +571 -476
- flowfile_frame/flow_frame.pyi +123 -104
- flowfile_frame/flow_frame_methods.py +227 -246
- 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 -7
- flowfile_worker/configs.py +41 -33
- flowfile_worker/create/__init__.py +14 -9
- flowfile_worker/create/funcs.py +114 -77
- flowfile_worker/create/models.py +46 -43
- 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 -90
- flowfile_worker/secrets.py +114 -21
- flowfile_worker/spawner.py +89 -54
- 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/__init__.py +1 -1
- tools/migrate/__main__.py +16 -29
- tools/migrate/legacy_schemas.py +251 -190
- tools/migrate/migrate.py +193 -181
- tools/migrate/tests/conftest.py +1 -3
- tools/migrate/tests/test_migrate.py +36 -41
- tools/migrate/tests/test_migration_e2e.py +28 -29
- tools/migrate/tests/test_node_migrations.py +50 -20
- flowfile/web/static/assets/CloudConnectionManager-2dfdce2f.css +0 -86
- flowfile/web/static/assets/ContextMenu-23e909da.js +0 -41
- flowfile/web/static/assets/ContextMenu-4c74eef1.css +0 -26
- flowfile/web/static/assets/ContextMenu-63cfa99b.css +0 -26
- flowfile/web/static/assets/ContextMenu-70ae0c79.js +0 -41
- flowfile/web/static/assets/ContextMenu-c13f91d0.css +0 -26
- flowfile/web/static/assets/ContextMenu-f149cf7c.js +0 -41
- flowfile/web/static/assets/CustomNode-74a37f74.css +0 -32
- flowfile/web/static/assets/DatabaseManager-30fa27e5.css +0 -64
- flowfile/web/static/assets/Filter-9b6d08db.js +0 -164
- flowfile/web/static/assets/Filter-f62091b3.css +0 -20
- flowfile/web/static/assets/GroupBy-b9505323.css +0 -51
- flowfile/web/static/assets/ManualInput-3246a08d.css +0 -96
- flowfile/web/static/assets/Output-283fe388.css +0 -37
- flowfile/web/static/assets/PivotValidation-891ddfb0.css +0 -13
- flowfile/web/static/assets/PivotValidation-c46cd420.css +0 -13
- flowfile/web/static/assets/SQLQueryComponent-36cef432.css +0 -27
- flowfile/web/static/assets/SliderInput-b8fb6a8c.css +0 -4
- flowfile/web/static/assets/Sort-3643d625.css +0 -51
- flowfile/web/static/assets/Unique-f9fb0809.css +0 -51
- flowfile/web/static/assets/UnpivotValidation-0d240eeb.css +0 -13
- flowfile/web/static/assets/nodeInput-5d0d6b79.js +0 -41
- flowfile/web/static/assets/outputCsv-9cc59e0b.css +0 -2499
- flowfile/web/static/assets/outputParquet-cf8cf3f2.css +0 -4
- flowfile/web/static/assets/secretApi-68435402.js +0 -46
- flowfile/web/static/assets/vue-codemirror-bccfde04.css +0 -32
- flowfile-0.5.1.dist-info/RECORD +0 -388
- {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/WHEEL +0 -0
- {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/entry_points.txt +0 -0
- {flowfile-0.5.1.dist-info → flowfile-0.5.4.dist-info}/licenses/LICENSE +0 -0
flowfile_core/auth/jwt.py
CHANGED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import secrets
|
|
5
5
|
from datetime import datetime, timedelta
|
|
6
|
-
from typing import Optional
|
|
7
6
|
|
|
8
|
-
from fastapi import APIRouter, Depends, HTTPException,
|
|
7
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|
9
8
|
from fastapi.security import OAuth2PasswordBearer
|
|
10
9
|
from jose import JWTError, jwt
|
|
11
10
|
from sqlalchemy.orm import Session
|
|
12
|
-
from flowfile_core.auth.secrets import get_password, set_password
|
|
13
11
|
|
|
14
|
-
from flowfile_core.auth.models import
|
|
12
|
+
from flowfile_core.auth.models import TokenData, User
|
|
13
|
+
from flowfile_core.auth.secrets import get_password, set_password
|
|
15
14
|
from flowfile_core.database import models as db_models
|
|
16
15
|
from flowfile_core.database.connection import get_db
|
|
17
16
|
|
|
@@ -59,10 +58,10 @@ def get_current_user_sync(token: str, db: Session):
|
|
|
59
58
|
except JWTError:
|
|
60
59
|
raise credentials_exception
|
|
61
60
|
|
|
62
|
-
# In Electron mode, if token is valid, return default user
|
|
61
|
+
# In Electron mode, if token is valid, return default user (always admin in electron mode)
|
|
63
62
|
if os.environ.get("FLOWFILE_MODE") == "electron":
|
|
64
63
|
if token_data.username == "local_user":
|
|
65
|
-
electron_user = User(username="local_user", id=1, disabled=False)
|
|
64
|
+
electron_user = User(username="local_user", id=1, disabled=False, is_admin=True, must_change_password=False)
|
|
66
65
|
return electron_user
|
|
67
66
|
else:
|
|
68
67
|
# Invalid username in token
|
|
@@ -74,7 +73,16 @@ def get_current_user_sync(token: str, db: Session):
|
|
|
74
73
|
raise credentials_exception
|
|
75
74
|
if user.disabled:
|
|
76
75
|
raise HTTPException(status_code=400, detail="Inactive user")
|
|
77
|
-
|
|
76
|
+
# Convert to Pydantic User model
|
|
77
|
+
return User(
|
|
78
|
+
username=user.username,
|
|
79
|
+
id=user.id,
|
|
80
|
+
email=user.email,
|
|
81
|
+
full_name=user.full_name,
|
|
82
|
+
disabled=user.disabled,
|
|
83
|
+
is_admin=user.is_admin,
|
|
84
|
+
must_change_password=user.must_change_password
|
|
85
|
+
)
|
|
78
86
|
|
|
79
87
|
|
|
80
88
|
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
|
|
@@ -98,10 +106,10 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De
|
|
|
98
106
|
except JWTError:
|
|
99
107
|
raise credentials_exception
|
|
100
108
|
|
|
101
|
-
# In Electron mode, if token is valid, return default user
|
|
109
|
+
# In Electron mode, if token is valid, return default user (always admin in electron mode)
|
|
102
110
|
if os.environ.get("FLOWFILE_MODE") == "electron":
|
|
103
111
|
if token_data.username == "local_user":
|
|
104
|
-
electron_user = User(username="local_user", id=1, disabled=False)
|
|
112
|
+
electron_user = User(username="local_user", id=1, disabled=False, is_admin=True, must_change_password=False)
|
|
105
113
|
return electron_user
|
|
106
114
|
else:
|
|
107
115
|
# Invalid username in token
|
|
@@ -113,16 +121,25 @@ async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = De
|
|
|
113
121
|
raise credentials_exception
|
|
114
122
|
if user.disabled:
|
|
115
123
|
raise HTTPException(status_code=400, detail="Inactive user")
|
|
116
|
-
|
|
124
|
+
# Convert to Pydantic User model
|
|
125
|
+
return User(
|
|
126
|
+
username=user.username,
|
|
127
|
+
id=user.id,
|
|
128
|
+
email=user.email,
|
|
129
|
+
full_name=user.full_name,
|
|
130
|
+
disabled=user.disabled,
|
|
131
|
+
is_admin=user.is_admin,
|
|
132
|
+
must_change_password=user.must_change_password
|
|
133
|
+
)
|
|
117
134
|
|
|
118
135
|
|
|
119
136
|
def get_current_active_user(current_user=Depends(get_current_user)):
|
|
120
|
-
if hasattr(current_user,
|
|
137
|
+
if hasattr(current_user, "disabled") and current_user.disabled:
|
|
121
138
|
raise HTTPException(status_code=400, detail="Inactive user")
|
|
122
139
|
return current_user
|
|
123
140
|
|
|
124
141
|
|
|
125
|
-
def create_access_token(data: dict, expires_delta:
|
|
142
|
+
def create_access_token(data: dict, expires_delta: timedelta | None = None):
|
|
126
143
|
to_encode = data.copy()
|
|
127
144
|
|
|
128
145
|
if expires_delta:
|
|
@@ -136,8 +153,7 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|
|
136
153
|
|
|
137
154
|
|
|
138
155
|
async def get_current_user_from_query(
|
|
139
|
-
access_token: str = Query(..., description="JWT access token"),
|
|
140
|
-
db: Session = Depends(get_db)
|
|
156
|
+
access_token: str = Query(..., description="JWT access token"), db: Session = Depends(get_db)
|
|
141
157
|
):
|
|
142
158
|
"""
|
|
143
159
|
Authenticate user using only the query parameter token.
|
|
@@ -165,7 +181,7 @@ async def get_current_user_from_query(
|
|
|
165
181
|
# Handle authentication based on deployment mode (same as your existing logic)
|
|
166
182
|
if os.environ.get("FLOWFILE_MODE") == "electron":
|
|
167
183
|
if token_data.username == "local_user":
|
|
168
|
-
electron_user = User(username="local_user", id=1, disabled=False)
|
|
184
|
+
electron_user = User(username="local_user", id=1, disabled=False, is_admin=True, must_change_password=False)
|
|
169
185
|
return electron_user
|
|
170
186
|
else:
|
|
171
187
|
raise credentials_exception
|
|
@@ -176,4 +192,23 @@ async def get_current_user_from_query(
|
|
|
176
192
|
raise credentials_exception
|
|
177
193
|
if user.disabled:
|
|
178
194
|
raise HTTPException(status_code=400, detail="Inactive user")
|
|
179
|
-
|
|
195
|
+
# Convert to Pydantic User model
|
|
196
|
+
return User(
|
|
197
|
+
username=user.username,
|
|
198
|
+
id=user.id,
|
|
199
|
+
email=user.email,
|
|
200
|
+
full_name=user.full_name,
|
|
201
|
+
disabled=user.disabled,
|
|
202
|
+
is_admin=user.is_admin,
|
|
203
|
+
must_change_password=user.must_change_password
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def get_current_admin_user(current_user: User = Depends(get_current_user)):
|
|
208
|
+
"""Dependency that requires the current user to be an admin"""
|
|
209
|
+
if not current_user.is_admin:
|
|
210
|
+
raise HTTPException(
|
|
211
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
212
|
+
detail="Admin privileges required"
|
|
213
|
+
)
|
|
214
|
+
return current_user
|
flowfile_core/auth/models.py
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
1
|
from pydantic import BaseModel, SecretStr
|
|
3
|
-
from typing import Optional, List
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
class Token(BaseModel):
|
|
@@ -9,21 +7,48 @@ class Token(BaseModel):
|
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
class TokenData(BaseModel):
|
|
12
|
-
username:
|
|
10
|
+
username: str | None = None
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
class User(BaseModel):
|
|
16
14
|
username: str
|
|
17
|
-
id:
|
|
18
|
-
email:
|
|
19
|
-
full_name:
|
|
20
|
-
disabled:
|
|
15
|
+
id: int | None = None
|
|
16
|
+
email: str | None = None
|
|
17
|
+
full_name: str | None = None
|
|
18
|
+
disabled: bool | None = False
|
|
19
|
+
is_admin: bool | None = False
|
|
20
|
+
must_change_password: bool | None = False
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class UserInDB(User):
|
|
24
24
|
hashed_password: str
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class UserCreate(BaseModel):
|
|
28
|
+
"""Model for creating a new user (admin only)"""
|
|
29
|
+
username: str
|
|
30
|
+
password: str
|
|
31
|
+
email: str | None = None
|
|
32
|
+
full_name: str | None = None
|
|
33
|
+
is_admin: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class UserUpdate(BaseModel):
|
|
37
|
+
"""Model for updating a user (admin only)"""
|
|
38
|
+
email: str | None = None
|
|
39
|
+
full_name: str | None = None
|
|
40
|
+
disabled: bool | None = None
|
|
41
|
+
is_admin: bool | None = None
|
|
42
|
+
password: str | None = None # Optional password change
|
|
43
|
+
must_change_password: bool | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ChangePassword(BaseModel):
|
|
47
|
+
"""Model for user changing their own password"""
|
|
48
|
+
current_password: str
|
|
49
|
+
new_password: str
|
|
50
|
+
|
|
51
|
+
|
|
27
52
|
class SecretInput(BaseModel):
|
|
28
53
|
name: str
|
|
29
54
|
value: SecretStr
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Password hashing and verification utilities."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from passlib.context import CryptContext
|
|
5
|
+
|
|
6
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
7
|
+
|
|
8
|
+
# Password requirements
|
|
9
|
+
PASSWORD_MIN_LENGTH = 8
|
|
10
|
+
PASSWORD_REQUIREMENTS = {
|
|
11
|
+
"min_length": PASSWORD_MIN_LENGTH,
|
|
12
|
+
"require_number": True,
|
|
13
|
+
"require_special": True,
|
|
14
|
+
"special_chars": "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PasswordValidationError(Exception):
|
|
19
|
+
"""Raised when password doesn't meet requirements."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_password(password: str) -> tuple[bool, str]:
|
|
24
|
+
"""
|
|
25
|
+
Validate password against security requirements.
|
|
26
|
+
|
|
27
|
+
Requirements:
|
|
28
|
+
- Minimum 8 characters
|
|
29
|
+
- At least one number
|
|
30
|
+
- At least one special character
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
password: The plain text password to validate
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (is_valid, error_message)
|
|
37
|
+
"""
|
|
38
|
+
if len(password) < PASSWORD_MIN_LENGTH:
|
|
39
|
+
return False, f"Password must be at least {PASSWORD_MIN_LENGTH} characters long"
|
|
40
|
+
|
|
41
|
+
if not re.search(r'\d', password):
|
|
42
|
+
return False, "Password must contain at least one number"
|
|
43
|
+
|
|
44
|
+
if not re.search(r'[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]', password):
|
|
45
|
+
return False, "Password must contain at least one special character (!@#$%^&*()_+-=[]{}|;:,.<>?)"
|
|
46
|
+
|
|
47
|
+
return True, ""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_password_or_raise(password: str) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Validate password and raise exception if invalid.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
password: The plain text password to validate
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
PasswordValidationError: If password doesn't meet requirements
|
|
59
|
+
"""
|
|
60
|
+
is_valid, error_message = validate_password(password)
|
|
61
|
+
if not is_valid:
|
|
62
|
+
raise PasswordValidationError(error_message)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def verify_password(plain: str, hashed: str) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Verify a plain password against a hashed password.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
plain: The plain text password
|
|
71
|
+
hashed: The hashed password to verify against
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if the password matches, False otherwise
|
|
75
|
+
"""
|
|
76
|
+
return pwd_context.verify(plain, hashed)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_password_hash(password: str) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Hash a plain text password.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
password: The plain text password to hash
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The hashed password
|
|
88
|
+
"""
|
|
89
|
+
return pwd_context.hash(password)
|
flowfile_core/auth/secrets.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Secure storage module for FlowFile credentials and secrets.
|
|
3
3
|
"""
|
|
4
|
-
|
|
5
|
-
import os
|
|
6
|
-
from pathlib import Path
|
|
4
|
+
|
|
7
5
|
import json
|
|
8
6
|
import logging
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from cryptography.fernet import Fernet
|
|
9
11
|
|
|
10
12
|
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
@@ -15,7 +17,7 @@ class SecureStorage:
|
|
|
15
17
|
|
|
16
18
|
def __init__(self):
|
|
17
19
|
env = os.environ.get("FLOWFILE_MODE")
|
|
18
|
-
logger.debug(f
|
|
20
|
+
logger.debug(f"Using secure storage in {env} mode")
|
|
19
21
|
if os.environ.get("FLOWFILE_MODE") == "electron":
|
|
20
22
|
app_data = os.environ.get("APPDATA") or os.path.expanduser("~/.config")
|
|
21
23
|
self.storage_path = Path(app_data) / "flowfile"
|
|
@@ -135,41 +137,84 @@ def delete_password(service_name, username):
|
|
|
135
137
|
_storage.delete_password(service_name, username)
|
|
136
138
|
|
|
137
139
|
|
|
138
|
-
def get_docker_secret_key():
|
|
140
|
+
def get_docker_secret_key() -> str | None:
|
|
139
141
|
"""
|
|
140
|
-
Get the master key from Docker secret.
|
|
142
|
+
Get the master key from Docker secret or environment variable.
|
|
141
143
|
|
|
142
144
|
Returns:
|
|
143
|
-
str: The master key if successfully read
|
|
145
|
+
str: The master key if successfully read, None if not configured.
|
|
144
146
|
|
|
145
147
|
Raises:
|
|
146
|
-
RuntimeError: If
|
|
148
|
+
RuntimeError: If the secret file exists but cannot be read, or key is invalid.
|
|
147
149
|
"""
|
|
150
|
+
env_key = os.environ.get("FLOWFILE_MASTER_KEY")
|
|
151
|
+
if env_key:
|
|
152
|
+
env_key = env_key.strip().strip('"').strip("'")
|
|
153
|
+
try:
|
|
154
|
+
Fernet(env_key.encode())
|
|
155
|
+
return env_key
|
|
156
|
+
except Exception:
|
|
157
|
+
raise RuntimeError("FLOWFILE_MASTER_KEY is not a valid Fernet key")
|
|
158
|
+
|
|
148
159
|
secret_path = "/run/secrets/flowfile_master_key"
|
|
149
|
-
if os.path.
|
|
160
|
+
if os.path.isfile(secret_path):
|
|
150
161
|
try:
|
|
151
|
-
with open(secret_path
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
with open(secret_path) as f:
|
|
163
|
+
key = f.read().strip()
|
|
164
|
+
Fernet(key.encode())
|
|
165
|
+
return key
|
|
166
|
+
except Exception:
|
|
155
167
|
raise RuntimeError("Failed to read master key from Docker secret")
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def is_master_key_configured() -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Check if the master key is properly configured.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
bool: True if master key is configured and valid, False otherwise.
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
if os.environ.get("FLOWFILE_MODE") == "docker":
|
|
181
|
+
return get_docker_secret_key() is not None
|
|
182
|
+
return True
|
|
183
|
+
except RuntimeError:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def generate_master_key() -> str:
|
|
188
|
+
"""
|
|
189
|
+
Generate a new Fernet master key.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
str: A new valid Fernet encryption key.
|
|
193
|
+
"""
|
|
194
|
+
return Fernet.generate_key().decode()
|
|
159
195
|
|
|
160
196
|
|
|
161
197
|
def get_master_key():
|
|
162
198
|
"""
|
|
163
199
|
Get or generate the master encryption key.
|
|
164
200
|
|
|
165
|
-
If running in Docker, retrieves the key from Docker secrets.
|
|
201
|
+
If running in Docker, retrieves the key from Docker secrets or environment.
|
|
166
202
|
Otherwise, retrieves or generates a key using the secure storage.
|
|
167
203
|
|
|
168
204
|
Returns:
|
|
169
205
|
str: The master encryption key
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
RuntimeError: If in Docker mode and no key is configured.
|
|
170
209
|
"""
|
|
171
|
-
if os.environ.get("
|
|
172
|
-
|
|
210
|
+
if os.environ.get("FLOWFILE_MODE") == "docker":
|
|
211
|
+
key = get_docker_secret_key()
|
|
212
|
+
if key is None:
|
|
213
|
+
raise RuntimeError(
|
|
214
|
+
"Master key not configured. Set FLOWFILE_MASTER_KEY environment variable "
|
|
215
|
+
"or mount the flowfile_master_key Docker secret."
|
|
216
|
+
)
|
|
217
|
+
return key
|
|
173
218
|
|
|
174
219
|
key = get_password("flowfile", "master_key")
|
|
175
220
|
if not key:
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# flowfile_core/flowfile_core/configs/__init__.py
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
5
|
-
import os
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
if "FLOWFILE_MODE" not in os.environ:
|
|
8
|
+
os.environ["FLOWFILE_MODE"] = "electron"
|
|
8
9
|
|
|
9
10
|
# Create and configure the logger
|
|
10
|
-
logger = logging.getLogger(
|
|
11
|
+
logger = logging.getLogger("PipelineHandler")
|
|
11
12
|
logger.setLevel(logging.INFO)
|
|
12
13
|
logger.propagate = False
|
|
13
14
|
|
|
@@ -17,9 +18,9 @@ if logger.hasHandlers():
|
|
|
17
18
|
|
|
18
19
|
# Try to determine the best output stream
|
|
19
20
|
output_stream = None
|
|
20
|
-
if hasattr(sys.stdout,
|
|
21
|
+
if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
|
|
21
22
|
output_stream = sys.stdout
|
|
22
|
-
elif hasattr(sys.stderr,
|
|
23
|
+
elif hasattr(sys.stderr, "isatty") and sys.stderr.isatty():
|
|
23
24
|
output_stream = sys.stderr
|
|
24
25
|
else:
|
|
25
26
|
# Use __stdout__ for debugger environments (PyDev, PyCharm, etc.)
|
|
@@ -29,7 +30,7 @@ console_handler = logging.StreamHandler(output_stream)
|
|
|
29
30
|
console_handler.setLevel(logging.INFO)
|
|
30
31
|
|
|
31
32
|
# Create formatter
|
|
32
|
-
formatter = logging.Formatter(
|
|
33
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
33
34
|
console_handler.setFormatter(formatter)
|
|
34
35
|
|
|
35
36
|
logger.addHandler(console_handler)
|
|
@@ -37,10 +38,11 @@ logger.addHandler(console_handler)
|
|
|
37
38
|
# Create logs directory in temp at startup
|
|
38
39
|
try:
|
|
39
40
|
from tempfile import gettempdir
|
|
41
|
+
|
|
40
42
|
log_dir = Path(gettempdir()) / "flowfile_logs"
|
|
41
43
|
log_dir.mkdir(exist_ok=True)
|
|
42
44
|
except Exception as e:
|
|
43
45
|
logger.warning(f"Failed to create logs directory: {e}")
|
|
44
46
|
|
|
45
47
|
# Initialize vault
|
|
46
|
-
logger.info("Logging system initialized")
|
|
48
|
+
logger.info("Logging system initialized")
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
import os
|
|
5
2
|
import logging.handlers
|
|
3
|
+
import os
|
|
6
4
|
import queue
|
|
7
5
|
import threading
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
8
9
|
from shared.storage_config import storage
|
|
9
10
|
|
|
10
11
|
_process_safe_queue = queue.Queue(-1)
|
|
11
|
-
main_logger = logging.getLogger(
|
|
12
|
+
main_logger = logging.getLogger("PipelineHandler")
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class NodeLogger:
|
|
@@ -38,6 +39,7 @@ class NodeLogger:
|
|
|
38
39
|
|
|
39
40
|
class FlowLogger:
|
|
40
41
|
"""Thread-safe logger for flow execution"""
|
|
42
|
+
|
|
41
43
|
_instances = {}
|
|
42
44
|
_instances_lock = threading.RLock()
|
|
43
45
|
_queue_listener = None
|
|
@@ -47,7 +49,7 @@ class FlowLogger:
|
|
|
47
49
|
def handle_extra_log_info(flow_id: int, extra: dict = None) -> dict:
|
|
48
50
|
if extra is None:
|
|
49
51
|
extra = {}
|
|
50
|
-
extra[
|
|
52
|
+
extra["flow_id"] = flow_id
|
|
51
53
|
return extra
|
|
52
54
|
|
|
53
55
|
def __new__(cls, flow_id: int, clear_existing_logs: bool = False):
|
|
@@ -75,7 +77,7 @@ class FlowLogger:
|
|
|
75
77
|
|
|
76
78
|
def _setup_new_logger(self):
|
|
77
79
|
"""Creates a new logger instance with appropriate handlers"""
|
|
78
|
-
logger_name = f
|
|
80
|
+
logger_name = f"FlowExecution.{self.flow_id}"
|
|
79
81
|
self._logger = logging.getLogger(logger_name)
|
|
80
82
|
self._logger.setLevel(logging.INFO)
|
|
81
83
|
self.setup_logging()
|
|
@@ -131,7 +133,7 @@ class FlowLogger:
|
|
|
131
133
|
|
|
132
134
|
try:
|
|
133
135
|
# Create an empty file
|
|
134
|
-
with open(self.log_file_path,
|
|
136
|
+
with open(self.log_file_path, "w") as f:
|
|
135
137
|
pass
|
|
136
138
|
|
|
137
139
|
# Re-setup the logger
|
|
@@ -154,9 +156,7 @@ class FlowLogger:
|
|
|
154
156
|
"""Start the queue listener for asynchronous logging"""
|
|
155
157
|
queue_handler = logging.handlers.QueueHandler(_process_safe_queue)
|
|
156
158
|
cls._queue_listener = logging.handlers.QueueListener(
|
|
157
|
-
_process_safe_queue,
|
|
158
|
-
queue_handler,
|
|
159
|
-
respect_handler_level=True
|
|
159
|
+
_process_safe_queue, queue_handler, respect_handler_level=True
|
|
160
160
|
)
|
|
161
161
|
cls._queue_listener.start()
|
|
162
162
|
|
|
@@ -193,7 +193,7 @@ class FlowLogger:
|
|
|
193
193
|
|
|
194
194
|
# Add file handler
|
|
195
195
|
file_handler = logging.FileHandler(self.log_file_path)
|
|
196
|
-
formatter = logging.Formatter(
|
|
196
|
+
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
|
|
197
197
|
file_handler.setFormatter(formatter)
|
|
198
198
|
self._logger.addHandler(file_handler)
|
|
199
199
|
|
|
@@ -214,7 +214,8 @@ class FlowLogger:
|
|
|
214
214
|
else:
|
|
215
215
|
# If still can't get lock, proceed anyway
|
|
216
216
|
main_logger.warning(
|
|
217
|
-
f"Could not acquire lock for flow {self.flow_id}, proceeding with file clearing anyway"
|
|
217
|
+
f"Could not acquire lock for flow {self.flow_id}, proceeding with file clearing anyway"
|
|
218
|
+
)
|
|
218
219
|
self._clear_log_impl()
|
|
219
220
|
|
|
220
221
|
def _clear_log_impl(self):
|
|
@@ -223,7 +224,7 @@ class FlowLogger:
|
|
|
223
224
|
# Ensure parent directory exists
|
|
224
225
|
self.refresh_logger_if_needed()
|
|
225
226
|
# Truncate file
|
|
226
|
-
with open(self.log_file_path,
|
|
227
|
+
with open(self.log_file_path, "w") as f:
|
|
227
228
|
pass
|
|
228
229
|
main_logger.info(f"Log file cleared for flow {self.flow_id}")
|
|
229
230
|
except Exception as e:
|
|
@@ -409,7 +410,7 @@ def read_log_from_line(log_file_path: Path, start_line: int = 0):
|
|
|
409
410
|
"""Read log file content starting from a specific line"""
|
|
410
411
|
lines = []
|
|
411
412
|
try:
|
|
412
|
-
with open(log_file_path
|
|
413
|
+
with open(log_file_path) as file:
|
|
413
414
|
# Skip lines efficiently if needed
|
|
414
415
|
if start_line > 0:
|
|
415
416
|
for _ in range(start_line):
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
|
+
|
|
2
3
|
from flowfile_core.configs.node_store.nodes import get_all_standard_nodes
|
|
3
|
-
from flowfile_core.
|
|
4
|
+
from flowfile_core.configs.node_store.user_defined_node_registry import (
|
|
5
|
+
get_all_nodes_from_standard_location,
|
|
6
|
+
load_single_node_from_file,
|
|
7
|
+
unload_node_by_name,
|
|
8
|
+
)
|
|
4
9
|
from flowfile_core.flowfile.node_designer.custom_node import CustomNodeBase
|
|
10
|
+
from flowfile_core.schemas.schemas import NodeTemplate
|
|
5
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
6
13
|
|
|
7
|
-
nodes_with_defaults = {
|
|
14
|
+
nodes_with_defaults = {"sample", "sort", "union", "select", "record_count"}
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
def register_custom_node(node: NodeTemplate):
|
|
@@ -18,6 +25,68 @@ def add_to_custom_node_store(custom_node: type[CustomNodeBase]):
|
|
|
18
25
|
register_custom_node(custom_node().to_node_template())
|
|
19
26
|
|
|
20
27
|
|
|
28
|
+
def remove_from_custom_node_store(node_key: str, file_stem: str = None) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Remove a custom node from both CUSTOM_NODE_STORE and node registries.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
node_key: The key/item name of the node to remove
|
|
34
|
+
file_stem: Optional file name stem (without .py) to use as fallback for matching
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
True if the node was found and removed, False otherwise
|
|
38
|
+
"""
|
|
39
|
+
removed = False
|
|
40
|
+
|
|
41
|
+
logger.info(f"Attempting to remove node with key: '{node_key}' (file_stem: '{file_stem}')")
|
|
42
|
+
logger.info(f"Current CUSTOM_NODE_STORE keys: {list(CUSTOM_NODE_STORE.keys())}")
|
|
43
|
+
logger.info(f"Current nodes_list items: {[n.item for n in nodes_list if hasattr(n, 'item')]}")
|
|
44
|
+
|
|
45
|
+
# Try to find the key - use exact match first, then fallback to file_stem
|
|
46
|
+
actual_key = None
|
|
47
|
+
if node_key in CUSTOM_NODE_STORE:
|
|
48
|
+
actual_key = node_key
|
|
49
|
+
elif file_stem and file_stem in CUSTOM_NODE_STORE:
|
|
50
|
+
actual_key = file_stem
|
|
51
|
+
logger.info(f"Using file_stem '{file_stem}' as key instead of '{node_key}'")
|
|
52
|
+
|
|
53
|
+
# Remove from CUSTOM_NODE_STORE
|
|
54
|
+
if actual_key and actual_key in CUSTOM_NODE_STORE:
|
|
55
|
+
del CUSTOM_NODE_STORE[actual_key]
|
|
56
|
+
logger.info(f"Removed '{actual_key}' from CUSTOM_NODE_STORE")
|
|
57
|
+
removed = True
|
|
58
|
+
else:
|
|
59
|
+
logger.warning(f"Key '{node_key}' (or file_stem '{file_stem}') not found in CUSTOM_NODE_STORE")
|
|
60
|
+
|
|
61
|
+
# Remove from node_dict - try both keys
|
|
62
|
+
key_to_use = actual_key or node_key
|
|
63
|
+
if key_to_use in node_dict:
|
|
64
|
+
del node_dict[key_to_use]
|
|
65
|
+
logger.info(f"Removed '{key_to_use}' from node_dict")
|
|
66
|
+
elif file_stem and file_stem in node_dict:
|
|
67
|
+
del node_dict[file_stem]
|
|
68
|
+
logger.info(f"Removed '{file_stem}' from node_dict")
|
|
69
|
+
|
|
70
|
+
# Remove from nodes_list - try both keys
|
|
71
|
+
removed_from_list = False
|
|
72
|
+
for i, node in enumerate(nodes_list):
|
|
73
|
+
if node.item == key_to_use or (file_stem and node.item == file_stem):
|
|
74
|
+
nodes_list.pop(i)
|
|
75
|
+
logger.info(f"Removed '{node.item}' from nodes_list at index {i}")
|
|
76
|
+
removed_from_list = True
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
if not removed_from_list:
|
|
80
|
+
logger.warning(f"Key '{node_key}' not found in nodes_list")
|
|
81
|
+
|
|
82
|
+
# Clean up module cache
|
|
83
|
+
unload_node_by_name(node_key)
|
|
84
|
+
if file_stem and file_stem != node_key:
|
|
85
|
+
unload_node_by_name(file_stem)
|
|
86
|
+
|
|
87
|
+
return removed
|
|
88
|
+
|
|
89
|
+
|
|
21
90
|
CUSTOM_NODE_STORE = get_all_nodes_from_standard_location()
|
|
22
91
|
nodes_list, node_dict, node_defaults = get_all_standard_nodes()
|
|
23
92
|
|
|
@@ -26,5 +95,4 @@ for custom_node in CUSTOM_NODE_STORE.values():
|
|
|
26
95
|
|
|
27
96
|
|
|
28
97
|
def check_if_has_default_setting(node_item: str):
|
|
29
|
-
|
|
30
98
|
return node_item in nodes_with_defaults
|