Flowfile 0.3.0__py3-none-any.whl → 0.3.0.1__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 +13 -6
- flowfile/__main__.py +51 -15
- flowfile/api.py +379 -0
- flowfile/web/__init__.py +155 -0
- flowfile/web/static/assets/AirbyteReader-1ac35765.css +314 -0
- flowfile/web/static/assets/AirbyteReader-cb0c1d4a.js +921 -0
- flowfile/web/static/assets/CrossJoin-41efa4cb.css +100 -0
- flowfile/web/static/assets/CrossJoin-a514fa59.js +153 -0
- flowfile/web/static/assets/DatabaseConnectionSettings-0c04b2e5.css +77 -0
- flowfile/web/static/assets/DatabaseConnectionSettings-f2cecf33.js +151 -0
- flowfile/web/static/assets/DatabaseManager-30fa27e5.css +64 -0
- flowfile/web/static/assets/DatabaseManager-83ee3c98.js +484 -0
- flowfile/web/static/assets/DatabaseReader-dc0c6881.js +426 -0
- flowfile/web/static/assets/DatabaseReader-f50c6558.css +158 -0
- flowfile/web/static/assets/DatabaseWriter-2f570e53.css +96 -0
- flowfile/web/static/assets/DatabaseWriter-5afe9f8d.js +312 -0
- flowfile/web/static/assets/ExploreData-5bdae813.css +45 -0
- flowfile/web/static/assets/ExploreData-c7ee19cf.js +118306 -0
- flowfile/web/static/assets/ExternalSource-17b23a01.js +225 -0
- flowfile/web/static/assets/ExternalSource-e37b6275.css +94 -0
- flowfile/web/static/assets/Filter-90856b4f.js +238 -0
- flowfile/web/static/assets/Filter-a9d08ba1.css +20 -0
- flowfile/web/static/assets/Formula-38b71e9e.js +197 -0
- flowfile/web/static/assets/Formula-d60a74f4.css +17 -0
- flowfile/web/static/assets/FuzzyMatch-6857de82.css +254 -0
- flowfile/web/static/assets/FuzzyMatch-d0f1fe81.js +422 -0
- flowfile/web/static/assets/GoogleSheet-854294a4.js +2616 -0
- flowfile/web/static/assets/GoogleSheet-92084da7.css +233 -0
- flowfile/web/static/assets/GraphSolver-0c86bbc6.js +382 -0
- flowfile/web/static/assets/GraphSolver-17fd26db.css +68 -0
- flowfile/web/static/assets/GroupBy-ab1ea74b.css +51 -0
- flowfile/web/static/assets/GroupBy-f2772e9f.js +413 -0
- flowfile/web/static/assets/Join-41c0f331.css +109 -0
- flowfile/web/static/assets/Join-bc3e1cf7.js +247 -0
- flowfile/web/static/assets/ManualInput-03aa0245.js +391 -0
- flowfile/web/static/assets/ManualInput-ac7b9972.css +84 -0
- flowfile/web/static/assets/Output-48f81019.css +2642 -0
- flowfile/web/static/assets/Output-5b35eee8.js +536 -0
- flowfile/web/static/assets/Pivot-7164087c.js +408 -0
- flowfile/web/static/assets/Pivot-f415e85f.css +35 -0
- flowfile/web/static/assets/PolarsCode-3abf6507.js +2863 -0
- flowfile/web/static/assets/PolarsCode-650322d1.css +35 -0
- flowfile/web/static/assets/PopOver-b37ff9be.js +577 -0
- flowfile/web/static/assets/PopOver-bccfde04.css +32 -0
- flowfile/web/static/assets/Read-65966a3e.js +701 -0
- flowfile/web/static/assets/Read-80dc1675.css +197 -0
- flowfile/web/static/assets/RecordCount-c66c6d6d.js +121 -0
- flowfile/web/static/assets/RecordId-826dc095.js +339 -0
- flowfile/web/static/assets/Sample-4ed555c8.js +184 -0
- flowfile/web/static/assets/SecretManager-eac1e97d.js +382 -0
- flowfile/web/static/assets/Select-085f05cc.js +231 -0
- flowfile/web/static/assets/SettingsSection-1f5e79c1.js +87 -0
- flowfile/web/static/assets/SettingsSection-9c836ecc.css +47 -0
- flowfile/web/static/assets/Sort-3e6cb414.js +309 -0
- flowfile/web/static/assets/Sort-7ccfa0fe.css +51 -0
- flowfile/web/static/assets/TextToRows-606349bc.js +307 -0
- flowfile/web/static/assets/TextToRows-c92d1ec2.css +48 -0
- flowfile/web/static/assets/UnavailableFields-5edd5322.css +49 -0
- flowfile/web/static/assets/UnavailableFields-b41976ed.js +36 -0
- flowfile/web/static/assets/Union-8d9ac7f9.css +30 -0
- flowfile/web/static/assets/Union-fca91665.js +145 -0
- flowfile/web/static/assets/Unique-a59f830e.js +273 -0
- flowfile/web/static/assets/Unique-b5615727.css +51 -0
- flowfile/web/static/assets/Unpivot-246e9bbd.css +77 -0
- flowfile/web/static/assets/Unpivot-c3815565.js +441 -0
- flowfile/web/static/assets/airbyte-292aa232.png +0 -0
- flowfile/web/static/assets/api-22b338bd.js +60 -0
- flowfile/web/static/assets/cross_join-d30c0290.png +0 -0
- flowfile/web/static/assets/database_reader-ce1e55f3.svg +24 -0
- flowfile/web/static/assets/database_writer-b4ad0753.svg +23 -0
- flowfile/web/static/assets/designer-2394122a.css +10697 -0
- flowfile/web/static/assets/designer-e5bbe26f.js +69712 -0
- flowfile/web/static/assets/documentation-08045cf2.js +33 -0
- flowfile/web/static/assets/documentation-12216a74.css +50 -0
- flowfile/web/static/assets/dropDown-35135ba8.css +143 -0
- flowfile/web/static/assets/dropDown-5e7e9a5a.js +319 -0
- flowfile/web/static/assets/dropDownGeneric-50a91b99.js +72 -0
- flowfile/web/static/assets/dropDownGeneric-895680d6.css +10 -0
- flowfile/web/static/assets/element-icons-9c88a535.woff +0 -0
- flowfile/web/static/assets/element-icons-de5eb258.ttf +0 -0
- flowfile/web/static/assets/explore_data-8a0a2861.png +0 -0
- flowfile/web/static/assets/fa-brands-400-808443ae.ttf +0 -0
- flowfile/web/static/assets/fa-brands-400-d7236a19.woff2 +0 -0
- flowfile/web/static/assets/fa-regular-400-54cf6086.ttf +0 -0
- flowfile/web/static/assets/fa-regular-400-e3456d12.woff2 +0 -0
- flowfile/web/static/assets/fa-solid-900-aa759986.woff2 +0 -0
- flowfile/web/static/assets/fa-solid-900-d2f05935.ttf +0 -0
- flowfile/web/static/assets/fa-v4compatibility-0ce9033c.woff2 +0 -0
- flowfile/web/static/assets/fa-v4compatibility-30f6abf6.ttf +0 -0
- flowfile/web/static/assets/filter-d7708bda.png +0 -0
- flowfile/web/static/assets/formula-eeeb1611.png +0 -0
- flowfile/web/static/assets/fullEditor-178376bb.css +256 -0
- flowfile/web/static/assets/fullEditor-705c6ccb.js +630 -0
- flowfile/web/static/assets/fuzzy_match-40c161b2.png +0 -0
- flowfile/web/static/assets/genericNodeSettings-65587f20.js +137 -0
- flowfile/web/static/assets/genericNodeSettings-924759c7.css +46 -0
- flowfile/web/static/assets/graph_solver-8b7888b8.png +0 -0
- flowfile/web/static/assets/group_by-80561fc3.png +0 -0
- flowfile/web/static/assets/index-552863fd.js +58652 -0
- flowfile/web/static/assets/index-681a3ed0.css +8843 -0
- flowfile/web/static/assets/input_data-ab2eb678.png +0 -0
- flowfile/web/static/assets/join-349043ae.png +0 -0
- flowfile/web/static/assets/manual_input-ae98f31d.png +0 -0
- flowfile/web/static/assets/nodeTitle-cf9bae3c.js +227 -0
- flowfile/web/static/assets/nodeTitle-f4b12bcb.css +134 -0
- flowfile/web/static/assets/old_join-5d0eb604.png +0 -0
- flowfile/web/static/assets/output-06ec0371.png +0 -0
- flowfile/web/static/assets/pivot-9660df51.png +0 -0
- flowfile/web/static/assets/polars_code-05ce5dc6.png +0 -0
- flowfile/web/static/assets/record_count-dab44eb5.png +0 -0
- flowfile/web/static/assets/record_id-0b15856b.png +0 -0
- flowfile/web/static/assets/sample-693a88b5.png +0 -0
- flowfile/web/static/assets/secretApi-3ad510e1.js +46 -0
- flowfile/web/static/assets/select-b0d0437a.png +0 -0
- flowfile/web/static/assets/selectDynamic-b062bc9b.css +107 -0
- flowfile/web/static/assets/selectDynamic-bd644891.js +302 -0
- flowfile/web/static/assets/sort-2aa579f0.png +0 -0
- flowfile/web/static/assets/summarize-2a099231.png +0 -0
- flowfile/web/static/assets/text_to_rows-859b29ea.png +0 -0
- flowfile/web/static/assets/union-2d8609f4.png +0 -0
- flowfile/web/static/assets/unique-1958b98a.png +0 -0
- flowfile/web/static/assets/unpivot-d3cb4b5b.png +0 -0
- flowfile/web/static/assets/view-7a0f0be1.png +0 -0
- flowfile/web/static/assets/vue-codemirror.esm-dd17b478.js +22281 -0
- flowfile/web/static/assets/vue-content-loader.es-6b36f05e.js +210 -0
- flowfile/web/static/flowfile.svg +47 -0
- flowfile/web/static/icons/flowfile.png +0 -0
- flowfile/web/static/images/airbyte.png +0 -0
- flowfile/web/static/images/flowfile.svg +47 -0
- flowfile/web/static/images/google.svg +1 -0
- flowfile/web/static/images/sheets.png +0 -0
- flowfile/web/static/index.html +22 -0
- flowfile/web/static/vite.svg +1 -0
- flowfile/web/static/vue.svg +1 -0
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/METADATA +1 -1
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/RECORD +146 -15
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/entry_points.txt +1 -1
- flowfile_core/configs/settings.py +7 -32
- flowfile_core/flowfile/FlowfileFlow.py +4 -2
- flowfile_core/flowfile/analytics/analytics_processor.py +1 -1
- flowfile_core/main.py +4 -1
- flowfile_core/schemas/input_schema.py +1 -8
- flowfile_frame/__init__.py +0 -1
- flowfile_frame/utils.py +0 -139
- flowfile_frame/__main__.py +0 -12
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/LICENSE +0 -0
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/WHEEL +0 -0
flowfile/__init__.py
CHANGED
|
@@ -7,9 +7,16 @@ This package ties together the FlowFile ecosystem components:
|
|
|
7
7
|
- flowfile_worker: Computation engine
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
__version__ = "0.
|
|
10
|
+
__version__ = "0.3.1"
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import os
|
|
13
|
+
import logging
|
|
14
|
+
|
|
15
|
+
os.environ['WORKER_PORT'] = "63578"
|
|
16
|
+
os.environ['SINGLE_FILE_MODE'] = "1"
|
|
17
|
+
|
|
18
|
+
from flowfile.web import start_server as start_web_ui
|
|
19
|
+
from flowfile.api import open_graph_in_editor
|
|
13
20
|
from flowfile_frame.flow_frame import (
|
|
14
21
|
FlowFrame, read_csv, read_parquet, from_dict, concat
|
|
15
22
|
)
|
|
@@ -18,7 +25,7 @@ from flowfile_frame.expr import (
|
|
|
18
25
|
sum, min, max, mean, count, when
|
|
19
26
|
)
|
|
20
27
|
from flowfile_frame.group_frame import GroupByFrame
|
|
21
|
-
from flowfile_frame.utils import create_flow_graph
|
|
28
|
+
from flowfile_frame.utils import create_flow_graph
|
|
22
29
|
from flowfile_frame.selectors import (
|
|
23
30
|
numeric, float_, integer, string, temporal,
|
|
24
31
|
datetime, date, time, duration, boolean,
|
|
@@ -26,7 +33,6 @@ from flowfile_frame.selectors import (
|
|
|
26
33
|
by_dtype, contains, starts_with, ends_with, matches
|
|
27
34
|
)
|
|
28
35
|
|
|
29
|
-
# Import Polars data types for convenience
|
|
30
36
|
from polars.datatypes import (
|
|
31
37
|
Int8, Int16, Int32, Int64, Int128,
|
|
32
38
|
UInt8, UInt16, UInt32, UInt64,
|
|
@@ -38,7 +44,6 @@ from polars.datatypes import (
|
|
|
38
44
|
DataType, DataTypeClass, Field
|
|
39
45
|
)
|
|
40
46
|
|
|
41
|
-
# Define what's publicly available from the package
|
|
42
47
|
__all__ = [
|
|
43
48
|
# Core FlowFrame classes
|
|
44
49
|
'FlowFrame', 'GroupByFrame',
|
|
@@ -68,4 +73,6 @@ __all__ = [
|
|
|
68
73
|
'Date', 'Time', 'Datetime', 'Duration',
|
|
69
74
|
'Categorical', 'Decimal', 'Enum', 'Unknown',
|
|
70
75
|
'DataType', 'DataTypeClass', 'Field',
|
|
71
|
-
|
|
76
|
+
'start_web_ui'
|
|
77
|
+
]
|
|
78
|
+
logging.getLogger("PipelineHandler").setLevel(logging.WARNING)
|
flowfile/__main__.py
CHANGED
|
@@ -1,24 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
Main entry point for the FlowFile package.
|
|
3
|
-
"""
|
|
4
|
-
|
|
1
|
+
# flowfile/__main__.py
|
|
5
2
|
|
|
6
3
|
def main():
|
|
7
4
|
"""
|
|
8
5
|
Display information about FlowFile when run directly as a module.
|
|
9
6
|
"""
|
|
10
7
|
import flowfile
|
|
8
|
+
import argparse
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
print("\nFor visual ETL:")
|
|
20
|
-
print(" ff.open_graph_in_editor(result.to_graph())")
|
|
10
|
+
parser = argparse.ArgumentParser(description="FlowFile: A visual ETL tool with a Polars-like API")
|
|
11
|
+
parser.add_argument("command", nargs="?", choices=["run"], help="Command to execute")
|
|
12
|
+
parser.add_argument("component", nargs="?", choices=["web", "core", "worker"],
|
|
13
|
+
help="Component to run (web, core, or worker)")
|
|
14
|
+
parser.add_argument("--host", default="127.0.0.1", help="Host to bind the server to")
|
|
15
|
+
parser.add_argument("--port", type=int, default=63578, help="Port to bind the server to")
|
|
16
|
+
parser.add_argument("--no-browser", action="store_true", help="Don't open a browser window")
|
|
21
17
|
|
|
18
|
+
# Parse arguments
|
|
19
|
+
args = parser.parse_args()
|
|
22
20
|
|
|
23
|
-
if
|
|
24
|
-
|
|
21
|
+
if args.command == "run" and args.component:
|
|
22
|
+
if args.component == "web":
|
|
23
|
+
try:
|
|
24
|
+
flowfile.start_web_ui(
|
|
25
|
+
host=args.host,
|
|
26
|
+
port=args.port,
|
|
27
|
+
open_browser=not args.no_browser
|
|
28
|
+
)
|
|
29
|
+
except KeyboardInterrupt:
|
|
30
|
+
print("\nFlowFile service stopped.")
|
|
31
|
+
elif args.component == "core":
|
|
32
|
+
# Only for direct core service usage
|
|
33
|
+
from flowfile_core.main import run as run_core
|
|
34
|
+
run_core(host=args.host, port=args.port)
|
|
35
|
+
elif args.component == "worker":
|
|
36
|
+
# Only for direct worker service usage
|
|
37
|
+
from flowfile_worker.main import run as run_worker
|
|
38
|
+
run_worker(host=args.host, port=args.port)
|
|
39
|
+
else:
|
|
40
|
+
# Default action - show info
|
|
41
|
+
print(f"FlowFile v{flowfile.__version__}")
|
|
42
|
+
print("A framework combining visual ETL with a Polars-like API")
|
|
43
|
+
print("\nUsage:")
|
|
44
|
+
print(" # Start the FlowFile web UI with integrated services")
|
|
45
|
+
print(" flowfile run web")
|
|
46
|
+
print("")
|
|
47
|
+
print(" # Advanced: Run individual components")
|
|
48
|
+
print(" flowfile run core # Start only the core service")
|
|
49
|
+
print(" flowfile run worker # Start only the worker service")
|
|
50
|
+
print("")
|
|
51
|
+
print(" # Options")
|
|
52
|
+
print(" flowfile run web --host 0.0.0.0 --port 8080 # Custom host/port")
|
|
53
|
+
print(" flowfile run web --no-browser # Don't open browser")
|
|
54
|
+
print("")
|
|
55
|
+
print(" # Python API usage examples")
|
|
56
|
+
print(" import flowfile as ff")
|
|
57
|
+
print(" df = ff.read_csv('data.csv')")
|
|
58
|
+
print(" result = df.filter(ff.col('value') > 10)")
|
|
59
|
+
print(" ff.open_graph_in_editor(result)")
|
|
60
|
+
print(" ff.start_web_ui()")
|
flowfile/api.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# flowfile/api.py
|
|
2
|
+
import uuid
|
|
3
|
+
import time
|
|
4
|
+
import os
|
|
5
|
+
import requests
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import atexit
|
|
9
|
+
import logging
|
|
10
|
+
import webbrowser
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Dict, Any, Union, Tuple, List
|
|
14
|
+
from subprocess import Popen
|
|
15
|
+
from flowfile_core.flowfile.FlowfileFlow import FlowGraph
|
|
16
|
+
from tempfile import TemporaryDirectory
|
|
17
|
+
|
|
18
|
+
# Configuration
|
|
19
|
+
FLOWFILE_HOST: str = os.environ.get("FLOWFILE_HOST", "127.0.0.1")
|
|
20
|
+
FLOWFILE_PORT: int = int(os.environ.get("FLOWFILE_PORT", 63578))
|
|
21
|
+
FLOWFILE_BASE_URL: str = f"http://{FLOWFILE_HOST}:{FLOWFILE_PORT}"
|
|
22
|
+
DEFAULT_MODULE_NAME: str = os.environ.get("FLOWFILE_MODULE_NAME", "flowfile")
|
|
23
|
+
FORCE_POETRY: bool = os.environ.get("FORCE_POETRY", "").lower() in ("true", "1", "yes")
|
|
24
|
+
POETRY_PATH: str = os.environ.get("POETRY_PATH", "poetry")
|
|
25
|
+
|
|
26
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Global variable to track the managed server process
|
|
29
|
+
_server_process: Optional[Popen] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_flowfile_running() -> bool:
|
|
33
|
+
"""Check if the Flowfile core API endpoint is responsive."""
|
|
34
|
+
try:
|
|
35
|
+
response: requests.Response = requests.get(f"{FLOWFILE_BASE_URL}/docs", timeout=1)
|
|
36
|
+
return 200 <= response.status_code < 300
|
|
37
|
+
except (requests.ConnectionError, requests.Timeout):
|
|
38
|
+
return False
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.error(f"Unexpected error checking Flowfile status: {e}")
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def stop_flowfile_server_process() -> None:
|
|
45
|
+
"""Stop the Flowfile server process if it was started by this module."""
|
|
46
|
+
global _server_process
|
|
47
|
+
if _server_process and _server_process.poll() is None:
|
|
48
|
+
logger.info(f"Stopping managed Flowfile server process (PID: {_server_process.pid})...")
|
|
49
|
+
_server_process.terminate()
|
|
50
|
+
try:
|
|
51
|
+
_server_process.wait(timeout=5)
|
|
52
|
+
logger.info("Server process terminated gracefully.")
|
|
53
|
+
except subprocess.TimeoutExpired:
|
|
54
|
+
logger.warning("Server process did not terminate gracefully, killing...")
|
|
55
|
+
_server_process.kill()
|
|
56
|
+
_server_process.wait()
|
|
57
|
+
logger.info("Server process killed.")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Error during server process termination: {e}")
|
|
60
|
+
finally:
|
|
61
|
+
_server_process = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_poetry_environment() -> bool:
|
|
65
|
+
"""
|
|
66
|
+
Detect if we're running in a Poetry environment by checking:
|
|
67
|
+
1. If pyproject.toml exists up the directory tree
|
|
68
|
+
2. If VIRTUAL_ENV points to a poetry environment
|
|
69
|
+
3. If POETRY_ACTIVE environment variable is set
|
|
70
|
+
"""
|
|
71
|
+
# Check if explicitly set via env variable
|
|
72
|
+
if FORCE_POETRY:
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
# Check if POETRY_ACTIVE is set
|
|
76
|
+
if os.environ.get("POETRY_ACTIVE", "").lower() in ("true", "1", "yes"):
|
|
77
|
+
return True
|
|
78
|
+
|
|
79
|
+
# Check if we're in a poetry virtual environment
|
|
80
|
+
venv_path = os.environ.get("VIRTUAL_ENV", "")
|
|
81
|
+
if venv_path and (
|
|
82
|
+
"poetry" in venv_path.lower() or
|
|
83
|
+
Path(venv_path).joinpath(".poetry-venv").exists()
|
|
84
|
+
):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
# Look for pyproject.toml with poetry section
|
|
88
|
+
cwd = Path.cwd()
|
|
89
|
+
for parent in [cwd, *cwd.parents]:
|
|
90
|
+
pyproject = parent / "pyproject.toml"
|
|
91
|
+
if pyproject.exists():
|
|
92
|
+
with open(pyproject, "r") as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
if "[tool.poetry]" in content:
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_command_available(command: str) -> bool:
|
|
101
|
+
"""Check if a command is available in the PATH."""
|
|
102
|
+
return shutil.which(command) is not None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def build_server_command(module_name: str) -> List[str]:
|
|
106
|
+
"""
|
|
107
|
+
Build the appropriate command to start the server based on environment detection.
|
|
108
|
+
Tries Poetry first if in a Poetry environment, falls back to direct module execution.
|
|
109
|
+
"""
|
|
110
|
+
command: List[str] = []
|
|
111
|
+
|
|
112
|
+
# Case 1: Check if we're in a Poetry environment
|
|
113
|
+
if is_poetry_environment():
|
|
114
|
+
logger.info("Poetry environment detected.")
|
|
115
|
+
if is_command_available(POETRY_PATH):
|
|
116
|
+
logger.info(f"Using Poetry to run {module_name}")
|
|
117
|
+
command = [
|
|
118
|
+
POETRY_PATH,
|
|
119
|
+
"run",
|
|
120
|
+
module_name,
|
|
121
|
+
"run",
|
|
122
|
+
"web",
|
|
123
|
+
"--no-browser",
|
|
124
|
+
f"--port={FLOWFILE_PORT}",
|
|
125
|
+
]
|
|
126
|
+
return command
|
|
127
|
+
else:
|
|
128
|
+
logger.warning(f"Poetry command not found at '{POETRY_PATH}'. Falling back to Python module.")
|
|
129
|
+
|
|
130
|
+
# Case 2: Try direct module execution
|
|
131
|
+
logger.info(f"Using Python module approach with {module_name}")
|
|
132
|
+
command = [
|
|
133
|
+
sys.executable,
|
|
134
|
+
"-m",
|
|
135
|
+
module_name,
|
|
136
|
+
"run",
|
|
137
|
+
"web",
|
|
138
|
+
"--no-browser",
|
|
139
|
+
f"--port={FLOWFILE_PORT}",
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
return command
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def start_flowfile_server_process(module_name: str = DEFAULT_MODULE_NAME) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Start the Flowfile server as a background process if it's not already running.
|
|
148
|
+
Automatically detects and uses Poetry if in a Poetry environment.
|
|
149
|
+
|
|
150
|
+
Parameters:
|
|
151
|
+
module_name: The module name to run. Defaults to the value from environment
|
|
152
|
+
variable or "flowfile".
|
|
153
|
+
"""
|
|
154
|
+
global _server_process
|
|
155
|
+
if is_flowfile_running():
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
if _server_process and _server_process.poll() is None:
|
|
159
|
+
logger.warning("Server process object exists but API not responding. Attempting to restart.")
|
|
160
|
+
stop_flowfile_server_process()
|
|
161
|
+
|
|
162
|
+
# Build command automatically based on environment detection
|
|
163
|
+
command = build_server_command(module_name)
|
|
164
|
+
logger.info(f"Starting server with command: {' '.join(command)}")
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
_server_process = Popen(
|
|
168
|
+
command,
|
|
169
|
+
stdout=subprocess.DEVNULL,
|
|
170
|
+
stderr=subprocess.PIPE,
|
|
171
|
+
)
|
|
172
|
+
logger.info(f"Started server process with PID: {_server_process.pid}")
|
|
173
|
+
|
|
174
|
+
atexit.register(stop_flowfile_server_process)
|
|
175
|
+
|
|
176
|
+
logger.info("Waiting for server to initialize...")
|
|
177
|
+
|
|
178
|
+
for i in range(10):
|
|
179
|
+
time.sleep(1)
|
|
180
|
+
if is_flowfile_running():
|
|
181
|
+
logger.info("Server started successfully.")
|
|
182
|
+
return True
|
|
183
|
+
else:
|
|
184
|
+
logger.error("Failed to start server: API did not become responsive.")
|
|
185
|
+
if _server_process and _server_process.stderr:
|
|
186
|
+
try:
|
|
187
|
+
stderr_output: str = _server_process.stderr.read().decode(errors='ignore')
|
|
188
|
+
logger.error(f"Server process stderr:\n{stderr_output[:1000]}...")
|
|
189
|
+
except Exception as read_err:
|
|
190
|
+
logger.error(f"Could not read stderr from server process: {read_err}")
|
|
191
|
+
stop_flowfile_server_process()
|
|
192
|
+
return False
|
|
193
|
+
else:
|
|
194
|
+
stop_flowfile_server_process()
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
except FileNotFoundError:
|
|
198
|
+
logger.error(f"Error: Could not execute command: '{' '.join(command)}'.")
|
|
199
|
+
logger.error(f"Ensure '{module_name}' is installed correctly.")
|
|
200
|
+
_server_process = None
|
|
201
|
+
return False
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"An unexpected error occurred while starting the server process: {e}")
|
|
204
|
+
if _server_process and _server_process.stderr:
|
|
205
|
+
try:
|
|
206
|
+
stderr_output = _server_process.stderr.read().decode(errors='ignore')
|
|
207
|
+
logger.error(f"Server process stderr:\n{stderr_output[:1000]}...")
|
|
208
|
+
except Exception as read_err:
|
|
209
|
+
logger.error(f"Could not read stderr from server process: {read_err}")
|
|
210
|
+
stop_flowfile_server_process()
|
|
211
|
+
_server_process = None
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def get_auth_token() -> Optional[str]:
|
|
216
|
+
"""Get an authentication token from the Flowfile API."""
|
|
217
|
+
try:
|
|
218
|
+
response: requests.Response = requests.post(
|
|
219
|
+
f"{FLOWFILE_BASE_URL}/auth/token",
|
|
220
|
+
json={},
|
|
221
|
+
timeout=5
|
|
222
|
+
)
|
|
223
|
+
response.raise_for_status()
|
|
224
|
+
token_data: Dict[str, Any] = response.json()
|
|
225
|
+
access_token: Optional[str] = token_data.get("access_token")
|
|
226
|
+
if not access_token:
|
|
227
|
+
logger.error("Auth token endpoint succeeded but 'access_token' was missing in response.")
|
|
228
|
+
return None
|
|
229
|
+
return access_token
|
|
230
|
+
|
|
231
|
+
except requests.exceptions.RequestException as e:
|
|
232
|
+
logger.error(f"Failed to get auth token: {e}")
|
|
233
|
+
return None
|
|
234
|
+
except Exception as e:
|
|
235
|
+
logger.error(f"An unexpected error occurred getting auth token: {e}")
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def import_flow_to_editor(flow_path: Path, auth_token: str) -> Optional[int]:
|
|
240
|
+
"""Import the flow into the Flowfile editor using the API endpoint."""
|
|
241
|
+
if not flow_path.is_file():
|
|
242
|
+
logger.error(f"Flow file not found: {flow_path}")
|
|
243
|
+
return None
|
|
244
|
+
if not auth_token:
|
|
245
|
+
logger.error("Cannot import flow without auth token.")
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
headers: Dict[str, str] = {"Authorization": f"Bearer {auth_token}"}
|
|
250
|
+
params: Dict[str, str] = {"flow_path": str(flow_path)}
|
|
251
|
+
|
|
252
|
+
response: requests.Response = requests.get(
|
|
253
|
+
f"{FLOWFILE_BASE_URL}/import_flow/",
|
|
254
|
+
params=params,
|
|
255
|
+
headers=headers,
|
|
256
|
+
timeout=10
|
|
257
|
+
)
|
|
258
|
+
response.raise_for_status()
|
|
259
|
+
|
|
260
|
+
flow_id_data: Union[int, Dict[str, Any], Any] = response.json()
|
|
261
|
+
flow_id: Optional[int] = None
|
|
262
|
+
|
|
263
|
+
if isinstance(flow_id_data, int):
|
|
264
|
+
flow_id = flow_id_data
|
|
265
|
+
elif isinstance(flow_id_data, dict) and "flow_id" in flow_id_data:
|
|
266
|
+
flow_id = int(flow_id_data["flow_id"])
|
|
267
|
+
else:
|
|
268
|
+
logger.error(f"Unexpected response format from import_flow endpoint: {flow_id_data}")
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
logger.info(f"Flow '{flow_path.name}' imported successfully with ID: {flow_id}")
|
|
272
|
+
return flow_id
|
|
273
|
+
|
|
274
|
+
except requests.exceptions.RequestException as e:
|
|
275
|
+
logger.error(f"Failed to import flow: {e}")
|
|
276
|
+
if e.response is not None:
|
|
277
|
+
logger.error(f"Server response: {e.response.status_code} - {e.response.text[:500]}")
|
|
278
|
+
return None
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error(f"An unexpected error occurred importing flow: {e}")
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _save_flow_to_location(
|
|
285
|
+
flow_graph: FlowGraph, storage_location: Optional[str]
|
|
286
|
+
) -> Tuple[Optional[Path], Optional[TemporaryDirectory]]:
|
|
287
|
+
"""Handles graph saving, path resolution, and temporary directory creation."""
|
|
288
|
+
temp_dir_obj: Optional[TemporaryDirectory] = None
|
|
289
|
+
flow_file_path: Path
|
|
290
|
+
try:
|
|
291
|
+
if storage_location:
|
|
292
|
+
flow_file_path = Path(storage_location).resolve()
|
|
293
|
+
flow_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
else:
|
|
295
|
+
temp_dir_obj = TemporaryDirectory(prefix="flowfile_graph_")
|
|
296
|
+
flow_file_path = Path(temp_dir_obj.name) / f"temp_flow_{uuid.uuid4().hex[:8]}.flowfile"
|
|
297
|
+
|
|
298
|
+
logger.info(f"Applying layout and saving flow to: {flow_file_path}")
|
|
299
|
+
flow_graph.apply_layout()
|
|
300
|
+
flow_graph.save_flow(str(flow_file_path))
|
|
301
|
+
logger.info("Flow saved successfully.")
|
|
302
|
+
return flow_file_path, temp_dir_obj
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"Failed to save flow graph to location '{storage_location}': {e}", exc_info=True)
|
|
305
|
+
if temp_dir_obj:
|
|
306
|
+
try:
|
|
307
|
+
temp_dir_obj.cleanup()
|
|
308
|
+
logger.info(f"Cleaned up temp dir {temp_dir_obj.name} after save failure.")
|
|
309
|
+
except Exception as cleanup_err:
|
|
310
|
+
logger.error(f"Error during immediate cleanup of temp dir: {cleanup_err}")
|
|
311
|
+
return None, None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _open_flow_in_browser(flow_id: int) -> None:
|
|
315
|
+
"""Opens the specified flow ID in a browser tab if in unified mode."""
|
|
316
|
+
if os.environ.get("FLOWFILE_MODE") == "electron":
|
|
317
|
+
flow_url = f"http://{FLOWFILE_HOST}:{FLOWFILE_PORT}/web/flow/{flow_id}"
|
|
318
|
+
logger.info(f"Unified mode detected. Opening imported flow in browser: {flow_url}")
|
|
319
|
+
try:
|
|
320
|
+
time.sleep(0.5)
|
|
321
|
+
webbrowser.open_new_tab(flow_url)
|
|
322
|
+
except Exception as wb_err:
|
|
323
|
+
logger.warning(f"Could not automatically open browser tab: {wb_err}")
|
|
324
|
+
else:
|
|
325
|
+
logger.info("Not in unified mode ('electron'), browser will not be opened automatically.")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _cleanup_temporary_storage(temp_dir_obj: Optional[TemporaryDirectory]) -> None:
|
|
329
|
+
"""Safely cleans up the temporary directory if one was created."""
|
|
330
|
+
if temp_dir_obj:
|
|
331
|
+
try:
|
|
332
|
+
temp_dir_obj.cleanup()
|
|
333
|
+
logger.info(f"Cleaned up temporary directory: {temp_dir_obj.name}")
|
|
334
|
+
except Exception as e:
|
|
335
|
+
logger.error(f"Error cleaning up temporary directory {temp_dir_obj.name}: {e}")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def open_graph_in_editor(flow_graph: FlowGraph, storage_location: Optional[str] = None, module_name: str = DEFAULT_MODULE_NAME) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Save the ETL graph, ensure the Flowfile server is running (starting it
|
|
341
|
+
if necessary), import the graph via API, and open it in a new browser
|
|
342
|
+
tab if running in unified mode.
|
|
343
|
+
|
|
344
|
+
Parameters:
|
|
345
|
+
flow_graph: The FlowGraph object to save and open.
|
|
346
|
+
storage_location: Optional path to save the .flowfile. If None,
|
|
347
|
+
a temporary file is used.
|
|
348
|
+
module_name: The module name to run if server needs to be started.
|
|
349
|
+
Use your Poetry package name if not using "flowfile".
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
True if the graph was successfully imported, False otherwise.
|
|
353
|
+
"""
|
|
354
|
+
temp_dir_obj: Optional[TemporaryDirectory] = None
|
|
355
|
+
try:
|
|
356
|
+
flow_file_path, temp_dir_obj = _save_flow_to_location(flow_graph, storage_location)
|
|
357
|
+
if not flow_file_path:
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
if not start_flowfile_server_process(module_name):
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
auth_token = get_auth_token()
|
|
364
|
+
if not auth_token:
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
flow_id = import_flow_to_editor(flow_file_path, auth_token)
|
|
368
|
+
|
|
369
|
+
if flow_id is not None:
|
|
370
|
+
_open_flow_in_browser(flow_id)
|
|
371
|
+
return True
|
|
372
|
+
else:
|
|
373
|
+
return False
|
|
374
|
+
|
|
375
|
+
except Exception as e:
|
|
376
|
+
logger.error(f"An unexpected error occurred in open_graph_in_editor: {e}", exc_info=True)
|
|
377
|
+
return False
|
|
378
|
+
finally:
|
|
379
|
+
_cleanup_temporary_storage(temp_dir_obj)
|
flowfile/web/__init__.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
flowfile/web/__init__.py
|
|
3
|
+
Web interface for Flowfile.
|
|
4
|
+
Extends the flowfile_core FastAPI app to serve the Vue.js frontend
|
|
5
|
+
and includes worker functionality.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import webbrowser
|
|
11
|
+
import asyncio
|
|
12
|
+
from fastapi import FastAPI, Response
|
|
13
|
+
from fastapi.staticfiles import StaticFiles
|
|
14
|
+
from fastapi.responses import FileResponse, RedirectResponse
|
|
15
|
+
|
|
16
|
+
static_dir = Path(__file__).parent / "static"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extend_app(app: FastAPI):
|
|
20
|
+
"""
|
|
21
|
+
Extend the flowfile_core FastAPI app with routes to serve the Vue.js frontend
|
|
22
|
+
and worker functionality.
|
|
23
|
+
"""
|
|
24
|
+
# Serve static files if the directory exists
|
|
25
|
+
if static_dir.exists():
|
|
26
|
+
# Mount the assets directory
|
|
27
|
+
if (static_dir / "assets").exists():
|
|
28
|
+
app.mount("/assets", StaticFiles(directory=str(static_dir / "assets")), name="assets")
|
|
29
|
+
|
|
30
|
+
# Mount other common directories
|
|
31
|
+
for dir_name in ["css", "js", "img", "fonts", "icons", "images"]:
|
|
32
|
+
dir_path = static_dir / dir_name
|
|
33
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
34
|
+
app.mount(f"/{dir_name}", StaticFiles(directory=str(dir_path)), name=dir_name)
|
|
35
|
+
|
|
36
|
+
@app.get("/favicon.ico", include_in_schema=False)
|
|
37
|
+
async def favicon():
|
|
38
|
+
"""Serve the favicon.ico file"""
|
|
39
|
+
favicon_path = static_dir / "favicon.ico"
|
|
40
|
+
if favicon_path.exists():
|
|
41
|
+
return FileResponse(favicon_path)
|
|
42
|
+
return Response(status_code=404)
|
|
43
|
+
|
|
44
|
+
@app.get("/flowfile.svg", include_in_schema=False)
|
|
45
|
+
async def svg_logo():
|
|
46
|
+
"""Serve the SVG logo file"""
|
|
47
|
+
svg_path = static_dir / "flowfile.svg"
|
|
48
|
+
if svg_path.exists():
|
|
49
|
+
return FileResponse(svg_path, media_type="image/svg+xml")
|
|
50
|
+
return Response(status_code=404)
|
|
51
|
+
|
|
52
|
+
@app.get("/test")
|
|
53
|
+
async def get_worker_host():
|
|
54
|
+
from flowfile_core.configs.settings import WORKER_URL
|
|
55
|
+
return WORKER_URL
|
|
56
|
+
|
|
57
|
+
@app.get("/web", include_in_schema=False)
|
|
58
|
+
async def web_ui_root():
|
|
59
|
+
"""Serve the main index.html file for the web UI"""
|
|
60
|
+
index_path = static_dir / "index.html"
|
|
61
|
+
if index_path.exists():
|
|
62
|
+
return FileResponse(index_path)
|
|
63
|
+
return {"error": "Web UI not installed. Build the frontend and install it in the package."}
|
|
64
|
+
|
|
65
|
+
@app.get("/web/{path:path}", include_in_schema=False)
|
|
66
|
+
async def serve_vue_app(path: str):
|
|
67
|
+
"""Serve static files or the index.html for client-side routing"""
|
|
68
|
+
# Try to serve the requested file
|
|
69
|
+
file_path = static_dir / path
|
|
70
|
+
if file_path.exists() and file_path.is_file():
|
|
71
|
+
return FileResponse(file_path)
|
|
72
|
+
|
|
73
|
+
# If it's a directory, redirect to add trailing slash
|
|
74
|
+
if (static_dir / path).exists() and (static_dir / path).is_dir():
|
|
75
|
+
return RedirectResponse(f"/web/{path}/")
|
|
76
|
+
|
|
77
|
+
# For client-side routing, serve the index.html
|
|
78
|
+
index_path = static_dir / "index.html"
|
|
79
|
+
if index_path.exists():
|
|
80
|
+
return FileResponse(index_path)
|
|
81
|
+
|
|
82
|
+
return {"error": f"File not found: {path}"}
|
|
83
|
+
|
|
84
|
+
# Include worker routes if simplified mode is enabled
|
|
85
|
+
include_worker_routes(app)
|
|
86
|
+
|
|
87
|
+
return app
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def include_worker_routes(app: FastAPI):
|
|
91
|
+
"""
|
|
92
|
+
Include worker routes from flowfile_worker for simplified deployments.
|
|
93
|
+
This creates a unified API that serves both the web UI and processes the worker operations.
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
# Import worker modules
|
|
97
|
+
from flowfile_worker.routes import router as worker_router
|
|
98
|
+
from flowfile_worker import mp_context, CACHE_DIR
|
|
99
|
+
|
|
100
|
+
# Add lifecycle event handler for worker cleanup
|
|
101
|
+
@app.on_event("shutdown")
|
|
102
|
+
async def shutdown_worker():
|
|
103
|
+
"""Clean up worker resources on shutdown"""
|
|
104
|
+
print("Cleaning up worker resources...")
|
|
105
|
+
for p in mp_context.active_children():
|
|
106
|
+
try:
|
|
107
|
+
p.terminate()
|
|
108
|
+
p.join()
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"Error cleaning up process: {e}")
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
CACHE_DIR.cleanup()
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"Error cleaning up cache directory: {e}")
|
|
116
|
+
|
|
117
|
+
await asyncio.sleep(0.1)
|
|
118
|
+
|
|
119
|
+
# Include the worker router with a prefix
|
|
120
|
+
app.include_router(worker_router, prefix="/worker")
|
|
121
|
+
|
|
122
|
+
print("Worker functionality included in unified API")
|
|
123
|
+
|
|
124
|
+
except ImportError as e:
|
|
125
|
+
print(f"Worker module could not be imported, running without worker functionality: {e}")
|
|
126
|
+
print("This is normal for lightweight deployments that don't need data processing.")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def start_server(host="127.0.0.1", port=63578, open_browser=True):
|
|
130
|
+
"""
|
|
131
|
+
Start the flowfile_core FastAPI app with the web UI routes and worker functionality.
|
|
132
|
+
This function is a wrapper around flowfile_core.main.run().
|
|
133
|
+
"""
|
|
134
|
+
# Set electron mode
|
|
135
|
+
os.environ["FLOWFILE_MODE"] = "electron"
|
|
136
|
+
|
|
137
|
+
# Import core app
|
|
138
|
+
from flowfile_core.main import run, app as core_app
|
|
139
|
+
|
|
140
|
+
# Extend the core app with web UI routes and worker functionality
|
|
141
|
+
extend_app(core_app)
|
|
142
|
+
|
|
143
|
+
# Open browser if requested
|
|
144
|
+
if open_browser:
|
|
145
|
+
time.sleep(2)
|
|
146
|
+
webbrowser.open_new_tab(f"http://{host}:{port}/web")
|
|
147
|
+
|
|
148
|
+
print("\n" + "=" * 60)
|
|
149
|
+
print(" FlowFile - Visual ETL Tool (Unified Mode)")
|
|
150
|
+
print(f" Web UI: http://{host}:{port}/web")
|
|
151
|
+
print(f" API Docs: http://{host}:{port}/docs")
|
|
152
|
+
print("=" * 60 + "\n")
|
|
153
|
+
|
|
154
|
+
# Run the core app
|
|
155
|
+
run(host=host, port=port)
|