ivoryos 1.2.7__py3-none-any.whl → 1.3.0__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 ivoryos might be problematic. Click here for more details.

ivoryos/__init__.py CHANGED
@@ -1,248 +1,13 @@
1
- import os
2
- import sys
3
- import uuid
4
- from typing import Union
5
-
6
- from flask import Flask, redirect, url_for, g, Blueprint, session
7
- from flask_login import AnonymousUserMixin
8
-
9
- # sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
10
-
11
- from ivoryos.config import Config, get_config
12
- from ivoryos.routes.auth.auth import auth, login_manager
13
- from ivoryos.routes.control.control import control
14
- from ivoryos.routes.data.data import data
15
- from ivoryos.routes.library.library import library
16
- from ivoryos.routes.design.design import design
17
- from ivoryos.routes.execute.execute import execute
18
- from ivoryos.routes.api.api import api
19
- from ivoryos.socket_handlers import socketio
20
- from ivoryos.routes.main.main import main
21
- # from ivoryos.routes.monitor.monitor import monitor
22
- from ivoryos.utils import utils
23
- from ivoryos.utils.db_models import db, User
24
- from ivoryos.utils.global_config import GlobalConfig
1
+ from ivoryos.server import run
25
2
  from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
26
- from ivoryos.utils.script_runner import ScriptRunner
27
3
  from ivoryos.version import __version__ as ivoryos_version
