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.

Files changed (147) hide show
  1. flowfile/__init__.py +13 -6
  2. flowfile/__main__.py +51 -15
  3. flowfile/api.py +379 -0
  4. flowfile/web/__init__.py +155 -0
  5. flowfile/web/static/assets/AirbyteReader-1ac35765.css +314 -0
  6. flowfile/web/static/assets/AirbyteReader-cb0c1d4a.js +921 -0
  7. flowfile/web/static/assets/CrossJoin-41efa4cb.css +100 -0
  8. flowfile/web/static/assets/CrossJoin-a514fa59.js +153 -0
  9. flowfile/web/static/assets/DatabaseConnectionSettings-0c04b2e5.css +77 -0
  10. flowfile/web/static/assets/DatabaseConnectionSettings-f2cecf33.js +151 -0
  11. flowfile/web/static/assets/DatabaseManager-30fa27e5.css +64 -0
  12. flowfile/web/static/assets/DatabaseManager-83ee3c98.js +484 -0
  13. flowfile/web/static/assets/DatabaseReader-dc0c6881.js +426 -0
  14. flowfile/web/static/assets/DatabaseReader-f50c6558.css +158 -0
  15. flowfile/web/static/assets/DatabaseWriter-2f570e53.css +96 -0
  16. flowfile/web/static/assets/DatabaseWriter-5afe9f8d.js +312 -0
  17. flowfile/web/static/assets/ExploreData-5bdae813.css +45 -0
  18. flowfile/web/static/assets/ExploreData-c7ee19cf.js +118306 -0
  19. flowfile/web/static/assets/ExternalSource-17b23a01.js +225 -0
  20. flowfile/web/static/assets/ExternalSource-e37b6275.css +94 -0
  21. flowfile/web/static/assets/Filter-90856b4f.js +238 -0
  22. flowfile/web/static/assets/Filter-a9d08ba1.css +20 -0
  23. flowfile/web/static/assets/Formula-38b71e9e.js +197 -0
  24. flowfile/web/static/assets/Formula-d60a74f4.css +17 -0
  25. flowfile/web/static/assets/FuzzyMatch-6857de82.css +254 -0
  26. flowfile/web/static/assets/FuzzyMatch-d0f1fe81.js +422 -0
  27. flowfile/web/static/assets/GoogleSheet-854294a4.js +2616 -0
  28. flowfile/web/static/assets/GoogleSheet-92084da7.css +233 -0
  29. flowfile/web/static/assets/GraphSolver-0c86bbc6.js +382 -0
  30. flowfile/web/static/assets/GraphSolver-17fd26db.css +68 -0
  31. flowfile/web/static/assets/GroupBy-ab1ea74b.css +51 -0
  32. flowfile/web/static/assets/GroupBy-f2772e9f.js +413 -0
  33. flowfile/web/static/assets/Join-41c0f331.css +109 -0
  34. flowfile/web/static/assets/Join-bc3e1cf7.js +247 -0
  35. flowfile/web/static/assets/ManualInput-03aa0245.js +391 -0
  36. flowfile/web/static/assets/ManualInput-ac7b9972.css +84 -0
  37. flowfile/web/static/assets/Output-48f81019.css +2642 -0
  38. flowfile/web/static/assets/Output-5b35eee8.js +536 -0
  39. flowfile/web/static/assets/Pivot-7164087c.js +408 -0
  40. flowfile/web/static/assets/Pivot-f415e85f.css +35 -0
  41. flowfile/web/static/assets/PolarsCode-3abf6507.js +2863 -0
  42. flowfile/web/static/assets/PolarsCode-650322d1.css +35 -0
  43. flowfile/web/static/assets/PopOver-b37ff9be.js +577 -0
  44. flowfile/web/static/assets/PopOver-bccfde04.css +32 -0
  45. flowfile/web/static/assets/Read-65966a3e.js +701 -0
  46. flowfile/web/static/assets/Read-80dc1675.css +197 -0
  47. flowfile/web/static/assets/RecordCount-c66c6d6d.js +121 -0
  48. flowfile/web/static/assets/RecordId-826dc095.js +339 -0
  49. flowfile/web/static/assets/Sample-4ed555c8.js +184 -0
  50. flowfile/web/static/assets/SecretManager-eac1e97d.js +382 -0
  51. flowfile/web/static/assets/Select-085f05cc.js +231 -0
  52. flowfile/web/static/assets/SettingsSection-1f5e79c1.js +87 -0
  53. flowfile/web/static/assets/SettingsSection-9c836ecc.css +47 -0
  54. flowfile/web/static/assets/Sort-3e6cb414.js +309 -0
  55. flowfile/web/static/assets/Sort-7ccfa0fe.css +51 -0
  56. flowfile/web/static/assets/TextToRows-606349bc.js +307 -0
  57. flowfile/web/static/assets/TextToRows-c92d1ec2.css +48 -0
  58. flowfile/web/static/assets/UnavailableFields-5edd5322.css +49 -0
  59. flowfile/web/static/assets/UnavailableFields-b41976ed.js +36 -0
  60. flowfile/web/static/assets/Union-8d9ac7f9.css +30 -0
  61. flowfile/web/static/assets/Union-fca91665.js +145 -0
  62. flowfile/web/static/assets/Unique-a59f830e.js +273 -0
  63. flowfile/web/static/assets/Unique-b5615727.css +51 -0
  64. flowfile/web/static/assets/Unpivot-246e9bbd.css +77 -0
  65. flowfile/web/static/assets/Unpivot-c3815565.js +441 -0
  66. flowfile/web/static/assets/airbyte-292aa232.png +0 -0
  67. flowfile/web/static/assets/api-22b338bd.js +60 -0
  68. flowfile/web/static/assets/cross_join-d30c0290.png +0 -0
  69. flowfile/web/static/assets/database_reader-ce1e55f3.svg +24 -0
  70. flowfile/web/static/assets/database_writer-b4ad0753.svg +23 -0
  71. flowfile/web/static/assets/designer-2394122a.css +10697 -0
  72. flowfile/web/static/assets/designer-e5bbe26f.js +69712 -0
  73. flowfile/web/static/assets/documentation-08045cf2.js +33 -0
  74. flowfile/web/static/assets/documentation-12216a74.css +50 -0
  75. flowfile/web/static/assets/dropDown-35135ba8.css +143 -0
  76. flowfile/web/static/assets/dropDown-5e7e9a5a.js +319 -0
  77. flowfile/web/static/assets/dropDownGeneric-50a91b99.js +72 -0
  78. flowfile/web/static/assets/dropDownGeneric-895680d6.css +10 -0
  79. flowfile/web/static/assets/element-icons-9c88a535.woff +0 -0
  80. flowfile/web/static/assets/element-icons-de5eb258.ttf +0 -0
  81. flowfile/web/static/assets/explore_data-8a0a2861.png +0 -0
  82. flowfile/web/static/assets/fa-brands-400-808443ae.ttf +0 -0
  83. flowfile/web/static/assets/fa-brands-400-d7236a19.woff2 +0 -0
  84. flowfile/web/static/assets/fa-regular-400-54cf6086.ttf +0 -0
  85. flowfile/web/static/assets/fa-regular-400-e3456d12.woff2 +0 -0
  86. flowfile/web/static/assets/fa-solid-900-aa759986.woff2 +0 -0
  87. flowfile/web/static/assets/fa-solid-900-d2f05935.ttf +0 -0
  88. flowfile/web/static/assets/fa-v4compatibility-0ce9033c.woff2 +0 -0
  89. flowfile/web/static/assets/fa-v4compatibility-30f6abf6.ttf +0 -0
  90. flowfile/web/static/assets/filter-d7708bda.png +0 -0
  91. flowfile/web/static/assets/formula-eeeb1611.png +0 -0
  92. flowfile/web/static/assets/fullEditor-178376bb.css +256 -0
  93. flowfile/web/static/assets/fullEditor-705c6ccb.js +630 -0
  94. flowfile/web/static/assets/fuzzy_match-40c161b2.png +0 -0
  95. flowfile/web/static/assets/genericNodeSettings-65587f20.js +137 -0
  96. flowfile/web/static/assets/genericNodeSettings-924759c7.css +46 -0
  97. flowfile/web/static/assets/graph_solver-8b7888b8.png +0 -0
  98. flowfile/web/static/assets/group_by-80561fc3.png +0 -0
  99. flowfile/web/static/assets/index-552863fd.js +58652 -0
  100. flowfile/web/static/assets/index-681a3ed0.css +8843 -0
  101. flowfile/web/static/assets/input_data-ab2eb678.png +0 -0
  102. flowfile/web/static/assets/join-349043ae.png +0 -0
  103. flowfile/web/static/assets/manual_input-ae98f31d.png +0 -0
  104. flowfile/web/static/assets/nodeTitle-cf9bae3c.js +227 -0
  105. flowfile/web/static/assets/nodeTitle-f4b12bcb.css +134 -0
  106. flowfile/web/static/assets/old_join-5d0eb604.png +0 -0
  107. flowfile/web/static/assets/output-06ec0371.png +0 -0
  108. flowfile/web/static/assets/pivot-9660df51.png +0 -0
  109. flowfile/web/static/assets/polars_code-05ce5dc6.png +0 -0
  110. flowfile/web/static/assets/record_count-dab44eb5.png +0 -0
  111. flowfile/web/static/assets/record_id-0b15856b.png +0 -0
  112. flowfile/web/static/assets/sample-693a88b5.png +0 -0
  113. flowfile/web/static/assets/secretApi-3ad510e1.js +46 -0
  114. flowfile/web/static/assets/select-b0d0437a.png +0 -0
  115. flowfile/web/static/assets/selectDynamic-b062bc9b.css +107 -0
  116. flowfile/web/static/assets/selectDynamic-bd644891.js +302 -0
  117. flowfile/web/static/assets/sort-2aa579f0.png +0 -0
  118. flowfile/web/static/assets/summarize-2a099231.png +0 -0
  119. flowfile/web/static/assets/text_to_rows-859b29ea.png +0 -0
  120. flowfile/web/static/assets/union-2d8609f4.png +0 -0
  121. flowfile/web/static/assets/unique-1958b98a.png +0 -0
  122. flowfile/web/static/assets/unpivot-d3cb4b5b.png +0 -0
  123. flowfile/web/static/assets/view-7a0f0be1.png +0 -0
  124. flowfile/web/static/assets/vue-codemirror.esm-dd17b478.js +22281 -0
  125. flowfile/web/static/assets/vue-content-loader.es-6b36f05e.js +210 -0
  126. flowfile/web/static/flowfile.svg +47 -0
  127. flowfile/web/static/icons/flowfile.png +0 -0
  128. flowfile/web/static/images/airbyte.png +0 -0
  129. flowfile/web/static/images/flowfile.svg +47 -0
  130. flowfile/web/static/images/google.svg +1 -0
  131. flowfile/web/static/images/sheets.png +0 -0
  132. flowfile/web/static/index.html +22 -0
  133. flowfile/web/static/vite.svg +1 -0
  134. flowfile/web/static/vue.svg +1 -0
  135. {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/METADATA +1 -1
  136. {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/RECORD +146 -15
  137. {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/entry_points.txt +1 -1
  138. flowfile_core/configs/settings.py +7 -32
  139. flowfile_core/flowfile/FlowfileFlow.py +4 -2
  140. flowfile_core/flowfile/analytics/analytics_processor.py +1 -1
  141. flowfile_core/main.py +4 -1
  142. flowfile_core/schemas/input_schema.py +1 -8
  143. flowfile_frame/__init__.py +0 -1
  144. flowfile_frame/utils.py +0 -139
  145. flowfile_frame/__main__.py +0 -12
  146. {flowfile-0.3.0.dist-info → flowfile-0.3.0.1.dist-info}/LICENSE +0 -0
  147. {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.2.1"
10
+ __version__ = "0.3.1"
11
11
 
12
- # Import the key components from flowfile_frame
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, open_graph_in_editor
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
- print(f"FlowFile v{flowfile.__version__}")
13
- print("A framework combining visual ETL with a Polars-like API")
14
- print("\nUsage examples:")
15
- print(" import flowfile as ff")
16
- print(" df = ff.read_csv('data.csv')")
17
- print(" result = df.filter(ff.col('value') > 10)")
18
- print(" result.write_csv('output.csv')")
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 __name__ == "__main__":
24
- main()
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)
@@ -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)