Flowfile 0.2.2__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.
- build_backends/__init__.py +0 -0
- build_backends/main.py +313 -0
- build_backends/main_prd.py +202 -0
- flowfile/__init__.py +71 -0
- flowfile/__main__.py +24 -0
- flowfile-0.2.2.dist-info/LICENSE +21 -0
- flowfile-0.2.2.dist-info/METADATA +225 -0
- flowfile-0.2.2.dist-info/RECORD +171 -0
- flowfile-0.2.2.dist-info/WHEEL +4 -0
- flowfile-0.2.2.dist-info/entry_points.txt +9 -0
- flowfile_core/__init__.py +13 -0
- flowfile_core/auth/__init__.py +0 -0
- flowfile_core/auth/jwt.py +140 -0
- flowfile_core/auth/models.py +40 -0
- flowfile_core/auth/secrets.py +178 -0
- flowfile_core/configs/__init__.py +35 -0
- flowfile_core/configs/flow_logger.py +433 -0
- flowfile_core/configs/node_store/__init__.py +0 -0
- flowfile_core/configs/node_store/nodes.py +98 -0
- flowfile_core/configs/settings.py +120 -0
- flowfile_core/database/__init__.py +0 -0
- flowfile_core/database/connection.py +51 -0
- flowfile_core/database/init_db.py +45 -0
- flowfile_core/database/models.py +41 -0
- flowfile_core/fileExplorer/__init__.py +0 -0
- flowfile_core/fileExplorer/funcs.py +259 -0
- flowfile_core/fileExplorer/utils.py +53 -0
- flowfile_core/flowfile/FlowfileFlow.py +1403 -0
- flowfile_core/flowfile/__init__.py +0 -0
- flowfile_core/flowfile/_extensions/__init__.py +0 -0
- flowfile_core/flowfile/_extensions/real_time_interface.py +51 -0
- flowfile_core/flowfile/analytics/__init__.py +0 -0
- flowfile_core/flowfile/analytics/analytics_processor.py +123 -0
- flowfile_core/flowfile/analytics/graphic_walker.py +60 -0
- flowfile_core/flowfile/analytics/schemas/__init__.py +0 -0
- flowfile_core/flowfile/analytics/utils.py +9 -0
- flowfile_core/flowfile/connection_manager/__init__.py +3 -0
- flowfile_core/flowfile/connection_manager/_connection_manager.py +48 -0
- flowfile_core/flowfile/connection_manager/models.py +10 -0
- flowfile_core/flowfile/database_connection_manager/__init__.py +0 -0
- flowfile_core/flowfile/database_connection_manager/db_connections.py +139 -0
- flowfile_core/flowfile/database_connection_manager/models.py +15 -0
- flowfile_core/flowfile/extensions.py +36 -0
- flowfile_core/flowfile/flow_data_engine/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/create/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/create/funcs.py +146 -0
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +1521 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +144 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/polars_type.py +24 -0
- flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +36 -0
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/__init__.py +0 -0
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/prepare_for_fuzzy_match.py +38 -0
- flowfile_core/flowfile/flow_data_engine/fuzzy_matching/settings_validator.py +90 -0
- flowfile_core/flowfile/flow_data_engine/join/__init__.py +1 -0
- flowfile_core/flowfile/flow_data_engine/join/verify_integrity.py +54 -0
- flowfile_core/flowfile/flow_data_engine/pivot_table.py +20 -0
- flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +249 -0
- flowfile_core/flowfile/flow_data_engine/read_excel_tables.py +143 -0
- flowfile_core/flowfile/flow_data_engine/sample_data.py +120 -0
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/__init__.py +1 -0
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +36 -0
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +503 -0
- flowfile_core/flowfile/flow_data_engine/threaded_processes.py +27 -0
- flowfile_core/flowfile/flow_data_engine/types.py +0 -0
- flowfile_core/flowfile/flow_data_engine/utils.py +212 -0
- flowfile_core/flowfile/flow_node/__init__.py +0 -0
- flowfile_core/flowfile/flow_node/flow_node.py +771 -0
- flowfile_core/flowfile/flow_node/models.py +111 -0
- flowfile_core/flowfile/flow_node/schema_callback.py +70 -0
- flowfile_core/flowfile/handler.py +123 -0
- flowfile_core/flowfile/manage/__init__.py +0 -0
- flowfile_core/flowfile/manage/compatibility_enhancements.py +70 -0
- flowfile_core/flowfile/manage/manage_flowfile.py +0 -0
- flowfile_core/flowfile/manage/open_flowfile.py +136 -0
- flowfile_core/flowfile/setting_generator/__init__.py +2 -0
- flowfile_core/flowfile/setting_generator/setting_generator.py +41 -0
- flowfile_core/flowfile/setting_generator/settings.py +176 -0
- flowfile_core/flowfile/sources/__init__.py +0 -0
- flowfile_core/flowfile/sources/external_sources/__init__.py +3 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/__init__.py +0 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/airbyte.py +159 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/models.py +172 -0
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/settings.py +173 -0
- flowfile_core/flowfile/sources/external_sources/base_class.py +39 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/__init__.py +2 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/exchange_rate.py +0 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/external_source.py +100 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/google_sheet.py +74 -0
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/sample_users.py +29 -0
- flowfile_core/flowfile/sources/external_sources/factory.py +22 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/__init__.py +0 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/models.py +90 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +328 -0
- flowfile_core/flowfile/sources/external_sources/sql_source/utils.py +379 -0
- flowfile_core/flowfile/util/__init__.py +0 -0
- flowfile_core/flowfile/util/calculate_layout.py +137 -0
- flowfile_core/flowfile/util/execution_orderer.py +141 -0
- flowfile_core/flowfile/utils.py +106 -0
- flowfile_core/main.py +138 -0
- flowfile_core/routes/__init__.py +0 -0
- flowfile_core/routes/auth.py +34 -0
- flowfile_core/routes/logs.py +163 -0
- flowfile_core/routes/public.py +10 -0
- flowfile_core/routes/routes.py +601 -0
- flowfile_core/routes/secrets.py +85 -0
- flowfile_core/run_lock.py +11 -0
- flowfile_core/schemas/__init__.py +0 -0
- flowfile_core/schemas/analysis_schemas/__init__.py +0 -0
- flowfile_core/schemas/analysis_schemas/graphic_walker_schemas.py +118 -0
- flowfile_core/schemas/defaults.py +9 -0
- flowfile_core/schemas/external_sources/__init__.py +0 -0
- flowfile_core/schemas/external_sources/airbyte_schemas.py +20 -0
- flowfile_core/schemas/input_schema.py +477 -0
- flowfile_core/schemas/models.py +193 -0
- flowfile_core/schemas/output_model.py +115 -0
- flowfile_core/schemas/schemas.py +106 -0
- flowfile_core/schemas/transform_schema.py +569 -0
- flowfile_core/secrets/__init__.py +0 -0
- flowfile_core/secrets/secrets.py +64 -0
- flowfile_core/utils/__init__.py +0 -0
- flowfile_core/utils/arrow_reader.py +247 -0
- flowfile_core/utils/excel_file_manager.py +18 -0
- flowfile_core/utils/fileManager.py +45 -0
- flowfile_core/utils/fl_executor.py +38 -0
- flowfile_core/utils/utils.py +8 -0
- flowfile_frame/__init__.py +56 -0
- flowfile_frame/__main__.py +12 -0
- flowfile_frame/adapters.py +17 -0
- flowfile_frame/expr.py +1163 -0
- flowfile_frame/flow_frame.py +2093 -0
- flowfile_frame/group_frame.py +199 -0
- flowfile_frame/join.py +75 -0
- flowfile_frame/selectors.py +242 -0
- flowfile_frame/utils.py +184 -0
- flowfile_worker/__init__.py +55 -0
- flowfile_worker/configs.py +95 -0
- flowfile_worker/create/__init__.py +37 -0
- flowfile_worker/create/funcs.py +146 -0
- flowfile_worker/create/models.py +86 -0
- flowfile_worker/create/pl_types.py +35 -0
- flowfile_worker/create/read_excel_tables.py +110 -0
- flowfile_worker/create/utils.py +84 -0
- flowfile_worker/external_sources/__init__.py +0 -0
- flowfile_worker/external_sources/airbyte_sources/__init__.py +0 -0
- flowfile_worker/external_sources/airbyte_sources/cache_manager.py +161 -0
- flowfile_worker/external_sources/airbyte_sources/main.py +89 -0
- flowfile_worker/external_sources/airbyte_sources/models.py +133 -0
- flowfile_worker/external_sources/airbyte_sources/settings.py +0 -0
- flowfile_worker/external_sources/sql_source/__init__.py +0 -0
- flowfile_worker/external_sources/sql_source/main.py +56 -0
- flowfile_worker/external_sources/sql_source/models.py +72 -0
- flowfile_worker/flow_logger.py +58 -0
- flowfile_worker/funcs.py +327 -0
- flowfile_worker/main.py +108 -0
- flowfile_worker/models.py +95 -0
- flowfile_worker/polars_fuzzy_match/__init__.py +0 -0
- flowfile_worker/polars_fuzzy_match/matcher.py +435 -0
- flowfile_worker/polars_fuzzy_match/models.py +36 -0
- flowfile_worker/polars_fuzzy_match/pre_process.py +213 -0
- flowfile_worker/polars_fuzzy_match/process.py +86 -0
- flowfile_worker/polars_fuzzy_match/utils.py +50 -0
- flowfile_worker/process_manager.py +36 -0
- flowfile_worker/routes.py +440 -0
- flowfile_worker/secrets.py +148 -0
- flowfile_worker/spawner.py +187 -0
- flowfile_worker/utils.py +25 -0
- test_utils/__init__.py +3 -0
- test_utils/postgres/__init__.py +1 -0
- test_utils/postgres/commands.py +109 -0
- test_utils/postgres/fixtures.py +417 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from flowfile_core.schemas.schemas import NodeTemplate, NodeDefault, TransformTypeLiteral, NodeTypeLiteral
|
|
3
|
+
|
|
4
|
+
nodes_list: List[NodeTemplate] = [
|
|
5
|
+
NodeTemplate(name='Read Airbyte', item='airbyte_reader', input=0, output=1, image='airbyte.png',
|
|
6
|
+
node_group='input'),
|
|
7
|
+
NodeTemplate(name='Google sheets', item='google_sheet', input=0, output=1,
|
|
8
|
+
image='google_sheet.png', node_group='input', prod_ready=False),
|
|
9
|
+
NodeTemplate(name='External source', item='external_source', input=0, output=1,
|
|
10
|
+
image='external_source.png', node_group='input', prod_ready=False),
|
|
11
|
+
NodeTemplate(name='Manual input', item='manual_input', input=0, output=1,
|
|
12
|
+
image='manual_input.png', node_group='input'),
|
|
13
|
+
NodeTemplate(name='Read data', item='read', input=0, output=1, image='input_data.png', node_group='input'),
|
|
14
|
+
NodeTemplate(name='Join', color='#49494970', item='join', input=2, output=1, image='join.png',
|
|
15
|
+
node_group='combine'),
|
|
16
|
+
NodeTemplate(name='Formula', color='blue', item='formula', input=1, output=1, image='formula.png',
|
|
17
|
+
node_group='transform'),
|
|
18
|
+
# NodeTemplate(name='Sql editor', color='blue', item='sql_editor', input=1, output=1, image='sql.png'),
|
|
19
|
+
NodeTemplate(name='Write data', item='output', input=1, output=0, image='output.png', node_group='output'),
|
|
20
|
+
NodeTemplate(name='Select data', item='select', input=1, output=1, image='select.png', node_group='transform'),
|
|
21
|
+
NodeTemplate(name='Filter data', item='filter', input=1, output=1, image='filter.png', node_group='transform'),
|
|
22
|
+
NodeTemplate(name='Group by', item='group_by', input=1, output=1, image='group_by.png', node_group='aggregate'),
|
|
23
|
+
NodeTemplate(name='Fuzzy match', item='fuzzy_match', input=2, output=1, image='fuzzy_match.png',
|
|
24
|
+
node_group='combine'),
|
|
25
|
+
NodeTemplate(name='Sort data', item='sort', input=1, output=1, image='sort.png', node_group='transform'),
|
|
26
|
+
NodeTemplate(name='Add record Id', item='record_id', input=1, output=1, image='record_id.png',
|
|
27
|
+
node_group='transform'),
|
|
28
|
+
NodeTemplate(name='Take Sample', item='sample', input=1, output=1, image='sample.png', node_group='transform'),
|
|
29
|
+
NodeTemplate(name='Explore data', item='explore_data', input=1, output=0,
|
|
30
|
+
image='explore_data.png', node_group='output'),
|
|
31
|
+
NodeTemplate(name='Pivot data', item='pivot', input=1, output=1, image='pivot.png', node_group='aggregate'),
|
|
32
|
+
NodeTemplate(name='Unpivot data', item='unpivot', input=1, output=1, image='unpivot.png', node_group='aggregate'),
|
|
33
|
+
NodeTemplate(name='Union data', item='union', input=10, output=1, image='union.png', multi=True,
|
|
34
|
+
node_group='combine'),
|
|
35
|
+
NodeTemplate(name='Drop duplicates', item='unique', input=1, output=1, image='unique.png', node_group='transform'),
|
|
36
|
+
NodeTemplate(name='Graph solver', item='graph_solver', input=1, output=1, image='graph_solver.png',
|
|
37
|
+
node_group='combine'),
|
|
38
|
+
NodeTemplate(name='Count records', item='record_count', input=1, output=1, image='record_count.png',
|
|
39
|
+
node_group='aggregate'),
|
|
40
|
+
NodeTemplate(name='Cross join', item='cross_join', input=2, output=1, image='cross_join.png', node_group='combine'),
|
|
41
|
+
NodeTemplate(name='Text to rows', item='text_to_rows', input=1, output=1, image='text_to_rows.png',
|
|
42
|
+
node_group='transform'),
|
|
43
|
+
NodeTemplate(name="Polars code", item="polars_code", input=10, output=1, image='polars_code.png',
|
|
44
|
+
node_group='transform', multi=True, can_be_start=True),
|
|
45
|
+
NodeTemplate(name="Read from Database", item="database_reader", input=0, output=1, image='database_reader.svg',
|
|
46
|
+
node_group='input'),
|
|
47
|
+
NodeTemplate(name='Write to Database', item='database_writer', input=1, output=0, image='database_writer.svg',
|
|
48
|
+
node_group='output'),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
nodes_list.sort(key=lambda x: x.name)
|
|
52
|
+
|
|
53
|
+
output = ['Explore data', 'Write data', 'Write to Database']
|
|
54
|
+
_input = ['Read Airbyte', 'Google sheets', 'Manual input', 'Read data', 'External source', 'Read from Database']
|
|
55
|
+
transform = ['Join', 'Formula', 'Select data', 'Filter data', 'Group by', 'Fuzzy match', 'Sort data', 'Add record Id',
|
|
56
|
+
'Take Sample', 'Pivot data', 'Unpivot data', 'Union data', 'Drop duplicates', 'Graph solver',
|
|
57
|
+
'Count records', 'Cross join', 'Text to rows', 'Polars code']
|
|
58
|
+
narrow = ['Select data', 'Filter data', 'Take Sample', 'Formula', 'Read data', 'Union data', 'Polars code']
|
|
59
|
+
wide = ['Join', 'Group by', 'Fuzzy match', 'Sort data', 'Pivot data', 'Unpivot data', 'Add record Id',
|
|
60
|
+
'Graph solver', 'Drop duplicates', 'Count records', 'Cross join', 'Text to rows']
|
|
61
|
+
other = ['Explore data', 'Write data', 'Read Airbyte', 'Google sheets', 'Manual input', 'Read data', 'External source',
|
|
62
|
+
'Read from Database', 'Write to Database']
|
|
63
|
+
nodes_with_defaults = {'sample', 'sort', 'union', 'select', 'record_count'}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_node_type(node_name: str) -> NodeTypeLiteral:
|
|
67
|
+
if node_name in output:
|
|
68
|
+
return 'output'
|
|
69
|
+
if node_name in _input:
|
|
70
|
+
return 'input'
|
|
71
|
+
if node_name in transform:
|
|
72
|
+
return 'process'
|
|
73
|
+
else:
|
|
74
|
+
raise ValueError(f'Node name {node_name} not found in any of the node types')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def check_if_has_default_setting(node_item: str):
|
|
78
|
+
return node_item in nodes_with_defaults
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_transform_type(node_name: str) -> TransformTypeLiteral:
|
|
82
|
+
if node_name in narrow:
|
|
83
|
+
return 'narrow'
|
|
84
|
+
if node_name in wide:
|
|
85
|
+
return 'wide'
|
|
86
|
+
if node_name in other:
|
|
87
|
+
return 'other'
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(f'Node name {node_name} not found in any of the transform types')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
node_defaults = {node.item: NodeDefault(node_name=node.name,
|
|
93
|
+
node_type=get_node_type(node.name),
|
|
94
|
+
transform_type=get_transform_type(node.name),
|
|
95
|
+
has_default_settings=check_if_has_default_setting(node.item)
|
|
96
|
+
) for node in nodes_list}
|
|
97
|
+
|
|
98
|
+
node_dict = {n.item: n for n in nodes_list}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
|
|
2
|
+
# flowfile_core/flowfile_core/configs/settings.py
|
|
3
|
+
import platform
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
from databases import DatabaseURL
|
|
9
|
+
from passlib.context import CryptContext
|
|
10
|
+
from starlette.config import Config
|
|
11
|
+
from starlette.datastructures import Secret
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Constants for server and worker configuration
|
|
15
|
+
DEFAULT_SERVER_HOST = "0.0.0.0"
|
|
16
|
+
DEFAULT_SERVER_PORT = 63578
|
|
17
|
+
DEFAULT_WORKER_PORT = 63579
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_args():
|
|
21
|
+
"""Parse command line arguments"""
|
|
22
|
+
parser = argparse.ArgumentParser(description="Flowfile Backend Server")
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--host", type=str, default=DEFAULT_SERVER_HOST, help="Host to bind to"
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--port", type=int, default=DEFAULT_SERVER_PORT, help="Port to bind to"
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--worker-port",
|
|
31
|
+
type=int,
|
|
32
|
+
default=DEFAULT_WORKER_PORT,
|
|
33
|
+
help="Port for the worker process",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Use known_args to handle PyInstaller's extra args
|
|
37
|
+
args = parser.parse_known_args()[0]
|
|
38
|
+
|
|
39
|
+
# Validate arguments
|
|
40
|
+
if args.port < 1 or args.port > 65535:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"Invalid port number: {args.port}. Port must be between 1 and 65535."
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if args.worker_port < 1 or args.worker_port > 65535:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Invalid worker port number: {args.worker_port}. Port must be between 1 and 65535."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Check if host is valid (basic check)
|
|
51
|
+
if not args.host:
|
|
52
|
+
raise ValueError("Host cannot be empty")
|
|
53
|
+
|
|
54
|
+
# Check that server and worker ports are different
|
|
55
|
+
if args.port == args.worker_port:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Server port ({args.port}) and worker port ({args.worker_port}) must be different"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return args
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_temp_dir() -> str:
|
|
64
|
+
"""Get the appropriate temp directory path based on environment"""
|
|
65
|
+
# Check for Docker environment variable first
|
|
66
|
+
docker_temp = os.getenv("TEMP_DIR")
|
|
67
|
+
if docker_temp:
|
|
68
|
+
return docker_temp
|
|
69
|
+
|
|
70
|
+
return tempfile.gettempdir()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_default_worker_url(worker_port=None):
|
|
74
|
+
"""
|
|
75
|
+
Get the default worker URL based on environment and settings
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
worker_port: Optional port override (used when passed as command line arg)
|
|
79
|
+
"""
|
|
80
|
+
# Check for Docker environment first
|
|
81
|
+
worker_host = os.getenv("WORKER_HOST", None)
|
|
82
|
+
|
|
83
|
+
# Use the provided port if available, otherwise get from env or default
|
|
84
|
+
if worker_port is None:
|
|
85
|
+
worker_port = os.getenv("WORKER_PORT", DEFAULT_WORKER_PORT)
|
|
86
|
+
|
|
87
|
+
# Convert to int if it's a string
|
|
88
|
+
worker_port = int(worker_port) if isinstance(worker_port, str) else worker_port
|
|
89
|
+
|
|
90
|
+
if worker_host:
|
|
91
|
+
return f"http://{worker_host}:{worker_port}"
|
|
92
|
+
|
|
93
|
+
if platform.system() == "Windows":
|
|
94
|
+
return f"http://127.0.0.1:{worker_port}"
|
|
95
|
+
else:
|
|
96
|
+
return f"http://0.0.0.0:{worker_port}"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Get server arguments
|
|
100
|
+
args = parse_args()
|
|
101
|
+
|
|
102
|
+
SERVER_HOST = args.host if args.host is not None else DEFAULT_SERVER_HOST
|
|
103
|
+
SERVER_PORT = args.port if args.port is not None else DEFAULT_SERVER_PORT
|
|
104
|
+
WORKER_PORT = args.worker_port if args.worker_port is not None else int(os.getenv("WORKER_PORT", DEFAULT_WORKER_PORT))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Worker configuration
|
|
108
|
+
WORKER_HOST = os.getenv("WORKER_HOST", "0.0.0.0" if platform.system() != "Windows" else "127.0.0.1")
|
|
109
|
+
|
|
110
|
+
config = Config(".env")
|
|
111
|
+
DEBUG: bool = config("DEBUG", cast=bool, default=False)
|
|
112
|
+
FILE_LOCATION = config("FILE_LOCATION", cast=str, default=".\\files\\")
|
|
113
|
+
AVAILABLE_RAM = config("AVAILABLE_RAM", cast=int, default=8)
|
|
114
|
+
WORKER_URL = config("WORKER_URL", cast=str, default=get_default_worker_url(WORKER_PORT))
|
|
115
|
+
IS_RUNNING_IN_DOCKER = os.getenv('RUNNING_IN_DOCKER', 'false').lower() == 'true'
|
|
116
|
+
TEMP_DIR = get_temp_dir()
|
|
117
|
+
|
|
118
|
+
ALGORITHM = "HS256"
|
|
119
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 120
|
|
120
|
+
PWD_CONTEXT = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
from sqlalchemy import create_engine
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from sqlalchemy.orm import sessionmaker
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_database_url():
|
|
9
|
+
if os.environ.get("TESTING") == "True":
|
|
10
|
+
# Use a file-based test database instead of in-memory
|
|
11
|
+
test_db_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../test_flowfile.db")
|
|
12
|
+
return f"sqlite:///{test_db_path}"
|
|
13
|
+
# elif os.environ.get("FLOWFILE_MODE") == "electron":
|
|
14
|
+
|
|
15
|
+
return "sqlite:///./flowfile.db"
|
|
16
|
+
|
|
17
|
+
# else:
|
|
18
|
+
# # Use PostgreSQL for Docker mode
|
|
19
|
+
# host = os.environ.get("DB_HOST", "localhost")
|
|
20
|
+
# port = os.environ.get("DB_PORT", "5432")
|
|
21
|
+
# user = os.environ.get("DB_USER", "postgres")
|
|
22
|
+
# password = os.environ.get("DB_PASSWORD", "postgres")
|
|
23
|
+
# db = os.environ.get("DB_NAME", "flowfile")
|
|
24
|
+
# return f"postgresql://{user}:{password}@{host}:{port}/{db}"
|
|
25
|
+
#
|
|
26
|
+
|
|
27
|
+
# Create database engine
|
|
28
|
+
engine = create_engine(
|
|
29
|
+
get_database_url(),
|
|
30
|
+
connect_args={"check_same_thread": False} if os.environ.get("FLOWFILE_MODE") == "electron" else {}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Create session factory
|
|
34
|
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_db():
|
|
38
|
+
db = SessionLocal()
|
|
39
|
+
try:
|
|
40
|
+
yield db
|
|
41
|
+
finally:
|
|
42
|
+
db.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@contextmanager
|
|
46
|
+
def get_db_context():
|
|
47
|
+
db = SessionLocal()
|
|
48
|
+
try:
|
|
49
|
+
yield db
|
|
50
|
+
finally:
|
|
51
|
+
db.close()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Generate a random secure password and hash it
|
|
2
|
+
import secrets
|
|
3
|
+
import string
|
|
4
|
+
from sqlalchemy.orm import Session
|
|
5
|
+
from flowfile_core.database import models as db_models
|
|
6
|
+
from flowfile_core.database.connection import engine, SessionLocal
|
|
7
|
+
|
|
8
|
+
from passlib.context import CryptContext
|
|
9
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
10
|
+
|
|
11
|
+
db_models.Base.metadata.create_all(bind=engine)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_default_local_user(db: Session):
|
|
15
|
+
local_user = db.query(db_models.User).filter(db_models.User.username == "local_user").first()
|
|
16
|
+
|
|
17
|
+
if not local_user:
|
|
18
|
+
random_password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32))
|
|
19
|
+
hashed_password = pwd_context.hash(random_password)
|
|
20
|
+
|
|
21
|
+
local_user = db_models.User(
|
|
22
|
+
username="local_user",
|
|
23
|
+
email="local@flowfile.app",
|
|
24
|
+
full_name="Local User",
|
|
25
|
+
hashed_password=hashed_password
|
|
26
|
+
)
|
|
27
|
+
db.add(local_user)
|
|
28
|
+
db.commit()
|
|
29
|
+
return True
|
|
30
|
+
else:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def init_db():
|
|
35
|
+
db = SessionLocal()
|
|
36
|
+
try:
|
|
37
|
+
create_default_local_user(db)
|
|
38
|
+
finally:
|
|
39
|
+
db.close()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
init_db()
|
|
44
|
+
print("Local user created successfully")
|
|
45
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text
|
|
3
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
4
|
+
|
|
5
|
+
Base = declarative_base()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class User(Base):
|
|
9
|
+
__tablename__ = "users"
|
|
10
|
+
|
|
11
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
12
|
+
username = Column(String, unique=True, index=True)
|
|
13
|
+
email = Column(String, unique=True, index=True)
|
|
14
|
+
full_name = Column(String)
|
|
15
|
+
hashed_password = Column(String)
|
|
16
|
+
disabled = Column(Boolean, default=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Secret(Base):
|
|
20
|
+
__tablename__ = "secrets"
|
|
21
|
+
|
|
22
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
23
|
+
name = Column(String, index=True)
|
|
24
|
+
encrypted_value = Column(Text)
|
|
25
|
+
iv = Column(String)
|
|
26
|
+
user_id = Column(Integer, ForeignKey("users.id"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DatabaseConnection(Base):
|
|
30
|
+
__tablename__ = "database_connections"
|
|
31
|
+
|
|
32
|
+
id = Column(Integer, primary_key=True, index=True)
|
|
33
|
+
connection_name = Column(String, index=True)
|
|
34
|
+
database_type = Column(String)
|
|
35
|
+
username = Column(String)
|
|
36
|
+
host = Column(String)
|
|
37
|
+
port = Column(Integer)
|
|
38
|
+
database = Column(String, default=None)
|
|
39
|
+
ssl_enabled = Column(Boolean, default=False)
|
|
40
|
+
password_id = Column(Integer, ForeignKey("secrets.id"))
|
|
41
|
+
user_id = Column(Integer, ForeignKey("users.id"))
|
|
File without changes
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import List, Optional, Set, Union
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from typing_extensions import Literal
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileInfo(BaseModel):
|
|
10
|
+
"""Comprehensive information about a file or directory."""
|
|
11
|
+
name: str
|
|
12
|
+
path: str
|
|
13
|
+
is_directory: bool
|
|
14
|
+
size: int
|
|
15
|
+
file_type: str
|
|
16
|
+
last_modified: datetime
|
|
17
|
+
created_date: datetime
|
|
18
|
+
is_hidden: bool
|
|
19
|
+
exists: bool = True
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_path(cls, path: Path) -> 'FileInfo':
|
|
23
|
+
"""Create FileInfo instance from a path."""
|
|
24
|
+
try:
|
|
25
|
+
stats = path.stat()
|
|
26
|
+
return cls(
|
|
27
|
+
name=path.name,
|
|
28
|
+
path=str(path.absolute()),
|
|
29
|
+
is_directory=path.is_dir(),
|
|
30
|
+
size=stats.st_size,
|
|
31
|
+
file_type=path.suffix[1:] if path.suffix else "",
|
|
32
|
+
last_modified=datetime.fromtimestamp(stats.st_mtime),
|
|
33
|
+
created_date=datetime.fromtimestamp(stats.st_ctime),
|
|
34
|
+
is_hidden=path.name.startswith('.') or (
|
|
35
|
+
os.name == 'nt' and bool(stats.st_file_attributes & 0x2)
|
|
36
|
+
),
|
|
37
|
+
exists=True
|
|
38
|
+
)
|
|
39
|
+
except (PermissionError, OSError):
|
|
40
|
+
return cls(
|
|
41
|
+
name=path.name,
|
|
42
|
+
path=str(path.absolute()),
|
|
43
|
+
is_directory=False,
|
|
44
|
+
size=0,
|
|
45
|
+
file_type="",
|
|
46
|
+
last_modified=datetime.fromtimestamp(0),
|
|
47
|
+
created_date=datetime.fromtimestamp(0),
|
|
48
|
+
is_hidden=False,
|
|
49
|
+
exists=False
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FileExplorer:
|
|
54
|
+
def __init__(self, start_path: Optional[str|Path] = None):
|
|
55
|
+
"""Initialize FileExplorer with user's home directory or specified path."""
|
|
56
|
+
if start_path is None:
|
|
57
|
+
self.current_path = Path.home()
|
|
58
|
+
else:
|
|
59
|
+
self.current_path = Path(start_path).expanduser().resolve()
|
|
60
|
+
|
|
61
|
+
if not self.current_path.exists():
|
|
62
|
+
raise ValueError(f"Path does not exist: {self.current_path}")
|
|
63
|
+
|
|
64
|
+
if not self.current_path.is_dir():
|
|
65
|
+
raise ValueError(f"Path is not a directory: {self.current_path}")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def current_directory(self) -> str:
|
|
69
|
+
"""Get the current directory path."""
|
|
70
|
+
return str(self.current_path.absolute())
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def parent_directory(self) -> Optional[str]:
|
|
74
|
+
"""Get the parent directory path if it exists."""
|
|
75
|
+
parent = self.current_path.parent
|
|
76
|
+
return str(parent.absolute()) if parent != self.current_path else None
|
|
77
|
+
|
|
78
|
+
def list_contents(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
show_hidden: bool = False,
|
|
82
|
+
file_types: Optional[List[str]] = None,
|
|
83
|
+
recursive: bool = False,
|
|
84
|
+
min_size: Optional[int] = None,
|
|
85
|
+
max_size: Optional[int] = None,
|
|
86
|
+
sort_by: Literal['name', 'date', 'size', 'type'] = 'name',
|
|
87
|
+
reverse: bool = False,
|
|
88
|
+
exclude_patterns: Optional[List[str]] = None
|
|
89
|
+
) -> List[FileInfo]:
|
|
90
|
+
"""
|
|
91
|
+
List contents of the current directory with advanced filtering and sorting.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
show_hidden: Whether to show hidden files and directories
|
|
95
|
+
file_types: List of file extensions to include (without dots)
|
|
96
|
+
recursive: Whether to scan subdirectories
|
|
97
|
+
min_size: Minimum file size in bytes
|
|
98
|
+
max_size: Maximum file size in bytes
|
|
99
|
+
sort_by: Field to sort results by
|
|
100
|
+
reverse: Whether to reverse sort order
|
|
101
|
+
exclude_patterns: Glob patterns to exclude
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List of FileInfo objects sorted according to parameters
|
|
105
|
+
"""
|
|
106
|
+
contents: List[FileInfo] = []
|
|
107
|
+
excluded_paths: Set[str] = set()
|
|
108
|
+
|
|
109
|
+
if exclude_patterns:
|
|
110
|
+
for pattern in exclude_patterns:
|
|
111
|
+
excluded_paths.update(str(p) for p in self.current_path.glob(pattern))
|
|
112
|
+
|
|
113
|
+
def should_include(info: FileInfo) -> bool:
|
|
114
|
+
"""Determine if a file should be included based on filters."""
|
|
115
|
+
if str(info.path) in excluded_paths:
|
|
116
|
+
return False
|
|
117
|
+
if not show_hidden and info.is_hidden:
|
|
118
|
+
return False
|
|
119
|
+
if min_size is not None and info.size < min_size:
|
|
120
|
+
return False
|
|
121
|
+
if max_size is not None and info.size > max_size:
|
|
122
|
+
return False
|
|
123
|
+
if file_types and not info.is_directory:
|
|
124
|
+
return info.file_type.lower() in (t.lower() for t in file_types)
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Define the scan pattern based on recursion
|
|
129
|
+
pattern = '**/*' if recursive else '*'
|
|
130
|
+
|
|
131
|
+
# Scan directory
|
|
132
|
+
for item in self.current_path.glob(pattern):
|
|
133
|
+
try:
|
|
134
|
+
file_info = FileInfo.from_path(item)
|
|
135
|
+
if should_include(file_info):
|
|
136
|
+
contents.append(file_info)
|
|
137
|
+
except (PermissionError, OSError):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
except PermissionError:
|
|
141
|
+
raise PermissionError(f"Permission denied to access directory: {self.current_path}")
|
|
142
|
+
|
|
143
|
+
# Sort results
|
|
144
|
+
sort_key = {
|
|
145
|
+
'name': lambda x: (not x.is_directory, x.name.lower()),
|
|
146
|
+
'date': lambda x: (not x.is_directory, x.last_modified),
|
|
147
|
+
'size': lambda x: (not x.is_directory, x.size),
|
|
148
|
+
'type': lambda x: (not x.is_directory, x.file_type.lower(), x.name.lower())
|
|
149
|
+
}[sort_by]
|
|
150
|
+
|
|
151
|
+
return sorted(contents, key=sort_key, reverse=reverse)
|
|
152
|
+
|
|
153
|
+
def navigate_to(self, path: str) -> bool:
|
|
154
|
+
"""
|
|
155
|
+
Navigate to a new directory path.
|
|
156
|
+
Returns True if navigation was successful, False otherwise.
|
|
157
|
+
"""
|
|
158
|
+
new_path = None
|
|
159
|
+
try:
|
|
160
|
+
new_path = Path(path).expanduser().resolve()
|
|
161
|
+
|
|
162
|
+
if not new_path.exists() or not new_path.is_dir():
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
# Test if we can actually read the directory
|
|
166
|
+
next(new_path.iterdir(), None)
|
|
167
|
+
self.current_path = new_path
|
|
168
|
+
return True
|
|
169
|
+
|
|
170
|
+
except PermissionError:
|
|
171
|
+
if new_path:
|
|
172
|
+
self.current_path = new_path
|
|
173
|
+
return True
|
|
174
|
+
except OSError:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
def navigate_up(self) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Navigate up to the parent directory.
|
|
180
|
+
Returns True if navigation was successful, False otherwise.
|
|
181
|
+
"""
|
|
182
|
+
parent = self.parent_directory
|
|
183
|
+
if parent is None:
|
|
184
|
+
return False
|
|
185
|
+
return self.navigate_to(parent)
|
|
186
|
+
|
|
187
|
+
def navigate_into(self, directory_name: str) -> bool:
|
|
188
|
+
"""
|
|
189
|
+
Navigate into a subdirectory of the current directory.
|
|
190
|
+
Returns True if navigation was successful, False otherwise.
|
|
191
|
+
"""
|
|
192
|
+
new_path = self.current_path / directory_name
|
|
193
|
+
return self.navigate_to(str(new_path))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_files_from_directory(
|
|
197
|
+
dir_name: Union[str, Path],
|
|
198
|
+
types: Optional[List[str]] = None,
|
|
199
|
+
*,
|
|
200
|
+
include_hidden: bool = False,
|
|
201
|
+
recursive: bool = False
|
|
202
|
+
) -> Optional[List[FileInfo]]:
|
|
203
|
+
"""
|
|
204
|
+
Get list of files from a directory with optional type filtering.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
dir_name: Directory path to scan
|
|
208
|
+
types: List of file extensions to include (without dots). None means all types
|
|
209
|
+
include_hidden: Whether to include hidden files
|
|
210
|
+
recursive: Whether to scan subdirectories
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of FileInfo objects or None if directory doesn't exist
|
|
214
|
+
|
|
215
|
+
Example:
|
|
216
|
+
>>> files = get_files_from_directory("/path/to/dir", types=["pdf", "txt"])
|
|
217
|
+
>>> for file in files:
|
|
218
|
+
... print(f"{file.name} - {file.size} bytes")
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
dir_path = Path(dir_name).resolve()
|
|
222
|
+
if not dir_path.exists():
|
|
223
|
+
return None
|
|
224
|
+
if not dir_path.is_dir():
|
|
225
|
+
raise ValueError(f"Path is not a directory: {dir_path}")
|
|
226
|
+
|
|
227
|
+
# Normalize file types
|
|
228
|
+
if types:
|
|
229
|
+
types = [t.lower().lstrip('.') for t in types]
|
|
230
|
+
|
|
231
|
+
files = []
|
|
232
|
+
pattern = '**/*' if recursive else '*'
|
|
233
|
+
|
|
234
|
+
for item in dir_path.glob(pattern):
|
|
235
|
+
try:
|
|
236
|
+
# Skip hidden files unless specifically requested
|
|
237
|
+
if not include_hidden and item.name.startswith('.'):
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
# Skip directories unless recursive is True
|
|
241
|
+
if item.is_dir() and not recursive:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Check file type if types are specified
|
|
245
|
+
if types and not item.is_dir():
|
|
246
|
+
if item.suffix[1:].lower() not in types:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
file_info = FileInfo.from_path(item)
|
|
250
|
+
files.append(file_info)
|
|
251
|
+
|
|
252
|
+
except (PermissionError, OSError):
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
return sorted(files, key=lambda x: (not x.is_directory, x.name.lower()))
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
raise type(e)(f"Error scanning directory {dir_name}: {str(e)}") from e
|
|
259
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
video_types = ['mp4', "webm", "opgg"]
|
|
5
|
+
audio_types = ['mp3', "wav", "ogg", "mpeg", "aac", "3gpp", "3gpp2", "aiff", "x-aiff", "amr", "mpga"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_chunk(start_byte=None, end_byte=None, full_path=None):
|
|
9
|
+
file_size = os.stat(full_path).st_size
|
|
10
|
+
if end_byte:
|
|
11
|
+
length = end_byte + 1 - start_byte
|
|
12
|
+
else:
|
|
13
|
+
length = file_size - start_byte
|
|
14
|
+
with open(full_path, 'rb') as f:
|
|
15
|
+
f.seek(start_byte)
|
|
16
|
+
chunk = f.read(length)
|
|
17
|
+
return chunk, start_byte, length, file_size
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_file(file_path, mimetype):
|
|
21
|
+
f = ""
|
|
22
|
+
range_header = f.headers.get('Range', None)
|
|
23
|
+
start_byte, end_byte = 0, None
|
|
24
|
+
if range_header:
|
|
25
|
+
match = re.search(r'(\d+)-(\d*)', range_header)
|
|
26
|
+
groups = match.groups()
|
|
27
|
+
if groups[0]:
|
|
28
|
+
start_byte = int(groups[0])
|
|
29
|
+
if groups[1]:
|
|
30
|
+
end_byte = int(groups[1])
|
|
31
|
+
|
|
32
|
+
chunk, start, length, file_size = get_chunk(start_byte, end_byte, file_path)
|
|
33
|
+
resp = Response(chunk, 206, mimetype=f'video/{mimetype}',
|
|
34
|
+
content_type=mimetype, direct_passthrough=True)
|
|
35
|
+
print(length)
|
|
36
|
+
resp.headers.add('Content-Range', 'bytes {0}-{1}/{2}'.format(start, start + length - 1, file_size))
|
|
37
|
+
return resp
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def is_media(filepath):
|
|
41
|
+
found_media = re.search("\.mp4$|\.mp3$", filepath, re.IGNORECASE)
|
|
42
|
+
if found_media:
|
|
43
|
+
extension = found_media[0].lower()[1:]
|
|
44
|
+
if found_media in video_types:
|
|
45
|
+
return f"video/{extension}"
|
|
46
|
+
return f"audio/{extension}"
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_file_extension(fname):
|
|
51
|
+
found_extension = re.search("\.[A-Za-z0-9]*$", fname, re.IGNORECASE)
|
|
52
|
+
if found_extension:
|
|
53
|
+
return found_extension[0][1:].lower()
|