28
- # from importlib.metadata import entry_points
29
-
30
- global_config = GlobalConfig()
31
- from sqlalchemy import event
32
- from sqlalchemy.engine import Engine
33
- import sqlite3
34
-
35
-
36
- @event.listens_for(Engine, "connect")
37
- def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
38
- if isinstance(dbapi_connection, sqlite3.Connection):
39
- cursor = dbapi_connection.cursor()
40
- cursor.execute("PRAGMA foreign_keys=ON")
41
- cursor.close()
42
-
43
-
44
- url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
45
- app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
46
- app.register_blueprint(main, url_prefix=url_prefix)
47
- app.register_blueprint(auth, url_prefix=f'{url_prefix}/{auth.name}')
48
- app.register_blueprint(library, url_prefix=f'{url_prefix}/{library.name}')
49
- app.register_blueprint(control, url_prefix=f'{url_prefix}/instruments')
50
- app.register_blueprint(design, url_prefix=f'{url_prefix}')
51
- app.register_blueprint(execute, url_prefix=f'{url_prefix}')
52
- app.register_blueprint(data, url_prefix=f'{url_prefix}')
53
- app.register_blueprint(api, url_prefix=f'{url_prefix}/{api.name}')
54
-
55
- @login_manager.user_loader
56
- def load_user(user_id):
57
- """
58
- This function is called by Flask-Login on every request to get the
59
- current user object from the user ID stored in the session.
60
- """
61
- # The correct implementation is to fetch the user from the database.
62
- return db.session.get(User, user_id)
63
-
64
-
65
- def create_app(config_class=None):
66
- """
67
- create app, init database
68
- """
69
- app.config.from_object(config_class or 'config.get_config()')
70
- os.makedirs(app.config["OUTPUT_FOLDER"], exist_ok=True)
71
- # Initialize extensions
72
- socketio.init_app(app, cors_allowed_origins="*", cookie=None)
73
- login_manager.init_app(app)
74
- login_manager.login_view = "auth.login"
75
- db.init_app(app)
76
-
77
- # Create database tables
78
- with app.app_context():
79
- db.create_all()
80
-
81
- # Additional setup
82
- utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
83
-
84
- # logger_list = app.config["LOGGERS"]
85
- logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
86
- logger = utils.start_logger(socketio, 'gui_logger', logger_path)
87
-
88
- @app.before_request
89
- def before_request():
90
- """
91
- Called before
92
-
93
- """
94
- g.logger = logger
95
- g.socketio = socketio
96
- session.permanent = False
97
- # DEMO_MODE: Simulate logged-in user per session
98
- if app.config.get("DEMO_MODE", False):
99
- if "demo_user_id" not in session:
100
- session["demo_user_id"] = f"demo_{str(uuid.uuid4())[:8]}"
101
-
102
- class SessionDemoUser(AnonymousUserMixin):
103
- @property
104
- def is_authenticated(self):
105
- return True
106
-
107
- def get_id(self):
108
- return session.get("demo_user_id")
109
-
110
- login_manager.anonymous_user = SessionDemoUser
111
-
112
-
113
-
114
- @app.route('/')
115
- def redirect_to_prefix():
116
- return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
117
-
118
- @app.template_filter('format_name')
119
- def format_name(name):
120
- name = name.split(".")[-1]
121
- text = ' '.join(word for word in name.split('_'))
122
- return text.capitalize()
123
-
124
- # app.config.setdefault("DEMO_MODE", False)
125
- return app
126
-
127
-
128
- def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, model=None,
129
- config: Config = None,
130
- logger: Union[str, list] = None,
131
- logger_output_name: str = None,
132
- enable_design: bool = True,
133
- blueprint_plugins: Union[list, Blueprint] = [],
134
- exclude_names: list = [],
135
- ):
136
- """
137
- Start ivoryOS app server.
138
-
139
- :param module: module name, __name__ for current module
140
- :param host: host address, defaults to 0.0.0.0
141
- :param port: port, defaults to None, and will use 8000
142
- :param debug: debug mode, defaults to None (True)
143
- :param llm_server: llm server, defaults to None.
144
- :param model: llm model, defaults to None. If None, app will run without text-to-code feature
145
- :param config: config class, defaults to None
146
- :param logger: logger name of list of logger names, defaults to None
147
- :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
148
- :param enable_design: enable design canvas, database and workflow execution
149
- :param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
150
- :param exclude_names: list[str] module names to exclude from parsing
151
- """
152
- app = create_app(config_class=config or get_config()) # Create app instance using factory function
153
-
154
- # plugins = load_installed_plugins(app, socketio)
155
- plugins = []
156
- if blueprint_plugins:
157
- config_plugins = load_plugins(blueprint_plugins, app, socketio)
158
- plugins.extend(config_plugins)
159
-
160
- def inject_nav_config():
161
- """Make NAV_CONFIG available globally to all templates."""
162
- return dict(
163
- enable_design=enable_design,
164
- plugins=plugins,
165
- )
166
-
167
- app.context_processor(inject_nav_config)
168
- port = port or int(os.environ.get("PORT", 8000))
169
- debug = debug if debug is not None else app.config.get('DEBUG', True)
170
-
171
- app.config["LOGGERS"] = logger
172
- app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
173
- logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
174
- dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
175
- global_config.optimizers = OPTIMIZER_REGISTRY
176
- if module:
177
- app.config["MODULE"] = module
178
- app.config["OFF_LINE"] = False
179
- global_config.deck = sys.modules[module]
180
- global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
181
- output_path=dummy_deck_path,
182
- save=True,
183
- exclude_names=exclude_names
184
- )
185
- else:
186
- app.config["OFF_LINE"] = True
187
- if model:
188
- app.config["ENABLE_LLM"] = True
189
- app.config["LLM_MODEL"] = model
190
- app.config["LLM_SERVER"] = llm_server
191
- utils.install_and_import('openai')
192
- from ivoryos.utils.llm_agent import LlmAgent
193
- global_config.agent = LlmAgent(host=llm_server, model=model,
194
- output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
195
- else:
196
- app.config["ENABLE_LLM"] = False
197
- if logger and type(logger) is str:
198
- utils.start_logger(socketio, log_filename=logger_path, logger_name=logger)
199
- elif type(logger) is list:
200
- for log in logger:
201
- utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
202
-
203
- # in case Python 3.12 or higher doesn't log URL
204
- if sys.version_info >= (3, 12):
205
- ip = utils.get_local_ip()
206
- print(f"Server running at http://localhost:{port}")
207
- if not ip == "127.0.0.1":
208
- print(f"Server running at http://{ip}:{port}")
209
- socketio.run(app, host=host, port=port, debug=debug, use_reloader=False, allow_unsafe_werkzeug=True)
210
- # return app
211
-
212
-
213
- # def load_installed_plugins(app, socketio):
214
- # """
215
- # Dynamically load installed plugins and attach Flask-SocketIO.
216
- # """
217
- # plugin_names = []
218
- # for entry_point in entry_points().get("ivoryos.plugins", []):
219
- # plugin = entry_point.load()
220
- #
221
- # # If the plugin has an `init_socketio()` function, pass socketio
222
- # if hasattr(plugin, 'init_socketio'):
223
- # plugin.init_socketio(socketio)
224
- #
225
- # plugin_names.append(entry_point.name)
226
- # app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
227
- #
228
- # return plugin_names
4
+ from ivoryos.utils.decorators import block, BUILDING_BLOCKS
229
5
 
230
6
 
231
- def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
232
- """
233
- Dynamically load installed plugins and attach Flask-SocketIO.
234
- :param blueprints: Union[list, Blueprint] list of Blueprint objects or a single Blueprint object
235
- :param app: Flask application instance
236
- :param socketio: Flask-SocketIO instance
237
- :return: list of plugin names
238
- """
239
- plugin_names = []
240
- if not isinstance(blueprints, list):
241
- blueprints = [blueprints]
242
- for blueprint in blueprints:
243
- # If the plugin has an `init_socketio()` function, pass socketio
244
- if hasattr(blueprint, 'init_socketio'):
245
- blueprint.init_socketio(socketio)
246
- plugin_names.append(blueprint.name)
247
- app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
248
- return plugin_names
7
+ __all__ = [
8
+ "block",
9
+ "BUILDING_BLOCKS",
10
+ "OPTIMIZER_REGISTRY",
11
+ "run",
12
+ "ivoryos_version",
13
+ ]
ivoryos/app.py ADDED
@@ -0,0 +1,131 @@
1
+ import os
2
+ import uuid
3
+
4
+ from flask import Flask, session, g, redirect, url_for
5
+ from flask_login import AnonymousUserMixin
6
+
7
+ from ivoryos.utils import utils
8
+ from ivoryos.utils.db_models import db
9
+ from ivoryos.config import Config, get_config
10
+ from ivoryos.routes.auth.auth import auth, login_manager
11
+ from ivoryos.routes.control.control import control
12
+ from ivoryos.routes.data.data import data
13
+ from ivoryos.routes.library.library import library
14
+ from ivoryos.routes.design.design import design
15
+ from ivoryos.routes.execute.execute import execute
16
+ from ivoryos.routes.api.api import api
17
+ from ivoryos.socket_handlers import socketio
18
+ from ivoryos.routes.main.main import main
19
+ from ivoryos.version import __version__ as ivoryos_version
20
+ from sqlalchemy import inspect, text
21
+ from flask import current_app
22
+
23
+
24
+ def reset_old_schema(engine, db_dir):
25
+ inspector = inspect(engine)
26
+ tables = inspector.get_table_names()
27
+
28
+
29
+ # Check if old tables exist (no workflow_phases table)
30
+ has_workflow_phase = 'workflow_phases' in tables
31
+ old_workflow_run = 'old_workflow_run' in tables
32
+ old_workflow_step = 'workflow_steps' in tables
33
+
34
+ if not has_workflow_phase:
35
+ print("⚠️ Old workflow database detected! All previous workflows have been reset to support the new schema.")
36
+ # Backup old DB
37
+ db_path = os.path.join(db_dir, "ivoryos.db")
38
+ if os.path.exists(db_path):
39
+ # os.makedirs(backup_dir, exist_ok=True)
40
+ from datetime import datetime
41
+ import shutil
42
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
43
+ backup_path = os.path.join(db_dir, f"ivoryos_backup_{ts}.db")
44
+ shutil.copy(db_path, backup_path)
45
+ print(f"Backup created at {backup_path}")
46
+ with engine.begin() as conn:
47
+ # Drop old tables
48
+ if old_workflow_step:
49
+ conn.execute(text("DROP TABLE IF EXISTS workflow_steps"))
50
+ if old_workflow_run:
51
+ conn.execute(text("DROP TABLE IF EXISTS workflow_runs"))
52
+
53
+ # Recreate new schema
54
+ db.create_all() # creates workflow_runs, workflow_phases, workflow_steps
55
+
56
+
57
+ def create_app(config_class=None):
58
+ """
59
+ create app, init database
60
+ """
61
+
62
+ url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
63
+ app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
64
+ app.register_blueprint(main, url_prefix=url_prefix)
65
+ app.register_blueprint(auth, url_prefix=f'{url_prefix}/{auth.name}')
66
+ app.register_blueprint(library, url_prefix=f'{url_prefix}/{library.name}')
67
+ app.register_blueprint(control, url_prefix=f'{url_prefix}/instruments')
68
+ app.register_blueprint(design, url_prefix=f'{url_prefix}')
69
+ app.register_blueprint(execute, url_prefix=f'{url_prefix}')
70
+ app.register_blueprint(data, url_prefix=f'{url_prefix}')
71
+ app.register_blueprint(api, url_prefix=f'{url_prefix}/{api.name}')
72
+
73
+
74
+ app.config.from_object(config_class or 'config.get_config()')
75
+ os.makedirs(app.config["OUTPUT_FOLDER"], exist_ok=True)
76
+ # Initialize extensions
77
+ socketio.init_app(app, cors_allowed_origins="*", cookie=None)
78
+ login_manager.init_app(app)
79
+ login_manager.login_view = "auth.login"
80
+ db.init_app(app)
81
+
82
+ # Create database tables
83
+ with app.app_context():
84
+ # db.create_all()
85
+ reset_old_schema(db.engine, app.config['OUTPUT_FOLDER'])
86
+
87
+ # Additional setup
88
+ utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
89
+
90
+ # logger_list = app.config["LOGGERS"]
91
+ logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
92
+ logger = utils.start_logger(socketio, 'gui_logger', logger_path)
93
+
94
+ @app.before_request
95
+ def before_request():
96
+ """
97
+ Called before
98
+
99
+ """
100
+ g.logger = logger
101
+ g.socketio = socketio
102
+ session.permanent = False
103
+ # DEMO_MODE: Simulate logged-in user per session
104
+ if app.config.get("DEMO_MODE", False):
105
+ if "demo_user_id" not in session:
106
+ session["demo_user_id"] = f"demo_{str(uuid.uuid4())[:8]}"
107
+
108
+ class SessionDemoUser(AnonymousUserMixin):
109
+ @property
110
+ def is_authenticated(self):
111
+ return True
112
+
113
+ def get_id(self):
114
+ return session.get("demo_user_id")
115
+
116
+ login_manager.anonymous_user = SessionDemoUser
117
+
118
+
119
+
120
+ @app.route('/')
121
+ def redirect_to_prefix():
122
+ return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
123
+
124
+ @app.template_filter('format_name')
125
+ def format_name(name):
126
+ name = name.split(".")[-1]
127
+ text = ' '.join(word for word in name.split('_'))
128
+ return text.capitalize()
129
+
130
+ # app.config.setdefault("DEMO_MODE", False)
131
+ return app
ivoryos/routes/api/api.py CHANGED
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import os
2
3
  from flask import Blueprint, jsonify, request, current_app
3
4
 
@@ -46,7 +47,7 @@ def backend_control(instrument: str=None):
46
47
  current_app=current_app._get_current_object())
