Flowfile 0.5.3__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.
- flowfile/__init__.py +16 -0
- flowfile/web/static/assets/{AdminView-49392a9a.js → AdminView-f9847d67.js} +1 -1
- flowfile/web/static/assets/{CloudConnectionView-36bcd6df.css → CloudConnectionView-cf85f943.css} +17 -17
- flowfile/web/static/assets/{CloudConnectionView-f13f202b.js → CloudConnectionView-faace55b.js} +4 -4
- flowfile/web/static/assets/{CloudStorageReader-0023d4a5.js → CloudStorageReader-d86ecaa7.js} +8 -8
- flowfile/web/static/assets/{CloudStorageWriter-8e781e11.js → CloudStorageWriter-0f4d9a44.js} +8 -8
- flowfile/web/static/assets/ColumnActionInput-c44b7aee.css +159 -0
- flowfile/web/static/assets/ColumnActionInput-f4189ae0.js +330 -0
- flowfile/web/static/assets/{ColumnSelector-8ad68ea9.js → ColumnSelector-e66b33da.js} +1 -1
- 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-03df6938.js → CrossJoin-24694b8f.js} +9 -9
- flowfile/web/static/assets/{CustomNode-8479239b.js → CustomNode-569d45ff.js} +27 -20
- flowfile/web/static/assets/CustomNode-edb9b939.css +42 -0
- flowfile/web/static/assets/{DatabaseConnectionSettings-e91df89a.css → DatabaseConnectionSettings-c20a1e16.css} +22 -20
- flowfile/web/static/assets/{DatabaseConnectionSettings-869e3efd.js → DatabaseConnectionSettings-cfc08938.js} +4 -4
- flowfile/web/static/assets/{DatabaseReader-36898a00.css → DatabaseReader-5bf8c75b.css} +39 -44
- flowfile/web/static/assets/{DatabaseReader-c58b9552.js → DatabaseReader-701feabb.js} +13 -13
- flowfile/web/static/assets/{DatabaseView-d26a9140.js → DatabaseView-0482e5b5.js} +2 -2
- flowfile/web/static/assets/{DatabaseWriter-4d05ddc7.js → DatabaseWriter-16721989.js} +12 -12
- flowfile/web/static/assets/{DatabaseWriter-217a99f1.css → DatabaseWriter-bdcf2c8b.css} +27 -25
- flowfile/web/static/assets/{DesignerView-a6d0ee84.css → DesignerView-49abb835.css} +391 -339
- flowfile/web/static/assets/{DesignerView-e6f5c0e8.js → DesignerView-f64749fb.js} +220 -118
- flowfile/web/static/assets/{DocumentationView-2e78ef1b.js → DocumentationView-61bd2990.js} +3 -3
- flowfile/web/static/assets/{DocumentationView-fd46c656.css → DocumentationView-9ea6e871.css} +9 -9
- flowfile/web/static/assets/{ExploreData-7b54caca.js → ExploreData-e2735b13.js} +7 -7
- flowfile/web/static/assets/{ExternalSource-3fa399b2.js → ExternalSource-2535c3b2.js} +9 -9
- flowfile/web/static/assets/{ExternalSource-47ab05a3.css → ExternalSource-7ac7373f.css} +17 -17
- flowfile/web/static/assets/{Filter-8cbbdbf3.js → Filter-2cdbc93c.js} +9 -9
- flowfile/web/static/assets/{Formula-aac42b1e.js → Formula-fcda3c2c.js} +9 -9
- flowfile/web/static/assets/{FuzzyMatch-cd9bbfca.js → FuzzyMatch-f8d3b7d3.js} +10 -10
- flowfile/web/static/assets/{GraphSolver-c24dec17.css → GraphSolver-4b4d7db9.css} +4 -4
- flowfile/web/static/assets/{GraphSolver-c7e6780e.js → GraphSolver-72eaa695.js} +11 -11
- flowfile/web/static/assets/GroupBy-5792782d.css +9 -0
- flowfile/web/static/assets/{GroupBy-93c5d22b.js → GroupBy-8aa0598b.js} +9 -9
- flowfile/web/static/assets/{Join-a19b2de2.js → Join-e40f0ffa.js} +10 -10
- flowfile/web/static/assets/{LoginView-0df4ed0a.js → LoginView-5111c9ae.js} +1 -1
- flowfile/web/static/assets/{ManualInput-8d3374b2.js → ManualInput-9b6f3224.js} +6 -6
- flowfile/web/static/assets/{MultiSelect-ad1b6243.js → MultiSelect-ef28e19e.js} +2 -2
- flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-e278950d.js → MultiSelect.vue_vue_type_script_setup_true_lang-83b3bbfd.js} +1 -1
- flowfile/web/static/assets/{NodeDesigner-5f53be3f.css → NodeDesigner-94cd4dd3.css} +190 -190
- flowfile/web/static/assets/{NodeDesigner-40b647c9.js → NodeDesigner-d2b7ee2b.js} +171 -69
- flowfile/web/static/assets/{NumericInput-7100234c.js → NumericInput-1d789794.js} +2 -2
- flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-5130219f.js → NumericInput.vue_vue_type_script_setup_true_lang-7775f83e.js} +1 -1
- flowfile/web/static/assets/{Output-35e97000.css → Output-692dd25d.css} +10 -10
- flowfile/web/static/assets/{Output-f5efd2aa.js → Output-cefef801.js} +13 -12
- flowfile/web/static/assets/{Pivot-d981d23c.js → Pivot-bab1b75b.js} +10 -10
- flowfile/web/static/assets/{PivotValidation-63de1f73.js → PivotValidation-e7941f91.js} +1 -1
- flowfile/web/static/assets/{PivotValidation-39386e95.js → PivotValidation-fba09336.js} +1 -1
- flowfile/web/static/assets/{PolarsCode-f9d69217.js → PolarsCode-740e40fa.js} +7 -7
- flowfile/web/static/assets/{PopOver-b22f049e.js → PopOver-862d7e28.js} +1 -1
- flowfile/web/static/assets/{Read-aec2e377.js → Read-225cc63f.js} +15 -14
- flowfile/web/static/assets/{Read-36e7bd51.css → Read-90f366bc.css} +13 -13
- flowfile/web/static/assets/{RecordCount-78ed6845.js → RecordCount-ffc71eca.js} +6 -6
- flowfile/web/static/assets/{RecordId-2156e890.js → RecordId-a70bb8df.js} +9 -9
- flowfile/web/static/assets/{SQLQueryComponent-48c72f5b.js → SQLQueryComponent-15a421f5.js} +3 -3
- flowfile/web/static/assets/SQLQueryComponent-edb90b98.css +29 -0
- flowfile/web/static/assets/{Sample-1352ca74.js → Sample-6c26afc7.js} +6 -6
- flowfile/web/static/assets/{SecretSelector-22b5ff89.js → SecretSelector-ceed9496.js} +2 -2
- flowfile/web/static/assets/{SecretsView-17df66ee.js → SecretsView-214d255a.js} +2 -2
- flowfile/web/static/assets/{Select-0aee4c54.js → Select-8fc29999.js} +9 -9
- flowfile/web/static/assets/{SettingsSection-f2002a6d.js → SettingsSection-3f70e4c3.js} +1 -1
- flowfile/web/static/assets/{SettingsSection-0784e157.js → SettingsSection-83090218.js} +1 -1
- flowfile/web/static/assets/{SettingsSection-cd341bb6.js → SettingsSection-9f0d1725.js} +1 -1
- flowfile/web/static/assets/SetupView-3fa0aa03.js +160 -0
- flowfile/web/static/assets/SetupView-e2da3442.css +230 -0
- flowfile/web/static/assets/{SingleSelect-460cc0ea.js → SingleSelect-a4a568cb.js} +2 -2
- flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-30741bb2.js → SingleSelect.vue_vue_type_script_setup_true_lang-c8ebdd33.js} +1 -1
- flowfile/web/static/assets/{SliderInput-5d926864.js → SliderInput-be533e71.js} +1 -1
- flowfile/web/static/assets/{Sort-3cdc971b.js → Sort-154dad81.js} +9 -9
- flowfile/web/static/assets/Sort-4abb7fae.css +9 -0
- flowfile/web/static/assets/{TextInput-a2d0bfbd.js → TextInput-454e2bda.js} +2 -2
- flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-abad1ca2.js → TextInput.vue_vue_type_script_setup_true_lang-e86510d0.js} +1 -1
- flowfile/web/static/assets/{TextToRows-918945f7.js → TextToRows-ea73433d.js} +9 -9
- flowfile/web/static/assets/{ToggleSwitch-f0ef5196.js → ToggleSwitch-9d7b30f1.js} +2 -2
- flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-5605c793.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-00f2580e.js} +1 -1
- flowfile/web/static/assets/{UnavailableFields-54d2f518.css → UnavailableFields-394a1f78.css} +13 -13
- flowfile/web/static/assets/{UnavailableFields-bdad6144.js → UnavailableFields-b72a2c72.js} +4 -4
- flowfile/web/static/assets/{Union-e8ab8c86.js → Union-1e44f263.js} +6 -6
- flowfile/web/static/assets/Unique-2b705521.css +3 -0
- flowfile/web/static/assets/{Unique-8cd4f976.js → Unique-a3bc6d0a.js} +12 -12
- flowfile/web/static/assets/{Unpivot-710a2948.css → Unpivot-b6ad6427.css} +6 -6
- flowfile/web/static/assets/{Unpivot-8da14095.js → Unpivot-e27935fc.js} +11 -11
- flowfile/web/static/assets/{UnpivotValidation-6f7d89ff.js → UnpivotValidation-72497680.js} +1 -1
- flowfile/web/static/assets/{VueGraphicWalker-3fb312e1.js → VueGraphicWalker-d9ab70a3.js} +1 -1
- flowfile/web/static/assets/{api-24483f0d.js → api-a2102880.js} +1 -1
- flowfile/web/static/assets/{api-8b81fa73.js → api-f75042b0.js} +1 -1
- flowfile/web/static/assets/{dropDown-3d8dc5fa.css → dropDown-1d6acbd9.css} +26 -26
- flowfile/web/static/assets/{dropDown-ac0fda9d.js → dropDown-2798a109.js} +3 -3
- flowfile/web/static/assets/{fullEditor-5497a84a.js → fullEditor-cf7d7d93.js} +3 -3
- flowfile/web/static/assets/{fullEditor-a0be62b3.css → fullEditor-fe9f7e18.css} +3 -3
- flowfile/web/static/assets/{genericNodeSettings-99014e1d.js → genericNodeSettings-14eac1c3.js} +3 -3
- flowfile/web/static/assets/{index-fb6493ae.js → index-387a6f18.js} +175 -86
- flowfile/web/static/assets/{index-07dda503.js → index-6b367bb5.js} +1 -1
- flowfile/web/static/assets/{index-e6289dd0.css → index-e96ab018.css} +249 -10
- flowfile/web/static/assets/{index-3ba44389.js → index-f0a6e5a5.js} +2 -2
- flowfile/web/static/assets/nodeInput-ed2ae8d7.js +2 -0
- flowfile/web/static/assets/{outputCsv-8f8ba42d.js → outputCsv-3c1757e8.js} +1 -1
- flowfile/web/static/assets/{outputExcel-393f4fef.js → outputExcel-686e1f48.js} +1 -1
- flowfile/web/static/assets/{outputParquet-07c81f65.js → outputParquet-df28faa7.js} +1 -1
- flowfile/web/static/assets/{readCsv-07f6d9ad.js → readCsv-e37eee21.js} +1 -1
- flowfile/web/static/assets/{readExcel-ed69bc8f.js → readExcel-a13f14bb.js} +3 -3
- flowfile/web/static/assets/{readParquet-e3ed4528.js → readParquet-344cf746.js} +1 -1
- flowfile/web/static/assets/{secrets.api-002e7d7e.js → secrets.api-ae198c5c.js} +1 -1
- flowfile/web/static/assets/{selectDynamic-80b92899.js → selectDynamic-6b4b0767.js} +3 -3
- flowfile/web/static/assets/{vue-codemirror.esm-0965f39f.js → vue-codemirror.esm-31ba0e0b.js} +1 -1
- flowfile/web/static/assets/{vue-content-loader.es-c506ad97.js → vue-content-loader.es-4469c8ff.js} +1 -1
- flowfile/web/static/index.html +2 -2
- {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/METADATA +2 -2
- {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/RECORD +134 -129
- flowfile_core/auth/secrets.py +56 -13
- flowfile_core/fileExplorer/funcs.py +26 -4
- flowfile_core/flowfile/code_generator/__init__.py +11 -0
- flowfile_core/flowfile/code_generator/code_generator.py +347 -2
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +13 -1
- flowfile_core/flowfile/flow_graph.py +2 -0
- flowfile_core/flowfile/flow_node/flow_node.py +52 -28
- flowfile_core/flowfile/node_designer/__init__.py +4 -0
- flowfile_core/flowfile/node_designer/ui_components.py +144 -1
- flowfile_core/routes/public.py +43 -1
- flowfile_core/schemas/cloud_storage_schemas.py +39 -15
- flowfile_core/secret_manager/secret_manager.py +107 -6
- flowfile_frame/__init__.py +11 -0
- flowfile_frame/database/__init__.py +36 -0
- flowfile_frame/database/connection_manager.py +205 -0
- flowfile_frame/database/frame_helpers.py +249 -0
- flowfile_worker/configs.py +31 -15
- flowfile_worker/secrets.py +105 -15
- flowfile_worker/spawner.py +10 -6
- flowfile/web/static/assets/ContextMenu-26d4dd27.css +0 -26
- flowfile/web/static/assets/ContextMenu-31ee57f0.js +0 -41
- flowfile/web/static/assets/ContextMenu-69a74055.js +0 -41
- flowfile/web/static/assets/ContextMenu-8e2051c6.js +0 -41
- flowfile/web/static/assets/ContextMenu-8ec1729e.css +0 -26
- flowfile/web/static/assets/ContextMenu-9b310c60.css +0 -26
- flowfile/web/static/assets/CustomNode-59e99a86.css +0 -32
- flowfile/web/static/assets/GroupBy-be7ac0bf.css +0 -51
- flowfile/web/static/assets/SQLQueryComponent-1c2f26b4.css +0 -27
- flowfile/web/static/assets/Sort-8a871341.css +0 -51
- flowfile/web/static/assets/Unique-9fb2f567.css +0 -51
- flowfile/web/static/assets/nodeInput-0eb13f1a.js +0 -2
- {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/WHEEL +0 -0
- {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/entry_points.txt +0 -0
- {flowfile-0.5.3.dist-info → flowfile-0.5.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -15,7 +15,9 @@ from .custom_node import CustomNodeBase, NodeSettings
|
|
|
15
15
|
|
|
16
16
|
# Import all UI components so they can be used directly
|
|
17
17
|
from .ui_components import (
|
|
18
|
+
ActionOption,
|
|
18
19
|
AvailableSecrets,
|
|
20
|
+
ColumnActionInput,
|
|
19
21
|
ColumnSelector,
|
|
20
22
|
IncomingColumns,
|
|
21
23
|
MultiSelect,
|
|
@@ -42,6 +44,8 @@ __all__ = [
|
|
|
42
44
|
"MultiSelect",
|
|
43
45
|
"NodeSettings",
|
|
44
46
|
"ColumnSelector",
|
|
47
|
+
"ColumnActionInput",
|
|
48
|
+
"ActionOption",
|
|
45
49
|
"IncomingColumns",
|
|
46
50
|
"AvailableSecrets",
|
|
47
51
|
"SecretSelector",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Literal
|
|
1
|
+
from typing import Any, Literal, NamedTuple
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field, SecretStr, computed_field
|
|
4
4
|
|
|
@@ -11,6 +11,29 @@ from flowfile_core.types import DataType, TypeSpec
|
|
|
11
11
|
InputType = Literal["text", "number", "secret", "array", "date", "boolean"]
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class ActionOption(NamedTuple):
|
|
15
|
+
"""
|
|
16
|
+
A named tuple representing an action option with a value and display label.
|
|
17
|
+
|
|
18
|
+
Use this to define actions with custom labels in ColumnActionInput:
|
|
19
|
+
actions=[
|
|
20
|
+
ActionOption("sum", "Sum"),
|
|
21
|
+
ActionOption("avg", "Average"),
|
|
22
|
+
"count" # plain strings also work
|
|
23
|
+
]
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
value: str
|
|
27
|
+
"""The internal value used in the data."""
|
|
28
|
+
|
|
29
|
+
label: str
|
|
30
|
+
"""The display label shown in the UI."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Type alias for action specifications - accepts strings or ActionOption tuples
|
|
34
|
+
ActionSpec = str | ActionOption
|
|
35
|
+
|
|
36
|
+
|
|
14
37
|
def normalize_input_to_data_types(v: Any) -> Literal["ALL"] | list[DataType]:
|
|
15
38
|
"""
|
|
16
39
|
Normalizes a wide variety of inputs to either 'ALL' or a sorted list of DataType enums.
|
|
@@ -376,3 +399,123 @@ class SecretSelector(FlowfileInComponent):
|
|
|
376
399
|
if self.name_prefix:
|
|
377
400
|
data["name_prefix"] = self.name_prefix
|
|
378
401
|
return data
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class ColumnActionInput(FlowfileInComponent):
|
|
405
|
+
"""
|
|
406
|
+
A generic UI component for configuring column-based transformations.
|
|
407
|
+
|
|
408
|
+
This component allows users to select columns, choose an action/transformation,
|
|
409
|
+
and optionally rename the output. It can be configured for many use cases:
|
|
410
|
+
rolling windows, aggregations, string transformations, type conversions, etc.
|
|
411
|
+
|
|
412
|
+
The component displays:
|
|
413
|
+
- A list of available columns (filterable by data type)
|
|
414
|
+
- A table of configured operations with: Column, Action, Output Name
|
|
415
|
+
- Optional group by and order by selectors
|
|
416
|
+
|
|
417
|
+
Example - Rolling Window:
|
|
418
|
+
ColumnActionInput(
|
|
419
|
+
label="Rolling Calculations",
|
|
420
|
+
actions=["sum", "mean", "min", "max"],
|
|
421
|
+
output_name_template="{column}_rolling_{action}",
|
|
422
|
+
show_group_by=True,
|
|
423
|
+
show_order_by=True,
|
|
424
|
+
data_types="Numeric"
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
Example - String Transformations:
|
|
428
|
+
ColumnActionInput(
|
|
429
|
+
label="String Operations",
|
|
430
|
+
actions=["upper", "lower", "trim", "reverse"],
|
|
431
|
+
output_name_template="{column}_{action}",
|
|
432
|
+
data_types="String"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
Example - Aggregations with ActionOption:
|
|
436
|
+
ColumnActionInput(
|
|
437
|
+
label="Aggregations",
|
|
438
|
+
actions=[
|
|
439
|
+
ActionOption("sum", "Sum"),
|
|
440
|
+
ActionOption("count", "Count"),
|
|
441
|
+
ActionOption("mean", "Average"),
|
|
442
|
+
"min", # plain strings also work
|
|
443
|
+
],
|
|
444
|
+
output_name_template="{column}_{action}",
|
|
445
|
+
show_group_by=True
|
|
446
|
+
)
|
|
447
|
+
"""
|
|
448
|
+
|
|
449
|
+
component_type: Literal["ColumnActionInput"] = "ColumnActionInput"
|
|
450
|
+
input_type: InputType = "array"
|
|
451
|
+
|
|
452
|
+
# Configurable actions - list of action names or ActionOption tuples
|
|
453
|
+
actions: list[ActionSpec] = Field(default_factory=list)
|
|
454
|
+
"""Actions available for selection. Can be strings or ActionOption(value, label) tuples."""
|
|
455
|
+
|
|
456
|
+
# Template for auto-generating output names
|
|
457
|
+
# Supports placeholders: {column}, {action}
|
|
458
|
+
output_name_template: str = "{column}_{action}"
|
|
459
|
+
"""Template for generating default output names. Use {column} and {action} placeholders."""
|
|
460
|
+
|
|
461
|
+
# Optional grouping/ordering support
|
|
462
|
+
show_group_by: bool = False
|
|
463
|
+
"""Whether to show the group by column selector."""
|
|
464
|
+
|
|
465
|
+
show_order_by: bool = False
|
|
466
|
+
"""Whether to show the order by column selector."""
|
|
467
|
+
|
|
468
|
+
# Type filtering for column selection
|
|
469
|
+
data_type_filter_input: TypeSpec = Field(default="ALL", alias="data_types", repr=False, exclude=True)
|
|
470
|
+
"""Filter columns by data type. Defaults to ALL."""
|
|
471
|
+
|
|
472
|
+
class Config:
|
|
473
|
+
arbitrary_types_allowed = True
|
|
474
|
+
|
|
475
|
+
def __init__(self, **data):
|
|
476
|
+
super().__init__(**data)
|
|
477
|
+
# Initialize value if not set
|
|
478
|
+
if self.value is None:
|
|
479
|
+
self.value = {
|
|
480
|
+
"rows": [],
|
|
481
|
+
"group_by_columns": [],
|
|
482
|
+
"order_by_column": None,
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
def set_value(self, value: Any):
|
|
486
|
+
"""
|
|
487
|
+
Sets the value from frontend.
|
|
488
|
+
"""
|
|
489
|
+
self.value = value
|
|
490
|
+
return self
|
|
491
|
+
|
|
492
|
+
@computed_field
|
|
493
|
+
@property
|
|
494
|
+
def data_types_filter(self) -> Literal["ALL"] | list[DataType]:
|
|
495
|
+
"""
|
|
496
|
+
A computed field that normalizes the `data_type_filter_input` into a
|
|
497
|
+
standardized format for the frontend.
|
|
498
|
+
"""
|
|
499
|
+
return normalize_input_to_data_types(self.data_type_filter_input)
|
|
500
|
+
|
|
501
|
+
def model_dump(self, **kwargs) -> dict:
|
|
502
|
+
"""
|
|
503
|
+
Serializes the component for the frontend.
|
|
504
|
+
"""
|
|
505
|
+
data = super().model_dump(**kwargs)
|
|
506
|
+
# Normalize actions to list of {value, label} objects for frontend
|
|
507
|
+
normalized_actions = []
|
|
508
|
+
for action in self.actions:
|
|
509
|
+
if isinstance(action, tuple):
|
|
510
|
+
normalized_actions.append({"value": action[0], "label": action[1]})
|
|
511
|
+
else:
|
|
512
|
+
normalized_actions.append({"value": action, "label": action})
|
|
513
|
+
data["actions"] = normalized_actions
|
|
514
|
+
data["output_name_template"] = self.output_name_template
|
|
515
|
+
data["show_group_by"] = self.show_group_by
|
|
516
|
+
data["show_order_by"] = self.show_order_by
|
|
517
|
+
if "data_types_filter" in data and data["data_types_filter"] != "ALL":
|
|
518
|
+
data["data_types"] = sorted([dt.value for dt in data["data_types_filter"]])
|
|
519
|
+
else:
|
|
520
|
+
data["data_types"] = "ALL"
|
|
521
|
+
return data
|
flowfile_core/routes/public.py
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
1
3
|
from fastapi import APIRouter
|
|
2
4
|
from fastapi.responses import RedirectResponse
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from flowfile_core.auth.secrets import generate_master_key, is_master_key_configured
|
|
3
8
|
|
|
4
|
-
# Router setup
|
|
5
9
|
router = APIRouter()
|
|
6
10
|
|
|
7
11
|
|
|
12
|
+
class SetupStatus(BaseModel):
|
|
13
|
+
"""Response model for the setup status endpoint."""
|
|
14
|
+
|
|
15
|
+
setup_required: bool
|
|
16
|
+
master_key_configured: bool
|
|
17
|
+
mode: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GeneratedKey(BaseModel):
|
|
21
|
+
"""Response model for the generate key endpoint."""
|
|
22
|
+
|
|
23
|
+
key: str
|
|
24
|
+
instructions: str
|
|
25
|
+
|
|
26
|
+
|
|
8
27
|
@router.get("/", tags=["admin"])
|
|
9
28
|
async def docs_redirect():
|
|
10
29
|
"""Redirects to the documentation page."""
|
|
11
30
|
return RedirectResponse(url="/docs")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get("/health/status", response_model=SetupStatus, tags=["health"])
|
|
34
|
+
async def get_setup_status():
|
|
35
|
+
"""Get the current setup status of the application."""
|
|
36
|
+
mode = os.environ.get("FLOWFILE_MODE", "electron")
|
|
37
|
+
master_key_ok = is_master_key_configured()
|
|
38
|
+
return SetupStatus(
|
|
39
|
+
setup_required=not master_key_ok,
|
|
40
|
+
master_key_configured=master_key_ok,
|
|
41
|
+
mode=mode,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.post("/setup/generate-key", response_model=GeneratedKey, tags=["setup"])
|
|
46
|
+
async def generate_key():
|
|
47
|
+
"""Generate a new master encryption key."""
|
|
48
|
+
key = generate_master_key()
|
|
49
|
+
instructions = (
|
|
50
|
+
f'Add to your .env file:\n FLOWFILE_MASTER_KEY="{key}"\n\n'
|
|
51
|
+
"Then restart: docker-compose down && docker-compose up"
|
|
52
|
+
)
|
|
53
|
+
return GeneratedKey(key=key, instructions=instructions)
|
|
@@ -14,14 +14,20 @@ AuthMethod = Literal[
|
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def encrypt_for_worker(secret_value: SecretStr | None) -> str | None:
|
|
17
|
+
def encrypt_for_worker(secret_value: SecretStr | None, user_id: int) -> str | None:
|
|
18
18
|
"""
|
|
19
|
-
Encrypts a secret value for use in worker contexts.
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
Encrypts a secret value for use in worker contexts using per-user key derivation.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
secret_value: The secret value to encrypt
|
|
23
|
+
user_id: The user ID for key derivation
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Encrypted secret with embedded user_id, or None if secret_value is None
|
|
22
27
|
"""
|
|
23
28
|
if secret_value is not None:
|
|
24
|
-
return encrypt_secret(secret_value.get_secret_value())
|
|
29
|
+
return encrypt_secret(secret_value.get_secret_value(), user_id)
|
|
30
|
+
return None
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
class AuthSettingsInput(BaseModel):
|
|
@@ -80,25 +86,31 @@ class FullCloudStorageConnection(AuthSettingsInput):
|
|
|
80
86
|
endpoint_url: str | None = None
|
|
81
87
|
verify_ssl: bool = True
|
|
82
88
|
|
|
83
|
-
def get_worker_interface(self) -> "FullCloudStorageConnectionWorkerInterface":
|
|
89
|
+
def get_worker_interface(self, user_id: int) -> "FullCloudStorageConnectionWorkerInterface":
|
|
84
90
|
"""
|
|
85
|
-
Convert to a
|
|
91
|
+
Convert to a worker interface model with encrypted secrets.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
user_id: The user ID for per-user key derivation
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
FullCloudStorageConnectionWorkerInterface with encrypted secrets
|
|
86
98
|
"""
|
|
87
99
|
return FullCloudStorageConnectionWorkerInterface(
|
|
88
100
|
storage_type=self.storage_type,
|
|
89
101
|
auth_method=self.auth_method,
|
|
90
102
|
connection_name=self.connection_name,
|
|
91
103
|
aws_allow_unsafe_html=self.aws_allow_unsafe_html,
|
|
92
|
-
aws_secret_access_key=encrypt_for_worker(self.aws_secret_access_key),
|
|
104
|
+
aws_secret_access_key=encrypt_for_worker(self.aws_secret_access_key, user_id),
|
|
93
105
|
aws_region=self.aws_region,
|
|
94
106
|
aws_access_key_id=self.aws_access_key_id,
|
|
95
107
|
aws_role_arn=self.aws_role_arn,
|
|
96
|
-
aws_session_token=encrypt_for_worker(self.aws_session_token),
|
|
108
|
+
aws_session_token=encrypt_for_worker(self.aws_session_token, user_id),
|
|
97
109
|
azure_account_name=self.azure_account_name,
|
|
98
110
|
azure_tenant_id=self.azure_tenant_id,
|
|
99
|
-
azure_account_key=encrypt_for_worker(self.azure_account_key),
|
|
111
|
+
azure_account_key=encrypt_for_worker(self.azure_account_key, user_id),
|
|
100
112
|
azure_client_id=self.azure_client_id,
|
|
101
|
-
azure_client_secret=encrypt_for_worker(self.azure_client_secret),
|
|
113
|
+
azure_client_secret=encrypt_for_worker(self.azure_client_secret, user_id),
|
|
102
114
|
endpoint_url=self.endpoint_url,
|
|
103
115
|
verify_ssl=self.verify_ssl,
|
|
104
116
|
)
|
|
@@ -202,18 +214,30 @@ def get_cloud_storage_write_settings_worker_interface(
|
|
|
202
214
|
write_settings: CloudStorageWriteSettings,
|
|
203
215
|
connection: FullCloudStorageConnection,
|
|
204
216
|
lf: pl.LazyFrame,
|
|
217
|
+
user_id: int,
|
|
205
218
|
flowfile_flow_id: int = 1,
|
|
206
219
|
flowfile_node_id: int | str = -1,
|
|
207
220
|
) -> CloudStorageWriteSettingsWorkerInterface:
|
|
208
221
|
"""
|
|
209
|
-
Convert to a worker interface model with
|
|
222
|
+
Convert to a worker interface model with encrypted secrets.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
write_settings: Cloud storage write settings
|
|
226
|
+
connection: Full cloud storage connection with secrets
|
|
227
|
+
lf: LazyFrame to serialize
|
|
228
|
+
user_id: User ID for per-user key derivation
|
|
229
|
+
flowfile_flow_id: Flow ID for tracking
|
|
230
|
+
flowfile_node_id: Node ID for tracking
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
CloudStorageWriteSettingsWorkerInterface ready for worker
|
|
210
234
|
"""
|
|
211
235
|
operation = base64.b64encode(lf.serialize()).decode()
|
|
212
236
|
|
|
213
237
|
return CloudStorageWriteSettingsWorkerInterface(
|
|
214
238
|
operation=operation,
|
|
215
239
|
write_settings=write_settings.get_write_setting_worker_interface(),
|
|
216
|
-
connection=connection.get_worker_interface(),
|
|
217
|
-
flowfile_flow_id=flowfile_flow_id,
|
|
218
|
-
flowfile_node_id=flowfile_node_id,
|
|
240
|
+
connection=connection.get_worker_interface(user_id),
|
|
241
|
+
flowfile_flow_id=flowfile_flow_id,
|
|
242
|
+
flowfile_node_id=flowfile_node_id,
|
|
219
243
|
)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
|
|
1
3
|
from cryptography.fernet import Fernet
|
|
4
|
+
from cryptography.hazmat.primitives import hashes
|
|
5
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
2
6
|
from fastapi.exceptions import HTTPException
|
|
3
7
|
from pydantic import SecretStr
|
|
4
8
|
from sqlalchemy import and_
|
|
@@ -9,21 +13,118 @@ from flowfile_core.auth.secrets import get_master_key
|
|
|
9
13
|
from flowfile_core.database import models as db_models
|
|
10
14
|
from flowfile_core.database.connection import get_db_context
|
|
11
15
|
|
|
16
|
+
# Version identifier for key derivation scheme (allows future migrations)
|
|
17
|
+
KEY_DERIVATION_VERSION = b"flowfile-secrets-v1"
|
|
18
|
+
|
|
19
|
+
# Encrypted secret format: $ffsec$1${user_id}${fernet_token}
|
|
20
|
+
# This embeds the user_id so the worker can derive the correct key
|
|
21
|
+
SECRET_FORMAT_PREFIX = "$ffsec$1$"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def derive_user_key(user_id: int) -> bytes:
|
|
25
|
+
"""
|
|
26
|
+
Derive a user-specific encryption key from the master key using HKDF.
|
|
27
|
+
|
|
28
|
+
This provides cryptographic isolation between users - each user's secrets
|
|
29
|
+
are encrypted with a unique key derived from the master key.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
user_id: The unique identifier for the user
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
bytes: A 32-byte URL-safe base64-encoded key suitable for Fernet
|
|
36
|
+
"""
|
|
37
|
+
master_key = get_master_key().encode()
|
|
12
38
|
|
|
13
|
-
|
|
14
|
-
|
|
39
|
+
# Use HKDF to derive a user-specific key
|
|
40
|
+
hkdf = HKDF(
|
|
41
|
+
algorithm=hashes.SHA256(),
|
|
42
|
+
length=32, # Fernet requires 32 bytes
|
|
43
|
+
salt=KEY_DERIVATION_VERSION, # Static salt is fine for key derivation
|
|
44
|
+
info=f"user-{user_id}".encode(), # User-specific context
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Derive raw key material and encode for Fernet
|
|
48
|
+
derived_key = hkdf.derive(master_key)
|
|
49
|
+
return base64.urlsafe_b64encode(derived_key)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _encrypt_with_master_key(secret_value: str) -> str:
|
|
53
|
+
"""Legacy encryption using master key directly (for backward compatibility)."""
|
|
15
54
|
key = get_master_key().encode()
|
|
16
55
|
f = Fernet(key)
|
|
17
56
|
return f.encrypt(secret_value.encode()).decode()
|
|
18
57
|
|
|
19
58
|
|
|
20
|
-
def
|
|
21
|
-
"""
|
|
59
|
+
def _decrypt_with_master_key(encrypted_value: str) -> SecretStr:
|
|
60
|
+
"""Legacy decryption using master key directly (for backward compatibility)."""
|
|
22
61
|
key = get_master_key().encode()
|
|
23
62
|
f = Fernet(key)
|
|
24
63
|
return SecretStr(f.decrypt(encrypted_value.encode()).decode())
|
|
25
64
|
|
|
26
65
|
|
|
66
|
+
def encrypt_secret(secret_value: str, user_id: int) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Encrypt a secret value using a user-specific derived key.
|
|
69
|
+
|
|
70
|
+
The encrypted format embeds the user_id so it can be decrypted
|
|
71
|
+
without knowing the user context (e.g., by the worker service).
|
|
72
|
+
|
|
73
|
+
Format: $ffsec$1${user_id}${fernet_token}
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
secret_value: The plaintext secret to encrypt
|
|
77
|
+
user_id: The user ID to derive the encryption key for
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
str: The encrypted value with embedded user_id
|
|
81
|
+
"""
|
|
82
|
+
key = derive_user_key(user_id)
|
|
83
|
+
f = Fernet(key)
|
|
84
|
+
fernet_token = f.encrypt(secret_value.encode()).decode()
|
|
85
|
+
return f"{SECRET_FORMAT_PREFIX}{user_id}${fernet_token}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def decrypt_secret(encrypted_value: str, user_id: int | None = None) -> SecretStr:
|
|
89
|
+
"""
|
|
90
|
+
Decrypt an encrypted value.
|
|
91
|
+
|
|
92
|
+
Supports both new format (with embedded user_id) and legacy format.
|
|
93
|
+
- New format: $ffsec$1${user_id}${fernet_token} - user_id extracted automatically
|
|
94
|
+
- Legacy format: raw Fernet token - requires user_id parameter or uses master key
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
encrypted_value: The encrypted secret
|
|
98
|
+
user_id: Optional user ID (required for legacy secrets, ignored for new format)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
SecretStr: The decrypted value wrapped in SecretStr
|
|
102
|
+
"""
|
|
103
|
+
# Check for new versioned format with embedded user_id
|
|
104
|
+
if encrypted_value.startswith(SECRET_FORMAT_PREFIX):
|
|
105
|
+
# Parse: $ffsec$1${user_id}${fernet_token}
|
|
106
|
+
remainder = encrypted_value[len(SECRET_FORMAT_PREFIX):]
|
|
107
|
+
parts = remainder.split("$", 1)
|
|
108
|
+
if len(parts) != 2:
|
|
109
|
+
raise ValueError("Invalid encrypted secret format")
|
|
110
|
+
|
|
111
|
+
embedded_user_id = int(parts[0])
|
|
112
|
+
fernet_token = parts[1]
|
|
113
|
+
|
|
114
|
+
key = derive_user_key(embedded_user_id)
|
|
115
|
+
f = Fernet(key)
|
|
116
|
+
return SecretStr(f.decrypt(fernet_token.encode()).decode())
|
|
117
|
+
|
|
118
|
+
# Legacy format - use provided user_id or fall back to master key
|
|
119
|
+
if user_id is not None:
|
|
120
|
+
key = derive_user_key(user_id)
|
|
121
|
+
f = Fernet(key)
|
|
122
|
+
return SecretStr(f.decrypt(encrypted_value.encode()).decode())
|
|
123
|
+
|
|
124
|
+
# Fall back to master key for legacy secrets without user context
|
|
125
|
+
return _decrypt_with_master_key(encrypted_value)
|
|
126
|
+
|
|
127
|
+
|
|
27
128
|
def get_encrypted_secret(current_user_id: int, secret_name: str) -> str | None:
|
|
28
129
|
with get_db_context() as db:
|
|
29
130
|
user_id = current_user_id
|
|
@@ -39,13 +140,13 @@ def get_encrypted_secret(current_user_id: int, secret_name: str) -> str | None:
|
|
|
39
140
|
|
|
40
141
|
|
|
41
142
|
def store_secret(db: Session, secret: SecretInput, user_id: int) -> db_models.Secret:
|
|
42
|
-
encrypted_value = encrypt_secret(secret.value.get_secret_value())
|
|
143
|
+
encrypted_value = encrypt_secret(secret.value.get_secret_value(), user_id)
|
|
43
144
|
|
|
44
145
|
# Store in database
|
|
45
146
|
db_secret = db_models.Secret(
|
|
46
147
|
name=secret.name,
|
|
47
148
|
encrypted_value=encrypted_value,
|
|
48
|
-
iv="", #
|
|
149
|
+
iv="", # Legacy field, not used with current encryption
|
|
49
150
|
user_id=user_id,
|
|
50
151
|
)
|
|
51
152
|
db.add(db_secret)
|
flowfile_frame/__init__.py
CHANGED
|
@@ -54,6 +54,17 @@ from flowfile_frame.cloud_storage.secret_manager import (
|
|
|
54
54
|
get_all_available_cloud_storage_connections,
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
+
# Database I/O
|
|
58
|
+
from flowfile_frame.database import (
|
|
59
|
+
create_database_connection,
|
|
60
|
+
create_database_connection_if_not_exists,
|
|
61
|
+
del_database_connection,
|
|
62
|
+
get_all_available_database_connections,
|
|
63
|
+
get_database_connection_by_name,
|
|
64
|
+
read_database,
|
|
65
|
+
write_database,
|
|
66
|
+
)
|
|
67
|
+
|
|
57
68
|
# Commonly used functions
|
|
58
69
|
from flowfile_frame.expr import ( # noqa: F401
|
|
59
70
|
col,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Database module for flowfile_frame.
|
|
2
|
+
|
|
3
|
+
This module provides functions for:
|
|
4
|
+
- Managing database connections (create, list, delete)
|
|
5
|
+
- Reading from databases
|
|
6
|
+
- Writing to databases
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from flowfile_frame.database.connection_manager import (
|
|
10
|
+
create_database_connection,
|
|
11
|
+
create_database_connection_if_not_exists,
|
|
12
|
+
del_database_connection,
|
|
13
|
+
get_all_available_database_connections,
|
|
14
|
+
get_database_connection_by_name,
|
|
15
|
+
)
|
|
16
|
+
from flowfile_frame.database.frame_helpers import (
|
|
17
|
+
add_read_from_database,
|
|
18
|
+
add_write_to_database,
|
|
19
|
+
read_database,
|
|
20
|
+
write_database,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Connection management
|
|
25
|
+
"create_database_connection",
|
|
26
|
+
"create_database_connection_if_not_exists",
|
|
27
|
+
"del_database_connection",
|
|
28
|
+
"get_all_available_database_connections",
|
|
29
|
+
"get_database_connection_by_name",
|
|
30
|
+
# FlowGraph helpers
|
|
31
|
+
"add_read_from_database",
|
|
32
|
+
"add_write_to_database",
|
|
33
|
+
# Direct read/write
|
|
34
|
+
"read_database",
|
|
35
|
+
"write_database",
|
|
36
|
+
]
|