Flowfile 0.3.5__py3-none-any.whl → 0.3.7__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.
Potentially problematic release.
This version of Flowfile might be problematic. Click here for more details.
- flowfile/__init__.py +27 -6
- flowfile/api.py +1 -0
- flowfile/web/__init__.py +2 -2
- flowfile/web/static/assets/CloudConnectionManager-2dfdce2f.css +86 -0
- flowfile/web/static/assets/CloudConnectionManager-c20a740f.js +783 -0
- flowfile/web/static/assets/CloudStorageReader-29d14fcc.css +143 -0
- flowfile/web/static/assets/CloudStorageReader-960b400a.js +437 -0
- flowfile/web/static/assets/CloudStorageWriter-49c9a4b2.css +138 -0
- flowfile/web/static/assets/CloudStorageWriter-e3decbdd.js +430 -0
- flowfile/web/static/assets/{CrossJoin-dfcf7351.js → CrossJoin-d67e2405.js} +8 -8
- flowfile/web/static/assets/{DatabaseConnectionSettings-b2afb1d7.js → DatabaseConnectionSettings-a81e0f7e.js} +2 -2
- flowfile/web/static/assets/{DatabaseManager-824a49b2.js → DatabaseManager-9ea35e84.js} +2 -2
- flowfile/web/static/assets/{DatabaseReader-a48124d8.js → DatabaseReader-9578bfa5.js} +9 -9
- flowfile/web/static/assets/{DatabaseWriter-b47cbae2.js → DatabaseWriter-19531098.js} +9 -9
- flowfile/web/static/assets/{ExploreData-fdfc45a4.js → ExploreData-40476474.js} +47141 -43697
- flowfile/web/static/assets/{ExternalSource-861b0e71.js → ExternalSource-2297ef96.js} +6 -6
- flowfile/web/static/assets/{Filter-f87bb897.js → Filter-f211c03a.js} +8 -8
- flowfile/web/static/assets/{Formula-b8cefc31.css → Formula-29f19d21.css} +10 -0
- flowfile/web/static/assets/{Formula-1e2ed720.js → Formula-4207ea31.js} +75 -9
- flowfile/web/static/assets/{FuzzyMatch-b6cc4fdd.js → FuzzyMatch-bf120df0.js} +9 -9
- flowfile/web/static/assets/{GraphSolver-6a371f4c.js → GraphSolver-5bb7497a.js} +5 -5
- flowfile/web/static/assets/{GroupBy-f7b7f472.js → GroupBy-92c81b65.js} +6 -6
- flowfile/web/static/assets/{Join-eec38203.js → Join-4e49a274.js} +23 -15
- flowfile/web/static/assets/{Join-41c0f331.css → Join-f45eff22.css} +20 -20
- flowfile/web/static/assets/{ManualInput-9aaa46fb.js → ManualInput-90998ae8.js} +106 -34
- flowfile/web/static/assets/{ManualInput-ac7b9972.css → ManualInput-a71b52c6.css} +29 -17
- flowfile/web/static/assets/{Output-3b2ca045.js → Output-81e3e917.js} +4 -4
- flowfile/web/static/assets/{Pivot-a4f5d88f.js → Pivot-a3419842.js} +6 -6
- flowfile/web/static/assets/{PolarsCode-49ce444f.js → PolarsCode-72710deb.js} +6 -6
- flowfile/web/static/assets/{Read-07acdc9a.js → Read-c4059daf.js} +6 -6
- flowfile/web/static/assets/{RecordCount-6a21da56.js → RecordCount-c2b5e095.js} +5 -5
- flowfile/web/static/assets/{RecordId-949bdc17.js → RecordId-10baf191.js} +6 -6
- flowfile/web/static/assets/{Sample-7afca6e1.js → Sample-3ed9a0ae.js} +5 -5
- flowfile/web/static/assets/{SecretManager-b41c029d.js → SecretManager-0d49c0e8.js} +2 -2
- flowfile/web/static/assets/{Select-32b28406.js → Select-8a02a0b3.js} +8 -8
- flowfile/web/static/assets/{SettingsSection-a0f15a05.js → SettingsSection-4c0f45f5.js} +1 -1
- flowfile/web/static/assets/{Sort-fc6ba0e2.js → Sort-f55c9f9d.js} +6 -6
- flowfile/web/static/assets/{TextToRows-23127596.js → TextToRows-5dbc2145.js} +8 -8
- flowfile/web/static/assets/{UnavailableFields-c42880a3.js → UnavailableFields-a1768e52.js} +2 -2
- flowfile/web/static/assets/{Union-39eecc6c.js → Union-f2aefdc9.js} +5 -5
- flowfile/web/static/assets/{Unique-a0e8fe61.js → Unique-46b250da.js} +8 -8
- flowfile/web/static/assets/{Unpivot-1e2d43f0.js → Unpivot-25ac84cc.js} +5 -5
- flowfile/web/static/assets/api-6ef0dcef.js +80 -0
- flowfile/web/static/assets/{api-44ca9e9c.js → api-a0abbdc7.js} +1 -1
- flowfile/web/static/assets/cloud_storage_reader-aa1415d6.png +0 -0
- flowfile/web/static/assets/{designer-267d44f1.js → designer-13eabd83.js} +36 -34
- flowfile/web/static/assets/{documentation-6c0810a2.js → documentation-b87e7f6f.js} +1 -1
- flowfile/web/static/assets/{dropDown-52790b15.js → dropDown-13564764.js} +1 -1
- flowfile/web/static/assets/{fullEditor-e272b506.js → fullEditor-fd2cd6f9.js} +2 -2
- flowfile/web/static/assets/{genericNodeSettings-4bdcf98e.js → genericNodeSettings-71e11604.js} +3 -3
- flowfile/web/static/assets/{index-e235a8bc.js → index-f6c15e76.js} +59 -22
- flowfile/web/static/assets/{nodeTitle-fc3fc4b7.js → nodeTitle-988d9efe.js} +3 -3
- flowfile/web/static/assets/{secretApi-cdc2a3fd.js → secretApi-dd636aa2.js} +1 -1
- flowfile/web/static/assets/{selectDynamic-96aa82cd.js → selectDynamic-af36165e.js} +3 -3
- flowfile/web/static/assets/{vue-codemirror.esm-25e75a08.js → vue-codemirror.esm-2847001e.js} +2 -1
- flowfile/web/static/assets/{vue-content-loader.es-6c4b1c24.js → vue-content-loader.es-0371da73.js} +1 -1
- flowfile/web/static/index.html +1 -1
- {flowfile-0.3.5.dist-info → flowfile-0.3.7.dist-info}/METADATA +9 -4
- {flowfile-0.3.5.dist-info → flowfile-0.3.7.dist-info}/RECORD +131 -124
- {flowfile-0.3.5.dist-info → flowfile-0.3.7.dist-info}/entry_points.txt +2 -0
- flowfile_core/__init__.py +3 -0
- flowfile_core/auth/jwt.py +39 -0
- flowfile_core/configs/node_store/nodes.py +9 -6
- flowfile_core/configs/settings.py +6 -5
- flowfile_core/database/connection.py +63 -15
- flowfile_core/database/init_db.py +0 -1
- flowfile_core/database/models.py +49 -2
- flowfile_core/flowfile/code_generator/code_generator.py +472 -17
- flowfile_core/flowfile/connection_manager/models.py +1 -1
- flowfile_core/flowfile/database_connection_manager/db_connections.py +216 -2
- flowfile_core/flowfile/extensions.py +1 -1
- flowfile_core/flowfile/flow_data_engine/cloud_storage_reader.py +259 -0
- flowfile_core/flowfile/flow_data_engine/create/funcs.py +19 -8
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +1062 -311
- flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +12 -2
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/settings_validator.py +1 -1
- flowfile_core/flowfile/flow_data_engine/join/__init__.py +2 -1
- flowfile_core/flowfile/flow_data_engine/join/utils.py +25 -0
- flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +3 -1
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +29 -22
- flowfile_core/flowfile/flow_data_engine/utils.py +1 -40
- flowfile_core/flowfile/flow_graph.py +718 -253
- flowfile_core/flowfile/flow_graph_utils.py +2 -2
- flowfile_core/flowfile/flow_node/flow_node.py +563 -117
- flowfile_core/flowfile/flow_node/models.py +154 -20
- flowfile_core/flowfile/flow_node/schema_callback.py +3 -2
- flowfile_core/flowfile/handler.py +2 -33
- flowfile_core/flowfile/manage/open_flowfile.py +1 -2
- flowfile_core/flowfile/sources/external_sources/__init__.py +0 -2
- flowfile_core/flowfile/sources/external_sources/factory.py +4 -7
- flowfile_core/flowfile/util/calculate_layout.py +0 -2
- flowfile_core/flowfile/utils.py +35 -26
- flowfile_core/main.py +35 -15
- flowfile_core/routes/cloud_connections.py +77 -0
- flowfile_core/routes/logs.py +2 -7
- flowfile_core/routes/public.py +1 -0
- flowfile_core/routes/routes.py +130 -90
- flowfile_core/routes/secrets.py +72 -14
- flowfile_core/schemas/__init__.py +8 -0
- flowfile_core/schemas/cloud_storage_schemas.py +215 -0
- flowfile_core/schemas/input_schema.py +121 -71
- flowfile_core/schemas/output_model.py +19 -3
- flowfile_core/schemas/schemas.py +150 -12
- flowfile_core/schemas/transform_schema.py +175 -35
- flowfile_core/utils/utils.py +40 -1
- flowfile_core/utils/validate_setup.py +41 -0
- flowfile_frame/__init__.py +9 -1
- flowfile_frame/cloud_storage/frame_helpers.py +39 -0
- flowfile_frame/cloud_storage/secret_manager.py +73 -0
- flowfile_frame/expr.py +28 -1
- flowfile_frame/expr.pyi +76 -61
- flowfile_frame/flow_frame.py +481 -208
- flowfile_frame/flow_frame.pyi +140 -91
- flowfile_frame/flow_frame_methods.py +160 -22
- flowfile_frame/group_frame.py +3 -0
- flowfile_frame/utils.py +25 -3
- flowfile_worker/external_sources/s3_source/main.py +216 -0
- flowfile_worker/external_sources/s3_source/models.py +142 -0
- flowfile_worker/funcs.py +51 -6
- flowfile_worker/models.py +22 -2
- flowfile_worker/routes.py +40 -38
- flowfile_worker/utils.py +1 -1
- test_utils/s3/commands.py +46 -0
- test_utils/s3/data_generator.py +292 -0
- test_utils/s3/demo_data_generator.py +186 -0
- test_utils/s3/fixtures.py +214 -0
- flowfile/web/static/assets/AirbyteReader-1ac35765.css +0 -314
- flowfile/web/static/assets/AirbyteReader-e08044e5.js +0 -922
- flowfile/web/static/assets/dropDownGeneric-60f56a8a.js +0 -72
- flowfile/web/static/assets/dropDownGeneric-895680d6.css +0 -10
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/airbyte.py +0 -159
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/models.py +0 -172
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/settings.py +0 -173
- flowfile_core/schemas/defaults.py +0 -9
- flowfile_core/schemas/external_sources/airbyte_schemas.py +0 -20
- flowfile_core/schemas/models.py +0 -193
- flowfile_worker/external_sources/airbyte_sources/cache_manager.py +0 -161
- flowfile_worker/external_sources/airbyte_sources/main.py +0 -89
- flowfile_worker/external_sources/airbyte_sources/models.py +0 -133
- flowfile_worker/external_sources/airbyte_sources/settings.py +0 -0
- {flowfile-0.3.5.dist-info → flowfile-0.3.7.dist-info}/LICENSE +0 -0
- {flowfile-0.3.5.dist-info → flowfile-0.3.7.dist-info}/WHEEL +0 -0
- {flowfile_core/flowfile/sources/external_sources/airbyte_sources → flowfile_frame/cloud_storage}/__init__.py +0 -0
- {flowfile_core/schemas/external_sources → flowfile_worker/external_sources/s3_source}/__init__.py +0 -0
- {flowfile_worker/external_sources/airbyte_sources → test_utils/s3}/__init__.py +0 -0
flowfile_core/routes/secrets.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Manages CRUD (Create, Read, Update, Delete) operations for secrets.
|
|
2
3
|
|
|
4
|
+
This router provides secure endpoints for creating, retrieving, and deleting
|
|
5
|
+
sensitive credentials for the authenticated user. Secrets are encrypted before
|
|
6
|
+
being stored and are associated with the user's ID.
|
|
7
|
+
"""
|
|
3
8
|
import os
|
|
4
9
|
from typing import List
|
|
5
10
|
|
|
@@ -10,20 +15,31 @@ from flowfile_core.auth.jwt import get_current_active_user
|
|
|
10
15
|
from flowfile_core.auth.models import Secret, SecretInput
|
|
11
16
|
from flowfile_core.database import models as db_models
|
|
12
17
|
from flowfile_core.database.connection import get_db
|
|
13
|
-
from flowfile_core.secret_manager.secret_manager import
|
|
18
|
+
from flowfile_core.secret_manager.secret_manager import store_secret, delete_secret as delete_secret_action
|
|
14
19
|
|
|
15
20
|
router = APIRouter(dependencies=[Depends(get_current_active_user)])
|
|
16
21
|
|
|
17
22
|
|
|
18
|
-
# Get all secrets for current user
|
|
19
23
|
@router.get("/secrets", response_model=List[Secret])
|
|
20
24
|
async def get_secrets(current_user=Depends(get_current_active_user), db: Session = Depends(get_db)):
|
|
25
|
+
"""Retrieves all secret names for the currently authenticated user.
|
|
26
|
+
|
|
27
|
+
Note: This endpoint returns the secret names and metadata but does not
|
|
28
|
+
expose the decrypted secret values.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
current_user: The authenticated user object, injected by FastAPI.
|
|
32
|
+
db: The database session, injected by FastAPI.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
A list of `Secret` objects, each containing the name and encrypted value.
|
|
36
|
+
"""
|
|
21
37
|
user_id = current_user.id
|
|
22
38
|
|
|
23
39
|
# Get secrets from database
|
|
24
40
|
db_secrets = db.query(db_models.Secret).filter(db_models.Secret.user_id == user_id).all()
|
|
25
41
|
|
|
26
|
-
#
|
|
42
|
+
# Prepare response model (without decrypting)
|
|
27
43
|
secrets = []
|
|
28
44
|
for db_secret in db_secrets:
|
|
29
45
|
secrets.append(Secret(
|
|
@@ -35,13 +51,27 @@ async def get_secrets(current_user=Depends(get_current_active_user), db: Session
|
|
|
35
51
|
return secrets
|
|
36
52
|
|
|
37
53
|
|
|
38
|
-
# Create a new secret
|
|
39
54
|
@router.post("/secrets", response_model=Secret)
|
|
40
55
|
async def create_secret(secret: SecretInput, current_user=Depends(get_current_active_user),
|
|
41
|
-
db: Session = Depends(get_db)):
|
|
42
|
-
|
|
56
|
+
db: Session = Depends(get_db)) -> Secret:
|
|
57
|
+
"""Creates a new secret for the authenticated user.
|
|
58
|
+
|
|
59
|
+
The secret value is encrypted before being stored in the database. A secret
|
|
60
|
+
name must be unique for a given user.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
secret: A `SecretInput` object containing the name and plaintext value of the secret.
|
|
64
|
+
current_user: The authenticated user object, injected by FastAPI.
|
|
65
|
+
db: The database session, injected by FastAPI.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
HTTPException: 400 if a secret with the same name already exists for the user.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A `Secret` object containing the name and the *encrypted* value.
|
|
72
|
+
"""
|
|
43
73
|
# Get user ID
|
|
44
|
-
user_id = 1 if os.environ.get("FLOWFILE_MODE") == "electron"
|
|
74
|
+
user_id = 1 if os.environ.get("FLOWFILE_MODE") == "electron" else current_user.id
|
|
45
75
|
|
|
46
76
|
existing_secret = db.query(db_models.Secret).filter(
|
|
47
77
|
db_models.Secret.user_id == user_id,
|
|
@@ -51,13 +81,30 @@ async def create_secret(secret: SecretInput, current_user=Depends(get_current_ac
|
|
|
51
81
|
if existing_secret:
|
|
52
82
|
raise HTTPException(status_code=400, detail="Secret with this name already exists")
|
|
53
83
|
|
|
54
|
-
|
|
55
|
-
|
|
84
|
+
# The store_secret function handles encryption and DB storage
|
|
85
|
+
stored_secret = store_secret(db, secret, user_id)
|
|
86
|
+
return Secret(name=stored_secret.name, value=stored_secret.encrypted_value, user_id=str(user_id))
|
|
56
87
|
|
|
57
88
|
|
|
58
|
-
# Get a specific secret by name
|
|
59
89
|
@router.get("/secrets/{secret_name}", response_model=Secret)
|
|
60
|
-
async def get_secret(secret_name: str,
|
|
90
|
+
async def get_secret(secret_name: str,
|
|
91
|
+
current_user=Depends(get_current_active_user), db: Session = Depends(get_db)) -> Secret:
|
|
92
|
+
"""Retrieves a specific secret by name for the authenticated user.
|
|
93
|
+
|
|
94
|
+
Note: This endpoint returns the secret name and metadata but does not
|
|
95
|
+
expose the decrypted secret value.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
secret_name: The name of the secret to retrieve.
|
|
99
|
+
current_user: The authenticated user object, injected by FastAPI.
|
|
100
|
+
db: The database session, injected by FastAPI.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
HTTPException: 404 if the secret is not found.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A `Secret` object containing the name and encrypted value.
|
|
107
|
+
"""
|
|
61
108
|
# Get user ID
|
|
62
109
|
user_id = 1 if os.environ.get("FLOWFILE_MODE") == "electron" else current_user.id
|
|
63
110
|
|
|
@@ -78,8 +125,19 @@ async def get_secret(secret_name: str, current_user=Depends(get_current_active_u
|
|
|
78
125
|
|
|
79
126
|
|
|
80
127
|
@router.delete("/secrets/{secret_name}", status_code=204)
|
|
81
|
-
async def delete_secret(secret_name: str, current_user=Depends(get_current_active_user),
|
|
128
|
+
async def delete_secret(secret_name: str, current_user=Depends(get_current_active_user),
|
|
129
|
+
db: Session = Depends(get_db)) -> None:
|
|
130
|
+
"""Deletes a secret by name for the authenticated user.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
secret_name: The name of the secret to delete.
|
|
134
|
+
current_user: The authenticated user object, injected by FastAPI.
|
|
135
|
+
db: The database session, injected by FastAPI.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
An empty response with a 204 No Content status code upon success.
|
|
139
|
+
"""
|
|
82
140
|
# Get user ID
|
|
83
|
-
user_id = 1 if os.environ.get("FLOWFILE_MODE") == "electron"
|
|
141
|
+
user_id = 1 if os.environ.get("FLOWFILE_MODE") == "electron" else current_user.id
|
|
84
142
|
delete_secret_action(db, secret_name, user_id)
|
|
85
143
|
return None
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from flowfile_core.schemas import input_schema as node_interface, transform_schema as transformation_settings
|
|
2
|
+
from flowfile_core.schemas.schemas import FlowSettings, FlowInformation
|
|
3
|
+
from flowfile_core.schemas.input_schema import RawData
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"transformation_settings", "node_interface", "FlowSettings", "FlowInformation", "RawData"
|
|
8
|
+
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Cloud storage connection schemas for S3, ADLS, and other cloud providers."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Literal
|
|
4
|
+
import polars as pl
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, SecretStr, field_validator, Field
|
|
8
|
+
|
|
9
|
+
from flowfile_core.secret_manager.secret_manager import encrypt_secret
|
|
10
|
+
|
|
11
|
+
CloudStorageType = Literal["s3", "adls", "gcs"]
|
|
12
|
+
AuthMethod = Literal["access_key", "iam_role", "service_principal", "managed_identity", "sas_token", "aws-cli", "env_vars"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def encrypt_for_worker(secret_value: SecretStr|None) -> str|None:
|
|
16
|
+
"""
|
|
17
|
+
Encrypts a secret value for use in worker contexts.
|
|
18
|
+
This is a placeholder function that simulates encryption.
|
|
19
|
+
In practice, you would use a secure encryption method.
|
|
20
|
+
"""
|
|
21
|
+
if secret_value is not None:
|
|
22
|
+
return encrypt_secret(secret_value.get_secret_value())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthSettingsInput(BaseModel):
|
|
26
|
+
"""
|
|
27
|
+
The information needed for the user to provide the details that are needed to provide how to connect to the
|
|
28
|
+
Cloud provider
|
|
29
|
+
"""
|
|
30
|
+
storage_type: CloudStorageType
|
|
31
|
+
auth_method: AuthMethod
|
|
32
|
+
connection_name: Optional[str] = "None" # This is the reference to the item we will fetch that contains the data
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FullCloudStorageConnectionWorkerInterface(AuthSettingsInput):
|
|
36
|
+
"""Internal model with decrypted secrets"""
|
|
37
|
+
|
|
38
|
+
# AWS S3
|
|
39
|
+
aws_region: Optional[str] = None
|
|
40
|
+
aws_access_key_id: Optional[str] = None
|
|
41
|
+
aws_secret_access_key: Optional[str] = None
|
|
42
|
+
aws_role_arn: Optional[str] = None
|
|
43
|
+
aws_allow_unsafe_html: Optional[bool] = None
|
|
44
|
+
aws_session_token: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
# Azure ADLS
|
|
47
|
+
azure_account_name: Optional[str] = None
|
|
48
|
+
azure_account_key: Optional[str] = None
|
|
49
|
+
azure_tenant_id: Optional[str] = None
|
|
50
|
+
azure_client_id: Optional[str] = None
|
|
51
|
+
azure_client_secret: Optional[str] = None
|
|
52
|
+
|
|
53
|
+
# Common
|
|
54
|
+
endpoint_url: Optional[str] = None
|
|
55
|
+
verify_ssl: bool = True
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FullCloudStorageConnection(AuthSettingsInput):
|
|
59
|
+
"""Internal model with decrypted secrets"""
|
|
60
|
+
|
|
61
|
+
# AWS S3
|
|
62
|
+
aws_region: Optional[str] = None
|
|
63
|
+
aws_access_key_id: Optional[str] = None
|
|
64
|
+
aws_secret_access_key: Optional[SecretStr] = None
|
|
65
|
+
aws_role_arn: Optional[str] = None
|
|
66
|
+
aws_allow_unsafe_html: Optional[bool] = None
|
|
67
|
+
aws_session_token: Optional[SecretStr] = None
|
|
68
|
+
|
|
69
|
+
# Azure ADLS
|
|
70
|
+
azure_account_name: Optional[str] = None
|
|
71
|
+
azure_account_key: Optional[SecretStr] = None
|
|
72
|
+
azure_tenant_id: Optional[str] = None
|
|
73
|
+
azure_client_id: Optional[str] = None
|
|
74
|
+
azure_client_secret: Optional[SecretStr] = None
|
|
75
|
+
|
|
76
|
+
# Common
|
|
77
|
+
endpoint_url: Optional[str] = None
|
|
78
|
+
verify_ssl: bool = True
|
|
79
|
+
|
|
80
|
+
def get_worker_interface(self) -> "FullCloudStorageConnectionWorkerInterface":
|
|
81
|
+
"""
|
|
82
|
+
Convert to a public interface model without secrets.
|
|
83
|
+
"""
|
|
84
|
+
return FullCloudStorageConnectionWorkerInterface(
|
|
85
|
+
storage_type=self.storage_type,
|
|
86
|
+
auth_method=self.auth_method,
|
|
87
|
+
connection_name=self.connection_name,
|
|
88
|
+
aws_allow_unsafe_html=self.aws_allow_unsafe_html,
|
|
89
|
+
aws_secret_access_key=encrypt_for_worker(self.aws_secret_access_key),
|
|
90
|
+
aws_region=self.aws_region,
|
|
91
|
+
aws_access_key_id=self.aws_access_key_id,
|
|
92
|
+
aws_role_arn=self.aws_role_arn,
|
|
93
|
+
aws_session_token=encrypt_for_worker(self.aws_session_token),
|
|
94
|
+
azure_account_name=self.azure_account_name,
|
|
95
|
+
azure_tenant_id=self.azure_tenant_id,
|
|
96
|
+
azure_account_key=encrypt_for_worker(self.azure_account_key),
|
|
97
|
+
azure_client_id=self.azure_client_id,
|
|
98
|
+
azure_client_secret=encrypt_for_worker(self.azure_client_secret),
|
|
99
|
+
endpoint_url=self.endpoint_url,
|
|
100
|
+
verify_ssl=self.verify_ssl
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class FullCloudStorageConnectionInterface(AuthSettingsInput):
|
|
105
|
+
"""API response model - no secrets exposed"""
|
|
106
|
+
|
|
107
|
+
# Public fields only
|
|
108
|
+
aws_allow_unsafe_html: Optional[bool] = None
|
|
109
|
+
aws_region: Optional[str] = None
|
|
110
|
+
aws_access_key_id: Optional[str] = None
|
|
111
|
+
aws_role_arn: Optional[str] = None
|
|
112
|
+
azure_account_name: Optional[str] = None
|
|
113
|
+
azure_tenant_id: Optional[str] = None
|
|
114
|
+
azure_client_id: Optional[str] = None
|
|
115
|
+
endpoint_url: Optional[str] = None
|
|
116
|
+
verify_ssl: bool = True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CloudStorageSettings(BaseModel):
|
|
120
|
+
"""Settings for cloud storage nodes in the visual designer"""
|
|
121
|
+
|
|
122
|
+
auth_mode: AuthMethod = "auto"
|
|
123
|
+
connection_name: Optional[str] = None # Required only for 'reference' mode
|
|
124
|
+
resource_path: str # s3://bucket/path/to/file.csv
|
|
125
|
+
|
|
126
|
+
@field_validator("auth_mode", mode="after")
|
|
127
|
+
def validate_auth_requirements(cls, v, values):
|
|
128
|
+
data = values.data
|
|
129
|
+
if v == "reference" and not data.get("connection_name"):
|
|
130
|
+
raise ValueError("connection_name required when using reference mode")
|
|
131
|
+
return v
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class CloudStorageReadSettings(CloudStorageSettings):
|
|
135
|
+
"""Settings for reading from cloud storage"""
|
|
136
|
+
|
|
137
|
+
scan_mode: Literal["single_file", "directory"] = "single_file"
|
|
138
|
+
file_format: Literal["csv", "parquet", "json", "delta", "iceberg"] = "parquet"
|
|
139
|
+
# CSV specific options
|
|
140
|
+
csv_has_header: Optional[bool] = True
|
|
141
|
+
csv_delimiter: Optional[str] = ","
|
|
142
|
+
csv_encoding: Optional[str] = "utf8"
|
|
143
|
+
# Deltalake specific settings
|
|
144
|
+
delta_version: Optional[int] = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CloudStorageReadSettingsInternal(BaseModel):
|
|
148
|
+
read_settings: CloudStorageReadSettings
|
|
149
|
+
connection: FullCloudStorageConnection
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class WriteSettingsWorkerInterface(BaseModel):
|
|
153
|
+
"""Settings for writing to cloud storage"""
|
|
154
|
+
resource_path: str # s3://bucket/path/to/file.csv
|
|
155
|
+
|
|
156
|
+
write_mode: Literal["overwrite", "append"] = "overwrite"
|
|
157
|
+
file_format: Literal["csv", "parquet", "json", "delta"] = "parquet"
|
|
158
|
+
|
|
159
|
+
parquet_compression: Literal["snappy", "gzip", "brotli", "lz4", "zstd"] = "snappy"
|
|
160
|
+
|
|
161
|
+
csv_delimiter: str = ","
|
|
162
|
+
csv_encoding: str = "utf8"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class CloudStorageWriteSettings(CloudStorageSettings, WriteSettingsWorkerInterface):
|
|
166
|
+
"""Settings for writing to cloud storage"""
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
def get_write_setting_worker_interface(self) -> WriteSettingsWorkerInterface:
|
|
170
|
+
"""
|
|
171
|
+
Convert to a worker interface model without secrets.
|
|
172
|
+
"""
|
|
173
|
+
return WriteSettingsWorkerInterface(
|
|
174
|
+
resource_path=self.resource_path,
|
|
175
|
+
write_mode=self.write_mode,
|
|
176
|
+
file_format=self.file_format,
|
|
177
|
+
parquet_compression=self.parquet_compression,
|
|
178
|
+
csv_delimiter=self.csv_delimiter,
|
|
179
|
+
csv_encoding=self.csv_encoding
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class CloudStorageWriteSettingsInternal(BaseModel):
|
|
184
|
+
write_settings: CloudStorageWriteSettings
|
|
185
|
+
connection: FullCloudStorageConnection
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class CloudStorageWriteSettingsWorkerInterface(BaseModel):
|
|
189
|
+
"""Settings for writing to cloud storage in worker context"""
|
|
190
|
+
operation: str
|
|
191
|
+
write_settings: WriteSettingsWorkerInterface
|
|
192
|
+
connection: FullCloudStorageConnectionWorkerInterface
|
|
193
|
+
flowfile_flow_id: int = 1
|
|
194
|
+
flowfile_node_id: int | str = -1
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_cloud_storage_write_settings_worker_interface(
|
|
198
|
+
write_settings: CloudStorageWriteSettings,
|
|
199
|
+
connection: FullCloudStorageConnection,
|
|
200
|
+
lf: pl.LazyFrame,
|
|
201
|
+
flowfile_flow_id: int = 1,
|
|
202
|
+
flowfile_node_id: int | str = -1,
|
|
203
|
+
) -> CloudStorageWriteSettingsWorkerInterface:
|
|
204
|
+
"""
|
|
205
|
+
Convert to a worker interface model with hashed secrets.
|
|
206
|
+
"""
|
|
207
|
+
operation = base64.b64encode(lf.serialize()).decode()
|
|
208
|
+
|
|
209
|
+
return CloudStorageWriteSettingsWorkerInterface(
|
|
210
|
+
operation=operation,
|
|
211
|
+
write_settings=write_settings.get_write_setting_worker_interface(),
|
|
212
|
+
connection=connection.get_worker_interface(),
|
|
213
|
+
flowfile_flow_id=flowfile_flow_id, # Default value, can be overridden
|
|
214
|
+
flowfile_node_id=flowfile_node_id # Default value, can be overridden
|
|
215
|
+
)
|