47
48
  return jsonify(output), 200
48
49
 
49
- snapshot = global_config.deck_snapshot.copy()
50
+ snapshot = copy.deepcopy(global_config.deck_snapshot)
50
51
  # Iterate through each instrument in the snapshot
51
52
  for instrument_key, instrument_data in snapshot.items():
52
53
  # Iterate through each function associated with the current instrument
@@ -5,7 +5,7 @@ from ivoryos.routes.control.control_file import control_file
5
5
  from ivoryos.routes.control.control_new_device import control_temp
6
6
  from ivoryos.routes.control.utils import post_session_by_instrument, get_session_by_instrument, find_instrument_by_name
7
7
  from ivoryos.utils.global_config import GlobalConfig
8
- from ivoryos.utils.form import create_form_from_module
8
+ from ivoryos.utils.form import create_form_from_module, create_form_from_pseudo
9
9
  from ivoryos.utils.task_runner import TaskRunner
10
10
 
11
11
  global_config = GlobalConfig()
@@ -48,7 +48,10 @@ def deck_controllers(instrument: str = None):
48
48
  forms = None
49
49
  if instrument:
50
50
  inst_object = find_instrument_by_name(instrument)
51
- forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
51
+ if instrument.startswith("blocks"):
52
+ forms = create_form_from_pseudo(pseudo=inst_object, autofill=False, design=False)
53
+ else:
54
+ forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
52
55
  order = get_session_by_instrument('card_order', instrument)
