Flowfile 0.3.0__py3-none-any.whl → 0.3.0.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.
- flowfile/__init__.py +13 -6
- flowfile/__main__.py +50 -15
- flowfile/api.py +383 -0
- flowfile/readme.md +130 -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.2.dist-info/METADATA +235 -0
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.2.dist-info}/RECORD +147 -15
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.2.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-0.3.0.dist-info/METADATA +0 -219
- flowfile_frame/__main__.py +0 -12
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.2.dist-info}/LICENSE +0 -0
- {flowfile-0.3.0.dist-info → flowfile-0.3.0.2.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,59 @@
|
|
|
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 == "ui":
|
|
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 ui")
|
|
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 ui --host 0.0.0.0 --port 8080 # Custom host/port")
|
|
53
|
+
print(" flowfile run ui --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)")
|
flowfile/api.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
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
|
+
"ui",
|
|
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
|
+
"ui",
|
|
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}/ui/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,
|
|
339
|
+
module_name: str = DEFAULT_MODULE_NAME) -> bool:
|
|
340
|
+
"""
|
|
341
|
+
Save the ETL graph, ensure the Flowfile server is running (starting it
|
|
342
|
+
if necessary), import the graph via API, and open it in a new browser
|
|
343
|
+
tab if running in unified mode.
|
|
344
|
+
|
|
345
|
+
Parameters:
|
|
346
|
+
flow_graph: The FlowGraph object to save and open.
|
|
347
|
+
storage_location: Optional path to save the .flowfile. If None,
|
|
348
|
+
a temporary file is used.
|
|
349
|
+
module_name: The module name to run if server needs to be started.
|
|
350
|
+
Use your Poetry package name if not using "flowfile".
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
True if the graph was successfully imported, False otherwise.
|
|
354
|
+
"""
|
|
355
|
+
temp_dir_obj: Optional[TemporaryDirectory] = None
|
|
356
|
+
try:
|
|
357
|
+
original_execution_settings = flow_graph.flow_settings.model_copy()
|
|
358
|
+
flow_graph.flow_settings.execution_location = "auto"
|
|
359
|
+
flow_graph.flow_settings.execution_mode = "Development"
|
|
360
|
+
flow_file_path, temp_dir_obj = _save_flow_to_location(flow_graph, storage_location)
|
|
361
|
+
if not flow_file_path:
|
|
362
|
+
return False
|
|
363
|
+
flow_graph.flow_settings = original_execution_settings
|
|
364
|
+
if not start_flowfile_server_process(module_name):
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
auth_token = get_auth_token()
|
|
368
|
+
if not auth_token:
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
flow_id = import_flow_to_editor(flow_file_path, auth_token)
|
|
372
|
+
|
|
373
|
+
if flow_id is not None:
|
|
374
|
+
_open_flow_in_browser(flow_id)
|
|
375
|
+
return True
|
|
376
|
+
else:
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
except Exception as e:
|
|
380
|
+
logger.error(f"An unexpected error occurred in open_graph_in_editor: {e}", exc_info=True)
|
|
381
|
+
return False
|
|
382
|
+
finally:
|
|
383
|
+
_cleanup_temporary_storage(temp_dir_obj)
|
flowfile/readme.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Flowfile Web UI Documentation
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Flowfile now supports a web-based user interface that can be launched directly from the pip-installed package. This enhancement allows users to quickly get started with the visual ETL tool without needing to install the desktop application, set up Docker, or manually configure the services.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **Integrated Web UI**: Launch the Flowfile interface directly in your browser
|
|
10
|
+
- **Unified Service**: Combined API that serves both the web UI and processes worker operations
|
|
11
|
+
- **Easy Installation**: Simple pip installation and startup process
|
|
12
|
+
- **Visual ETL**: Access to all the visual ETL capabilities through a web interface
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install Flowfile from PyPI using pip:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install Flowfile
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Starting the Web UI
|
|
23
|
+
|
|
24
|
+
You can start the Flowfile web UI using either the Python module or the command-line interface:
|
|
25
|
+
|
|
26
|
+
### Using the Command-Line Interface
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Start the web UI with default settings
|
|
30
|
+
flowfile run ui
|
|
31
|
+
|
|
32
|
+
# Customize host and port
|
|
33
|
+
flowfile run ui --host 0.0.0.0 --port 8080
|
|
34
|
+
|
|
35
|
+
# Start without automatically opening a browser window
|
|
36
|
+
flowfile run ui --no-browser
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Using Python
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import flowfile
|
|
43
|
+
|
|
44
|
+
# Start the web UI with default settings
|
|
45
|
+
flowfile.start_web_ui()
|
|
46
|
+
|
|
47
|
+
# Customize host, port, and browser launch
|
|
48
|
+
flowfile.start_web_ui(host="0.0.0.0", port=8080, open_browser=False)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Architecture Overview
|
|
52
|
+
|
|
53
|
+
The web UI functionality combines multiple components:
|
|
54
|
+
|
|
55
|
+
1. **Core Service**: The main ETL engine (flowfile_core) that processes data transformations
|
|
56
|
+
2. **Worker Service**: Handles computation and caching of data operations (flowfile_worker)
|
|
57
|
+
3. **Web UI**: A Vue.js frontend that provides the visual interface
|
|
58
|
+
|
|
59
|
+
When you start the web UI, all these services are launched together in a unified mode, making it simple to get started without configuration.
|
|
60
|
+
|
|
61
|
+
## Using the Web UI with FlowFrame API
|
|
62
|
+
|
|
63
|
+
You can create data pipelines programmatically with the FlowFrame API and then visualize them in the web UI:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import flowfile as ff
|
|
67
|
+
from flowfile import open_graph_in_editor
|
|
68
|
+
|
|
69
|
+
# Create a data pipeline
|
|
70
|
+
df = ff.from_dict({
|
|
71
|
+
"id": [1, 2, 3, 4, 5],
|
|
72
|
+
"category": ["A", "B", "A", "C", "B"],
|
|
73
|
+
"value": [100, 200, 150, 300, 250]
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
# Process the data
|
|
77
|
+
result = df.filter(ff.col("value") > 150).with_columns([
|
|
78
|
+
(ff.col("value") * 2).alias("double_value")
|
|
79
|
+
])
|
|
80
|
+
|
|
81
|
+
# Open the graph in the web UI (starts the server if it's not running)
|
|
82
|
+
open_graph_in_editor(result.flow_graph)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The `open_graph_in_editor` function automatically:
|
|
86
|
+
1. Saves the flow graph to a temporary file
|
|
87
|
+
2. Starts the Flowfile server if it's not already running
|
|
88
|
+
3. Imports the flow into the editor
|
|
89
|
+
4. Opens a browser tab with the imported flow
|
|
90
|
+
|
|
91
|
+
## Advanced Server Configuration
|
|
92
|
+
|
|
93
|
+
For advanced users who need to customize the server behavior:
|
|
94
|
+
|
|
95
|
+
### Environment Variables
|
|
96
|
+
|
|
97
|
+
- `FLOWFILE_HOST`: Host to bind the server to (default: "127.0.0.1")
|
|
98
|
+
- `FLOWFILE_PORT`: Port to bind the server to (default: 63578)
|
|
99
|
+
- `FLOWFILE_MODE`: Set to "electron" to enable browser auto-opening behavior
|
|
100
|
+
- `WORKER_URL`: URL for the worker service
|
|
101
|
+
- `SINGLE_FILE_MODE`: Set to "1" to run in unified mode with worker functionality
|
|
102
|
+
- `FLOWFILE_MODULE_NAME`: Module name to run (default: "flowfile")
|
|
103
|
+
|
|
104
|
+
### Running Individual Components
|
|
105
|
+
|
|
106
|
+
For development or specialized deployments, you can run the components separately:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Run only the core service
|
|
110
|
+
flowfile run core --host 0.0.0.0 --port 8080
|
|
111
|
+
|
|
112
|
+
# Run only the worker service
|
|
113
|
+
flowfile run worker --host 0.0.0.0 --port 8081
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Troubleshooting
|
|
117
|
+
|
|
118
|
+
- If the web UI doesn't open automatically, manually navigate to http://localhost:63578/ui
|
|
119
|
+
- If you encounter connection issues, check if the port is already in use
|
|
120
|
+
- Look for server logs in the terminal where you started the service for error messages
|
|
121
|
+
- For issues with the API, navigate to http://localhost:63578/docs to verify the API is running
|
|
122
|
+
|
|
123
|
+
## Next Steps
|
|
124
|
+
|
|
125
|
+
Once you're familiar with the web UI, you might want to explore:
|
|
126
|
+
|
|
127
|
+
1. The desktop application for a more native experience
|
|
128
|
+
2. Docker deployment for production environments
|
|
129
|
+
3. Advanced ETL operations using the FlowFrame API
|
|
130
|
+
4. Custom node development for specialized transformations
|