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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from typing import List
|
|
2
1
|
from flowfile_core.flowfile.flow_node.flow_node import FlowNode
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
|
|
4
|
+
def determine_nodes_to_skip(nodes: list[FlowNode]) -> list[FlowNode]:
|
|
5
|
+
"""Finds nodes to skip on the execution step."""
|
|
6
6
|
skip_nodes = [node for node in nodes if not node.is_correct]
|
|
7
7
|
skip_nodes.extend([lead_to_node for node in skip_nodes for lead_to_node in node.leads_to_nodes])
|
|
8
|
-
return skip_nodes
|
|
8
|
+
return skip_nodes
|
flowfile_core/flowfile/utils.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import datetime
|
|
2
|
+
import hashlib
|
|
2
3
|
import json
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
3
6
|
import shutil
|
|
4
|
-
|
|
5
|
-
import datetime
|
|
6
|
-
from typing import List
|
|
7
|
-
from decimal import Decimal
|
|
7
|
+
import socket
|
|
8
8
|
import time
|
|
9
|
-
import random
|
|
10
9
|
import uuid
|
|
11
|
-
import
|
|
12
|
-
import hashlib
|
|
10
|
+
from decimal import Decimal
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
def generate_sha256_hash(data: bytes):
|
|
@@ -25,25 +23,25 @@ def create_directory_if_not_exists(directory: str):
|
|
|
25
23
|
|
|
26
24
|
def snake_case_to_camel_case(text: str) -> str:
|
|
27
25
|
# Split the text by underscores, capitalize each piece, and join them together
|
|
28
|
-
transformed_text =
|
|
26
|
+
transformed_text = "".join(word.capitalize() for word in text.split("_"))
|
|
29
27
|
return transformed_text
|
|
30
28
|
|
|
31
29
|
|
|
32
30
|
def json_default(val):
|
|
33
31
|
if isinstance(val, datetime.datetime):
|
|
34
|
-
return val.isoformat(timespec=
|
|
32
|
+
return val.isoformat(timespec="microseconds")
|
|
35
33
|
elif isinstance(val, datetime.date):
|
|
36
34
|
return val.isoformat()
|
|
37
35
|
elif isinstance(val, datetime.time):
|
|
38
36
|
return val.isoformat()
|
|
39
|
-
elif hasattr(val,
|
|
37
|
+
elif hasattr(val, "__dict__"):
|
|
40
38
|
return val.__dict__
|
|
41
39
|
elif isinstance(val, Decimal):
|
|
42
40
|
if val.as_integer_ratio()[1] == 1:
|
|
43
41
|
return int(val)
|
|
44
42
|
return float(val)
|
|
45
43
|
else:
|
|
46
|
-
raise Exception(
|
|
44
|
+
raise Exception("Value is not serializable")
|
|
47
45
|
|
|
48
46
|
|
|
49
47
|
def json_dumps(thing) -> str:
|
|
@@ -53,22 +51,22 @@ def json_dumps(thing) -> str:
|
|
|
53
51
|
ensure_ascii=False,
|
|
54
52
|
sort_keys=True,
|
|
55
53
|
indent=None,
|
|
56
|
-
separators=(
|
|
54
|
+
separators=(",", ":"),
|
|
57
55
|
)
|
|
58
56
|
|
|
59
57
|
|
|
60
58
|
def get_hash(val):
|
|
61
|
-
if hasattr(val,
|
|
59
|
+
if hasattr(val, "overridden_hash") and val.overridden_hash():
|
|
62
60
|
val = hash(val)
|
|
63
|
-
elif hasattr(val,
|
|
64
|
-
val = {k: v for k, v in val.__dict__.items() if k not in {
|
|
65
|
-
elif hasattr(val,
|
|
61
|
+
elif hasattr(val, "__dict__"):
|
|
62
|
+
val = {k: v for k, v in val.__dict__.items() if k not in {"pos_x", "pos_y", "description"}}
|
|
63
|
+
elif hasattr(val, "json"):
|
|
66
64
|
pass
|
|
67
|
-
return generate_sha256_hash(json_dumps(val).encode(
|
|
65
|
+
return generate_sha256_hash(json_dumps(val).encode("utf-8"))
|
|
68
66
|
|
|
69
67
|
|
|
70
|
-
def cleanup(start_location: str =
|
|
71
|
-
def get_all_files_and_folders(_start_location) ->
|
|
68
|
+
def cleanup(start_location: str = "temp_storage"):
|
|
69
|
+
def get_all_files_and_folders(_start_location) -> list[str]:
|
|
72
70
|
inspect_items = [_start_location]
|
|
73
71
|
output = []
|
|
74
72
|
while len(inspect_items) > 0:
|
|
@@ -107,7 +105,7 @@ def cleanup(start_location: str = 'temp_storage'):
|
|
|
107
105
|
shutil.rmtree(_f)
|
|
108
106
|
|
|
109
107
|
|
|
110
|
-
def batch_generator(input_list:
|
|
108
|
+
def batch_generator(input_list: list, batch_size: int = 10000):
|
|
111
109
|
run: bool = True
|
|
112
110
|
while run:
|
|
113
111
|
if len(input_list) > batch_size:
|
flowfile_core/main.py
CHANGED
|
@@ -7,27 +7,34 @@ import uvicorn
|
|
|
7
7
|
from fastapi import FastAPI
|
|
8
8
|
from fastapi.middleware.cors import CORSMiddleware
|
|
9
9
|
|
|
10
|
-
from shared.storage_config import storage
|
|
11
|
-
|
|
12
10
|
from flowfile_core import ServerRun
|
|
13
|
-
from flowfile_core.configs.
|
|
14
|
-
|
|
11
|
+
from flowfile_core.configs.flow_logger import clear_all_flow_logs
|
|
12
|
+
from flowfile_core.configs.settings import (
|
|
13
|
+
SERVER_HOST,
|
|
14
|
+
SERVER_PORT,
|
|
15
|
+
WORKER_HOST,
|
|
16
|
+
WORKER_PORT,
|
|
17
|
+
WORKER_URL,
|
|
18
|
+
)
|
|
15
19
|
from flowfile_core.routes.auth import router as auth_router
|
|
16
|
-
from flowfile_core.routes.secrets import router as secrets_router
|
|
17
|
-
from flowfile_core.routes.routes import router
|
|
18
|
-
from flowfile_core.routes.public import router as public_router
|
|
19
|
-
from flowfile_core.routes.logs import router as logs_router
|
|
20
20
|
from flowfile_core.routes.cloud_connections import router as cloud_connections_router
|
|
21
|
+
from flowfile_core.routes.logs import router as logs_router
|
|
22
|
+
from flowfile_core.routes.public import router as public_router
|
|
23
|
+
from flowfile_core.routes.routes import router
|
|
24
|
+
from flowfile_core.routes.secrets import router as secrets_router
|
|
21
25
|
from flowfile_core.routes.user_defined_components import router as user_defined_components_router
|
|
26
|
+
from shared.storage_config import storage
|
|
22
27
|
|
|
23
|
-
from flowfile_core.configs.flow_logger import clear_all_flow_logs
|
|
24
28
|
storage.cleanup_directories()
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
# Set default mode to electron if not already set (allows Docker mode override)
|
|
31
|
+
if "FLOWFILE_MODE" not in os.environ:
|
|
32
|
+
os.environ["FLOWFILE_MODE"] = "electron"
|
|
27
33
|
|
|
28
34
|
should_exit = False
|
|
29
35
|
server_instance = None
|
|
30
36
|
|
|
37
|
+
|
|
31
38
|
@asynccontextmanager
|
|
32
39
|
async def shutdown_handler(app: FastAPI):
|
|
33
40
|
"""Handles the graceful startup and shutdown of the FastAPI application.
|
|
@@ -35,11 +42,11 @@ async def shutdown_handler(app: FastAPI):
|
|
|
35
42
|
This context manager ensures that resources, such as log files, are cleaned
|
|
36
43
|
up properly when the application is terminated.
|
|
37
44
|
"""
|
|
38
|
-
print(
|
|
45
|
+
print("Starting core application...")
|
|
39
46
|
try:
|
|
40
47
|
yield
|
|
41
48
|
finally:
|
|
42
|
-
print(
|
|
49
|
+
print("Shutting down core application...")
|
|
43
50
|
print("Cleaning up core service resources...")
|
|
44
51
|
clear_all_flow_logs()
|
|
45
52
|
await asyncio.sleep(0.1) # Give a moment for cleanup
|
|
@@ -47,10 +54,10 @@ async def shutdown_handler(app: FastAPI):
|
|
|
47
54
|
|
|
48
55
|
# Initialize FastAPI with metadata
|
|
49
56
|
app = FastAPI(
|
|
50
|
-
title=
|
|
51
|
-
version=
|
|
52
|
-
description=
|
|
53
|
-
lifespan=shutdown_handler
|
|
57
|
+
title="Flowfile Backend",
|
|
58
|
+
version="0.1",
|
|
59
|
+
description="Backend for the Flowfile application",
|
|
60
|
+
lifespan=shutdown_handler,
|
|
54
61
|
)
|
|
55
62
|
|
|
56
63
|
# Configure CORS
|
|
@@ -63,7 +70,7 @@ origins = [
|
|
|
63
70
|
"http://localhost:4173",
|
|
64
71
|
"http://localhost:4174",
|
|
65
72
|
"http://localhost:63578",
|
|
66
|
-
"http://127.0.0.1:63578"
|
|
73
|
+
"http://127.0.0.1:63578",
|
|
67
74
|
]
|
|
68
75
|
|
|
69
76
|
app.add_middleware(
|
|
@@ -148,8 +155,8 @@ def run(host: str = None, port: int = None):
|
|
|
148
155
|
server = uvicorn.Server(config)
|
|
149
156
|
server_instance = server # Store server instance globally
|
|
150
157
|
|
|
151
|
-
print(
|
|
152
|
-
print(
|
|
158
|
+
print("Starting core server...")
|
|
159
|
+
print("Core server started")
|
|
153
160
|
|
|
154
161
|
try:
|
|
155
162
|
server.run()
|
flowfile_core/routes/auth.py
CHANGED
|
@@ -1,34 +1,307 @@
|
|
|
1
1
|
# app_routes/auth.py
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from typing import Optional, List
|
|
4
5
|
|
|
5
|
-
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
|
6
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Request, Form
|
|
6
7
|
from sqlalchemy.orm import Session
|
|
7
8
|
|
|
8
|
-
from flowfile_core.auth.jwt import get_current_active_user, create_access_token
|
|
9
|
-
from flowfile_core.auth.models import Token, User
|
|
9
|
+
from flowfile_core.auth.jwt import get_current_active_user, get_current_admin_user, create_access_token
|
|
10
|
+
from flowfile_core.auth.models import Token, User, UserCreate, UserUpdate, ChangePassword
|
|
11
|
+
from flowfile_core.auth.password import verify_password, get_password_hash, validate_password, PASSWORD_REQUIREMENTS
|
|
10
12
|
from flowfile_core.database.connection import get_db
|
|
13
|
+
from flowfile_core.database import models as db_models
|
|
11
14
|
|
|
12
15
|
router = APIRouter()
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
@router.post("/token", response_model=Token)
|
|
16
|
-
async def login_for_access_token(
|
|
19
|
+
async def login_for_access_token(
|
|
20
|
+
request: Request,
|
|
21
|
+
db: Session = Depends(get_db),
|
|
22
|
+
username: Optional[str] = Form(None),
|
|
23
|
+
password: Optional[str] = Form(None)
|
|
24
|
+
):
|
|
17
25
|
# In Electron mode, auto-authenticate without requiring form data
|
|
18
|
-
if os.environ.get("FLOWFILE_MODE") == "electron"
|
|
26
|
+
if os.environ.get("FLOWFILE_MODE") == "electron":
|
|
19
27
|
access_token = create_access_token(data={"sub": "local_user"})
|
|
20
28
|
return {"access_token": access_token, "token_type": "bearer"}
|
|
21
29
|
else:
|
|
22
30
|
# In Docker mode, authenticate against database
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
if not username or not password:
|
|
32
|
+
raise HTTPException(
|
|
33
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
34
|
+
detail="Incorrect username or password",
|
|
35
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
user = db.query(db_models.User).filter(
|
|
39
|
+
db_models.User.username == username
|
|
40
|
+
).first()
|
|
41
|
+
|
|
42
|
+
if not user or not verify_password(password, user.hashed_password):
|
|
43
|
+
raise HTTPException(
|
|
44
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
45
|
+
detail="Incorrect username or password",
|
|
46
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
access_token = create_access_token(data={"sub": user.username})
|
|
50
|
+
return {"access_token": access_token, "token_type": "bearer"}
|
|
29
51
|
|
|
30
52
|
|
|
31
53
|
# Get current user endpoint
|
|
32
54
|
@router.get("/users/me", response_model=User)
|
|
33
55
|
async def read_users_me(current_user=Depends(get_current_active_user)):
|
|
34
56
|
return current_user
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ============= Admin User Management Endpoints =============
|
|
60
|
+
|
|
61
|
+
@router.get("/users", response_model=List[User])
|
|
62
|
+
async def list_users(
|
|
63
|
+
current_user: User = Depends(get_current_admin_user),
|
|
64
|
+
db: Session = Depends(get_db)
|
|
65
|
+
):
|
|
66
|
+
"""List all users (admin only)"""
|
|
67
|
+
users = db.query(db_models.User).all()
|
|
68
|
+
return [
|
|
69
|
+
User(
|
|
70
|
+
username=u.username,
|
|
71
|
+
id=u.id,
|
|
72
|
+
email=u.email,
|
|
73
|
+
full_name=u.full_name,
|
|
74
|
+
disabled=u.disabled,
|
|
75
|
+
is_admin=u.is_admin,
|
|
76
|
+
must_change_password=u.must_change_password
|
|
77
|
+
)
|
|
78
|
+
for u in users
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.post("/users", response_model=User)
|
|
83
|
+
async def create_user(
|
|
84
|
+
user_data: UserCreate,
|
|
85
|
+
current_user: User = Depends(get_current_admin_user),
|
|
86
|
+
db: Session = Depends(get_db)
|
|
87
|
+
):
|
|
88
|
+
"""Create a new user (admin only)"""
|
|
89
|
+
# Check if username already exists
|
|
90
|
+
existing_user = db.query(db_models.User).filter(
|
|
91
|
+
db_models.User.username == user_data.username
|
|
92
|
+
).first()
|
|
93
|
+
if existing_user:
|
|
94
|
+
raise HTTPException(
|
|
95
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
96
|
+
detail="Username already exists"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Check if email already exists (if provided)
|
|
100
|
+
if user_data.email:
|
|
101
|
+
existing_email = db.query(db_models.User).filter(
|
|
102
|
+
db_models.User.email == user_data.email
|
|
103
|
+
).first()
|
|
104
|
+
if existing_email:
|
|
105
|
+
raise HTTPException(
|
|
106
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
107
|
+
detail="Email already exists"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Validate password requirements
|
|
111
|
+
is_valid, error_message = validate_password(user_data.password)
|
|
112
|
+
if not is_valid:
|
|
113
|
+
raise HTTPException(
|
|
114
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
115
|
+
detail=error_message
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Create new user with must_change_password=True
|
|
119
|
+
hashed_password = get_password_hash(user_data.password)
|
|
120
|
+
new_user = db_models.User(
|
|
121
|
+
username=user_data.username,
|
|
122
|
+
email=user_data.email or f"{user_data.username}@flowfile.app",
|
|
123
|
+
full_name=user_data.full_name,
|
|
124
|
+
hashed_password=hashed_password,
|
|
125
|
+
is_admin=user_data.is_admin,
|
|
126
|
+
must_change_password=True
|
|
127
|
+
)
|
|
128
|
+
db.add(new_user)
|
|
129
|
+
db.commit()
|
|
130
|
+
db.refresh(new_user)
|
|
131
|
+
|
|
132
|
+
return User(
|
|
133
|
+
username=new_user.username,
|
|
134
|
+
id=new_user.id,
|
|
135
|
+
email=new_user.email,
|
|
136
|
+
full_name=new_user.full_name,
|
|
137
|
+
disabled=new_user.disabled,
|
|
138
|
+
is_admin=new_user.is_admin,
|
|
139
|
+
must_change_password=new_user.must_change_password
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@router.put("/users/{user_id}", response_model=User)
|
|
144
|
+
async def update_user(
|
|
145
|
+
user_id: int,
|
|
146
|
+
user_data: UserUpdate,
|
|
147
|
+
current_user: User = Depends(get_current_admin_user),
|
|
148
|
+
db: Session = Depends(get_db)
|
|
149
|
+
):
|
|
150
|
+
"""Update a user (admin only)"""
|
|
151
|
+
user = db.query(db_models.User).filter(db_models.User.id == user_id).first()
|
|
152
|
+
if not user:
|
|
153
|
+
raise HTTPException(
|
|
154
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
155
|
+
detail="User not found"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Prevent admin from disabling themselves
|
|
159
|
+
if user.id == current_user.id and user_data.disabled:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
162
|
+
detail="Cannot disable your own account"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Prevent admin from removing their own admin status
|
|
166
|
+
if user.id == current_user.id and user_data.is_admin is False:
|
|
167
|
+
raise HTTPException(
|
|
168
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
169
|
+
detail="Cannot remove your own admin privileges"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Update fields
|
|
173
|
+
if user_data.email is not None:
|
|
174
|
+
# Check if email already exists for another user
|
|
175
|
+
existing_email = db.query(db_models.User).filter(
|
|
176
|
+
db_models.User.email == user_data.email,
|
|
177
|
+
db_models.User.id != user_id
|
|
178
|
+
).first()
|
|
179
|
+
if existing_email:
|
|
180
|
+
raise HTTPException(
|
|
181
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
182
|
+
detail="Email already exists"
|
|
183
|
+
)
|
|
184
|
+
user.email = user_data.email
|
|
185
|
+
|
|
186
|
+
if user_data.full_name is not None:
|
|
187
|
+
user.full_name = user_data.full_name
|
|
188
|
+
|
|
189
|
+
if user_data.disabled is not None:
|
|
190
|
+
user.disabled = user_data.disabled
|
|
191
|
+
|
|
192
|
+
if user_data.is_admin is not None:
|
|
193
|
+
user.is_admin = user_data.is_admin
|
|
194
|
+
|
|
195
|
+
if user_data.password is not None:
|
|
196
|
+
# Validate password requirements
|
|
197
|
+
is_valid, error_message = validate_password(user_data.password)
|
|
198
|
+
if not is_valid:
|
|
199
|
+
raise HTTPException(
|
|
200
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
201
|
+
detail=error_message
|
|
202
|
+
)
|
|
203
|
+
user.hashed_password = get_password_hash(user_data.password)
|
|
204
|
+
# Reset must_change_password when admin sets a new password
|
|
205
|
+
user.must_change_password = True
|
|
206
|
+
|
|
207
|
+
if user_data.must_change_password is not None:
|
|
208
|
+
user.must_change_password = user_data.must_change_password
|
|
209
|
+
|
|
210
|
+
db.commit()
|
|
211
|
+
db.refresh(user)
|
|
212
|
+
|
|
213
|
+
return User(
|
|
214
|
+
username=user.username,
|
|
215
|
+
id=user.id,
|
|
216
|
+
email=user.email,
|
|
217
|
+
full_name=user.full_name,
|
|
218
|
+
disabled=user.disabled,
|
|
219
|
+
is_admin=user.is_admin,
|
|
220
|
+
must_change_password=user.must_change_password
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@router.delete("/users/{user_id}")
|
|
225
|
+
async def delete_user(
|
|
226
|
+
user_id: int,
|
|
227
|
+
current_user: User = Depends(get_current_admin_user),
|
|
228
|
+
db: Session = Depends(get_db)
|
|
229
|
+
):
|
|
230
|
+
"""Delete a user (admin only)"""
|
|
231
|
+
user = db.query(db_models.User).filter(db_models.User.id == user_id).first()
|
|
232
|
+
if not user:
|
|
233
|
+
raise HTTPException(
|
|
234
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
235
|
+
detail="User not found"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Prevent admin from deleting themselves
|
|
239
|
+
if user.id == current_user.id:
|
|
240
|
+
raise HTTPException(
|
|
241
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
242
|
+
detail="Cannot delete your own account"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Delete user's secrets and connections first (cascade)
|
|
246
|
+
db.query(db_models.Secret).filter(db_models.Secret.user_id == user_id).delete()
|
|
247
|
+
db.query(db_models.DatabaseConnection).filter(db_models.DatabaseConnection.user_id == user_id).delete()
|
|
248
|
+
db.query(db_models.CloudStorageConnection).filter(db_models.CloudStorageConnection.user_id == user_id).delete()
|
|
249
|
+
|
|
250
|
+
db.delete(user)
|
|
251
|
+
db.commit()
|
|
252
|
+
|
|
253
|
+
return {"message": f"User '{user.username}' deleted successfully"}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ============= User Self-Service Endpoints =============
|
|
257
|
+
|
|
258
|
+
@router.post("/users/me/change-password", response_model=User)
|
|
259
|
+
async def change_own_password(
|
|
260
|
+
password_data: ChangePassword,
|
|
261
|
+
current_user: User = Depends(get_current_active_user),
|
|
262
|
+
db: Session = Depends(get_db)
|
|
263
|
+
):
|
|
264
|
+
"""Change the current user's password"""
|
|
265
|
+
user = db.query(db_models.User).filter(db_models.User.id == current_user.id).first()
|
|
266
|
+
if not user:
|
|
267
|
+
raise HTTPException(
|
|
268
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
269
|
+
detail="User not found"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Verify current password
|
|
273
|
+
if not verify_password(password_data.current_password, user.hashed_password):
|
|
274
|
+
raise HTTPException(
|
|
275
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
276
|
+
detail="Current password is incorrect"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Validate new password requirements
|
|
280
|
+
is_valid, error_message = validate_password(password_data.new_password)
|
|
281
|
+
if not is_valid:
|
|
282
|
+
raise HTTPException(
|
|
283
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
284
|
+
detail=error_message
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Update password and clear must_change_password flag
|
|
288
|
+
user.hashed_password = get_password_hash(password_data.new_password)
|
|
289
|
+
user.must_change_password = False
|
|
290
|
+
db.commit()
|
|
291
|
+
db.refresh(user)
|
|
292
|
+
|
|
293
|
+
return User(
|
|
294
|
+
username=user.username,
|
|
295
|
+
id=user.id,
|
|
296
|
+
email=user.email,
|
|
297
|
+
full_name=user.full_name,
|
|
298
|
+
disabled=user.disabled,
|
|
299
|
+
is_admin=user.is_admin,
|
|
300
|
+
must_change_password=user.must_change_password
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@router.get("/password-requirements")
|
|
305
|
+
async def get_password_requirements():
|
|
306
|
+
"""Get password requirements for client-side validation"""
|
|
307
|
+
return PASSWORD_REQUIREMENTS
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
from fastapi import HTTPException, Depends, APIRouter
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
4
2
|
from sqlalchemy.orm import Session
|
|
5
3
|
|
|
6
4
|
# Core modules
|
|
7
5
|
from flowfile_core.auth.jwt import get_current_active_user
|
|
8
6
|
from flowfile_core.configs import logger
|
|
9
7
|
from flowfile_core.database.connection import get_db
|
|
10
|
-
from flowfile_core.flowfile.database_connection_manager.db_connections import (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
from flowfile_core.flowfile.database_connection_manager.db_connections import (
|
|
9
|
+
delete_cloud_connection,
|
|
10
|
+
get_all_cloud_connections_interface,
|
|
11
|
+
get_cloud_connection_schema,
|
|
12
|
+
store_cloud_connection,
|
|
13
|
+
)
|
|
14
|
+
|
|
14
15
|
# Schema and models
|
|
15
16
|
from flowfile_core.schemas.cloud_storage_schemas import FullCloudStorageConnection, FullCloudStorageConnectionInterface
|
|
16
17
|
|
|
@@ -19,11 +20,12 @@ from flowfile_core.schemas.cloud_storage_schemas import FullCloudStorageConnecti
|
|
|
19
20
|
router = APIRouter()
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
@router.post("/cloud_connection", tags=[
|
|
23
|
-
def create_cloud_storage_connection(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
@router.post("/cloud_connection", tags=["cloud_connections"])
|
|
24
|
+
def create_cloud_storage_connection(
|
|
25
|
+
input_connection: FullCloudStorageConnection,
|
|
26
|
+
current_user=Depends(get_current_active_user),
|
|
27
|
+
db: Session = Depends(get_db),
|
|
28
|
+
):
|
|
27
29
|
"""
|
|
28
30
|
Create a new cloud storage connection.
|
|
29
31
|
Parameters
|
|
@@ -33,38 +35,36 @@ def create_cloud_storage_connection(input_connection: FullCloudStorageConnection
|
|
|
33
35
|
Returns
|
|
34
36
|
Dict with a success message
|
|
35
37
|
"""
|
|
36
|
-
logger.info(f
|
|
38
|
+
logger.info(f"Create cloud connection {input_connection.connection_name}")
|
|
37
39
|
try:
|
|
38
40
|
store_cloud_connection(db, input_connection, current_user.id)
|
|
39
41
|
except ValueError:
|
|
40
|
-
raise HTTPException(422,
|
|
42
|
+
raise HTTPException(422, "Connection name already exists")
|
|
41
43
|
except Exception as e:
|
|
42
44
|
logger.error(e)
|
|
43
45
|
raise HTTPException(422, str(e))
|
|
44
46
|
return {"message": "Cloud connection created successfully"}
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
@router.delete(
|
|
48
|
-
def delete_cloud_connection_with_connection_name(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
):
|
|
49
|
+
@router.delete("/cloud_connection", tags=["cloud_connections"])
|
|
50
|
+
def delete_cloud_connection_with_connection_name(
|
|
51
|
+
connection_name: str, current_user=Depends(get_current_active_user), db: Session = Depends(get_db)
|
|
52
|
+
):
|
|
52
53
|
"""
|
|
53
54
|
Delete a cloud connection.
|
|
54
55
|
"""
|
|
55
|
-
logger.info(f
|
|
56
|
+
logger.info(f"Deleting cloud connection {connection_name}")
|
|
56
57
|
cloud_storage_connection = get_cloud_connection_schema(db, connection_name, current_user.id)
|
|
57
58
|
if cloud_storage_connection is None:
|
|
58
|
-
raise HTTPException(404,
|
|
59
|
+
raise HTTPException(404, "Cloud connection connection not found")
|
|
59
60
|
delete_cloud_connection(db, connection_name, current_user.id)
|
|
60
61
|
return {"message": "Cloud connection deleted successfully"}
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
@router.get(
|
|
64
|
-
response_model=List[FullCloudStorageConnectionInterface])
|
|
64
|
+
@router.get("/cloud_connections", tags=["cloud_connection"], response_model=list[FullCloudStorageConnectionInterface])
|
|
65
65
|
def get_cloud_connections(
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
db: Session = Depends(get_db), current_user=Depends(get_current_active_user)
|
|
67
|
+
) -> list[FullCloudStorageConnectionInterface]:
|
|
68
68
|
"""
|
|
69
69
|
Get all cloud storage connections for the current user.
|
|
70
70
|
Parameters
|