53
56
  hidden_functions = get_session_by_instrument('hidden_functions', instrument)
54
57
  functions = list(forms.keys())
@@ -99,12 +102,11 @@ def deck_controllers(instrument: str = None):
99
102
  function_data["signature"] = str(function_data["signature"])
100
103
  return jsonify(snapshot)
101
104
 
102
- deck_variables = global_config.deck_snapshot.keys()
103
- temp_variables = global_config.defined_variables.keys()
104
105
  return render_template(
105
106
  "controllers.html",
106
- defined_variables=deck_variables,
107
- temp_variables=temp_variables,
107
+ defined_variables=global_config.deck_snapshot.keys(),
108
+ block_variables=global_config.building_blocks.keys(),
109
+ temp_variables=global_config.defined_variables.keys(),
108
110
  instrument=instrument,
109
111
  forms=forms,
110
112
  session=session
@@ -49,6 +49,24 @@
49
49
  </div>
50
50
  </div>
51
51
  {% endif %}
52
+
53
+ {% if block_variables %}
54
+ <div class="mb-4">
55
+ <h6 class="fw-bold text-secondary mb-2" style="letter-spacing: 1px;">Methods</h6>
56
+ <div class="list-group">
57
+ {% for inst in block_variables %}
58
+ <a class="list-group-item list-group-item-action d-flex align-items-center {% if instrument == inst %}active bg-warning text-dark border-0{% else %}bg-light{% endif %}"
59
+ href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
60
+ style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
61
+ <span class="flex-grow-1">{{ inst | format_name }}</span>
62
+ {% if instrument == inst %}
63
+ <span class="ms-auto">&gt;</span>
64
+ {% endif %}
65
+ </a>
66
+ {% endfor %}
67
+ </div>
68
+ </div>
69
+ {% endif %}
52
70
  <!-- Action Buttons -->
53
71
  <div class="mb-4">
54
72
  <a href="{{ url_for('control.file.download_proxy', filetype='proxy') }}" class="btn btn-outline-primary w-100 mb-2">
@@ -89,6 +107,15 @@
89
107
  {{ field(class="btn btn-dark") }}
90
108
  {% elif field.type == "BooleanField" %}
91
109
  {{ field(class="form-check-input") }}
110
+ {% elif field.type == "FlexibleEnumField" %}
111
+ <input type="text" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.data }}"
112
+ list="{{ field.id }}_options" placeholder="{{ field.render_kw.placeholder if field.render_kw and field.render_kw.placeholder }}"
113
+ class="form-control">
114
+ <datalist id="{{ field.id }}_options">
115
+ {% for key in field.choices %}
116
+ <option value="{{ key }}">{{ key }}</option>
117
+ {% endfor %}
118
+ </datalist>
92
119
  {% else %}
93
120
  {{ field(class="form-control") }}
94
121
  {% endif %}
@@ -13,6 +13,8 @@ def find_instrument_by_name(name: str):
13
13
  if name.startswith("deck"):
14
14
  name = name.replace("deck.", "")
15
15
  return getattr(global_config.deck, name)
16
+ elif name.startswith("blocks"):
17
+ return global_config.building_blocks[name]
16
18
  elif name in global_config.defined_variables:
17
19
  return global_config.defined_variables[name]
18
20
  elif name in globals():
@@ -3,7 +3,7 @@ import os
3
3
  from flask import Blueprint, redirect, url_for, request, render_template, current_app, jsonify, send_file
4
4
  from flask_login import login_required
5
5
 
6
- from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep
6
+ from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep, WorkflowPhase
7
7
 
8
8
  data = Blueprint('data', __name__, template_folder='templates')
9
9
 
@@ -37,7 +37,6 @@ def list_workflows():
37
37
  else:
38
38
  return render_template('workflow_database.html', workflows=workflows)
39
39
 
40
-
41
40
  @data.get("/executions/records/<int:workflow_id>")
42
41
  def workflow_logs(workflow_id:int):
43
42
  """
@@ -50,47 +49,87 @@ def workflow_logs(workflow_id:int):
50
49
  :param workflow_id: workflow id
51
50
  :type workflow_id: int
52
51
  """
52
+ workflow = db.session.get(WorkflowRun, workflow_id)
53
+ if not workflow:
54
+ return jsonify({"error": "Workflow not found"}), 404
55
+
56
+ # Query all phases for this run, ordered by start_time
57
+ phases = WorkflowPhase.query.filter_by(run_id=workflow_id).order_by(WorkflowPhase.start_time).all()
58
+
59
+ # Prepare grouped data for template (full objects)
60
+ grouped = {
61
+ "prep": [],
62
+ "script": {},
63
+ "cleanup": [],
64
+ }
65
+
66
+ # Prepare grouped data for JSON (dicts)
67
+ grouped_json = {
68
+ "prep": [],
69
+ "script": {},
70
+ "cleanup": [],
71
+ }
72
+
73
+ for phase in phases:
74
+ phase_dict = phase.as_dict()
75
+
76
+ # Steps sorted by step_index
77
+ steps = sorted(phase.steps, key=lambda s: s.step_index)
78
+ phase_steps_dicts = [s.as_dict() for s in steps]
79
+
80
+ if phase.name == "prep":
81
+ grouped["prep"].append(phase)
82
+ grouped_json["prep"].append({
83
+ **phase_dict,
84
+ "steps": phase_steps_dicts
85
+ })
53
86
 
54
- if request.method == 'GET':
55
- workflow = db.session.get(WorkflowRun, workflow_id)
56
- steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
57
-
58
- # Use full objects for template rendering
59
- grouped = {
60
- "prep": [],
61
- "script": {},
62
- "cleanup": [],
63
- }
64
-
65
- # Use dicts for JSON response
66
- grouped_json = {
67
- "prep": [],
68
- "script": {},
69
- "cleanup": [],
70
- }
71
-
72
- for step in steps:
73
- step_dict = step.as_dict()
74
-
75
- if step.phase == "prep":
76
- grouped["prep"].append(step)
77
- grouped_json["prep"].append(step_dict)
78
-
79
- elif step.phase == "script":
80
- grouped["script"].setdefault(step.repeat_index, []).append(step)
81
- grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
82
-
83
- elif step.phase == "cleanup" or step.method_name == "stop":
84
- grouped["cleanup"].append(step)
85
- grouped_json["cleanup"].append(step_dict)
86
-
87
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
88
- return jsonify({
89
- "workflow_info": workflow.as_dict(),
90
- "steps": grouped_json,
87
+ elif phase.name == "main":
88
+ grouped["script"].setdefault(phase.repeat_index, []).append(phase)
89
+ grouped_json["script"].setdefault(phase.repeat_index, []).append({
90
+ **phase_dict,
91
+ "steps": phase_steps_dicts
91
92
  })
92
- else:
93
- return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
93
+
94
+ elif phase.name == "cleanup":
95
+ grouped["cleanup"].append(phase)
96
+ grouped_json["cleanup"].append({
97
+ **phase_dict,
98
+ "steps": phase_steps_dicts
99
+ })
100
+
101
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
102
+ return jsonify({
103
+ "workflow_info": workflow.as_dict(),
104
+ "phases": grouped_json,
105
+ })
106
+ else:
107
+ return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
108
+
109
+
110
+ @data.get("/executions/data/<int:workflow_id>")
111
+ def workflow_phase_data(workflow_id: int):
112
+ workflow = db.session.get(WorkflowRun, workflow_id)
113
+ if not workflow:
114
+ return jsonify({})
115
+
116
+ phase_data = {}
117
+ # Only plot 'main' phases
118
+ main_phases = WorkflowPhase.query.filter_by(run_id=workflow_id, name='main').order_by(
119
+ WorkflowPhase.repeat_index).all()
120
+
121
+ for phase in main_phases:
122
+ outputs = phase.outputs or {}
123
+ phase_index = phase.repeat_index
124
+ # Convert each key to list of dicts for x (phase_index) and y (value)
125
+ phase_data[phase_index] = {}
126
+ for k, v in outputs.items():
127
+ if isinstance(v, (int, float)):
128
+ phase_data[phase_index][k] = [{"x": phase_index, "y": v}]
129
+ elif isinstance(v, list) and all(isinstance(i, (int, float)) for i in v):
130
+ phase_data[phase_index][k] = v.map(lambda val, idx=0: {"x": phase_index, "y": val})
131
+
132
+ return jsonify(phase_data)
94
133
 
95
134
 
96
135
  @data.delete("/executions/records/<int:workflow_id>")