ivoryos 1.2.7__tar.gz → 1.3.0__tar.gz

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.

Files changed (112) hide show
  1. {ivoryos-1.2.7 → ivoryos-1.3.0}/PKG-INFO +1 -1
  2. ivoryos-1.3.0/ivoryos/__init__.py +13 -0
  3. ivoryos-1.3.0/ivoryos/app.py +131 -0
  4. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/api/api.py +2 -1
  5. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/control/control.py +8 -6
  6. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/control/templates/controllers.html +27 -0
  7. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/control/utils.py +2 -0
  8. ivoryos-1.3.0/ivoryos/routes/data/data.py +170 -0
  9. ivoryos-1.3.0/ivoryos/routes/data/templates/components/step_card.html +42 -0
  10. ivoryos-1.3.0/ivoryos/routes/data/templates/workflow_view.html +351 -0
  11. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/design.py +11 -4
  12. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/action_form.html +2 -2
  13. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
  14. ivoryos-1.2.7/ivoryos/__init__.py → ivoryos-1.3.0/ivoryos/server.py +18 -98
  15. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/socket_handler.js +39 -4
  16. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/sortable_design.js +28 -11
  17. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/db_models.py +84 -13
  18. ivoryos-1.3.0/ivoryos/utils/decorators.py +33 -0
  19. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/form.py +8 -4
  20. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/global_config.py +10 -0
  21. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/script_runner.py +123 -49
  22. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/task_runner.py +7 -2
  23. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/utils.py +25 -3
  24. ivoryos-1.3.0/ivoryos/version.py +1 -0
  25. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos.egg-info/PKG-INFO +1 -1
  26. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos.egg-info/SOURCES.txt +3 -0
  27. ivoryos-1.2.7/ivoryos/routes/data/data.py +0 -131
  28. ivoryos-1.2.7/ivoryos/routes/data/templates/components/step_card.html +0 -13
  29. ivoryos-1.2.7/ivoryos/routes/data/templates/workflow_view.html +0 -130
  30. ivoryos-1.2.7/ivoryos/version.py +0 -1
  31. {ivoryos-1.2.7 → ivoryos-1.3.0}/LICENSE +0 -0
  32. {ivoryos-1.2.7 → ivoryos-1.3.0}/README.md +0 -0
  33. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/config.py +0 -0
  34. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/optimizer/ax_optimizer.py +0 -0
  35. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/optimizer/base_optimizer.py +0 -0
  36. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/optimizer/baybe_optimizer.py +0 -0
  37. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/optimizer/registry.py +0 -0
  38. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/__init__.py +0 -0
  39. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/auth/__init__.py +0 -0
  40. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/auth/auth.py +0 -0
  41. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/auth/templates/login.html +0 -0
  42. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/auth/templates/signup.html +0 -0
  43. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/control/__init__.py +0 -0
  44. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/control/control_file.py +0 -0
  45. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/control/control_new_device.py +0 -0
  46. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  47. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/data/__init__.py +0 -0
  48. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  49. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/__init__.py +0 -0
  50. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/design_file.py +0 -0
  51. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/design_step.py +0 -0
  52. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  53. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  54. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  55. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  56. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  57. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/canvas_main.html +0 -0
  58. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  59. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/edit_action_form.html +0 -0
  60. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  61. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  62. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  63. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  64. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  65. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/modals.html +0 -0
  66. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
  67. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  68. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  69. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
  70. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/__init__.py +0 -0
  71. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/execute.py +0 -0
  72. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/execute_file.py +0 -0
  73. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  74. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  75. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  76. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  77. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  78. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
  79. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
  80. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  81. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  82. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/library/__init__.py +0 -0
  83. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/library/library.py +0 -0
  84. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/library/templates/library.html +0 -0
  85. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/main/__init__.py +0 -0
  86. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/main/main.py +0 -0
  87. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/main/templates/help.html +0 -0
  88. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/routes/main/templates/home.html +0 -0
  89. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/socket_handlers.py +0 -0
  90. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/favicon.ico +0 -0
  91. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  92. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  93. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/action_handlers.js +0 -0
  94. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/db_delete.js +0 -0
  95. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/overlay.js +0 -0
  96. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/script_metadata.js +0 -0
  97. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/sortable_card.js +0 -0
  98. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/js/ui_state.js +0 -0
  99. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/logo.webp +0 -0
  100. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/static/style.css +0 -0
  101. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/templates/base.html +0 -0
  102. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/__init__.py +0 -0
  103. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/bo_campaign.py +0 -0
  104. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/client_proxy.py +0 -0
  105. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/llm_agent.py +0 -0
  106. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/py_to_json.py +0 -0
  107. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos/utils/serilize.py +0 -0
  108. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos.egg-info/dependency_links.txt +0 -0
  109. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos.egg-info/requires.txt +0 -0
  110. {ivoryos-1.2.7 → ivoryos-1.3.0}/ivoryos.egg-info/top_level.txt +0 -0
  111. {ivoryos-1.2.7 → ivoryos-1.3.0}/pyproject.toml +0 -0
  112. {ivoryos-1.2.7 → ivoryos-1.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.2.7
3
+ Version: 1.3.0
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -0,0 +1,13 @@
1
+ from ivoryos.server import run
2
+ from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
3
+ from ivoryos.version import __version__ as ivoryos_version
4
+ from ivoryos.utils.decorators import block, BUILDING_BLOCKS
5
+
6
+
7
+ __all__ = [
8
+ "block",
9
+ "BUILDING_BLOCKS",
10
+ "OPTIMIZER_REGISTRY",
11
+ "run",
12
+ "ivoryos_version",
13
+ ]
@@ -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
@@ -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():
@@ -0,0 +1,170 @@
1
+ import os
2
+
3
+ from flask import Blueprint, redirect, url_for, request, render_template, current_app, jsonify, send_file
4
+ from flask_login import login_required
5
+
6
+ from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep, WorkflowPhase
7
+
8
+ data = Blueprint('data', __name__, template_folder='templates')
9
+
10
+
11
+
12
+ @data.route('/executions/records')
13
+ @login_required
14
+ def list_workflows():
15
+ """
16
+ .. :quickref: Workflow Execution Database; list all workflow execution records
17
+
18
+ list all workflow execution records
19
+
20
+ .. http:get:: /executions/records
21
+
22
+ """
23
+ query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
24
+ search_term = request.args.get("keyword", None)
25
+ if search_term:
26
+ query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
27
+ page = request.args.get('page', default=1, type=int)
28
+ per_page = 10
29
+
30
+ workflows = query.paginate(page=page, per_page=per_page, error_out=False)
31
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
32
+ workflows = query.all()
33
+ workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
34
+ return jsonify({
35
+ "workflow_data": workflow_data,
36
+ })
37
+ else:
38
+ return render_template('workflow_database.html', workflows=workflows)
39
+
40
+ @data.get("/executions/records/<int:workflow_id>")
41
+ def workflow_logs(workflow_id:int):
42
+ """
43
+ .. :quickref: Workflow Data Database; get workflow data, steps, and logs
44
+
45
+ get workflow data logs by workflow id
46
+
47
+ .. http:get:: /executions/<int:workflow_id>
48
+
49
+ :param workflow_id: workflow id
50
+ :type workflow_id: int
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
+ })
86
+
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
92
+ })
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)
133
+
134
+
135
+ @data.delete("/executions/records/<int:workflow_id>")
136
+ @login_required
137
+ def delete_workflow_record(workflow_id: int):
138
+ """
139
+ .. :quickref: Workflow Data Database; delete a workflow execution record
140
+
141
+ delete a workflow execution record by workflow id
142
+
143
+ .. http:delete:: /executions/records/<int:workflow_id>
144
+
145
+ :param workflow_id: workflow id
146
+ :type workflow_id: int
147
+ :status 200: return success message
148
+ """
149
+ run = WorkflowRun.query.get(workflow_id)
150
+ db.session.delete(run)
151
+ db.session.commit()
152
+ return jsonify(success=True)
153
+
154
+
155
+ @data.route('/files/execution-data/<string:filename>')
156
+ @login_required
157
+ def download_results(filename:str):
158
+ """
159
+ .. :quickref: Workflow data; download a workflow data file (.CSV)
160
+
161
+ .. http:get:: /files/execution-data/<string:filename>
162
+
163
+ :param filename: workflow data filename
164
+ :type filename: str
165
+
166
+ # :status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions`
167
+ """
168
+
169
+ filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
170
+ return send_file(os.path.abspath(filepath), as_attachment=True)
@@ -0,0 +1,42 @@
1
+ <div class="card mb-2 {{ 'border-danger text-danger bg-light' if phase.run_error else 'border-secondary' }}">
2
+ <div class="card-body p-2">
3
+ <small class="text-muted">
4
+ <i class="fas fa-play-circle me-1"></i> Start: {{ phase.start_time.strftime('%H:%M:%S') if phase.start_time else 'N/A' }}
5
+ <i class="fas fa-stop-circle ms-2 me-1"></i> End: {{ phase.end_time.strftime('%H:%M:%S') if phase.end_time else 'N/A' }}
6
+ </small>
7
+ {% if phase.parameters %}
8
+ <div class="mt-2">
9
+ <strong>Parameters: </strong>
10
+ {% for key, value in phase.parameters.items() %}
11
+ <span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
12
+ {% endfor %}
13
+ </div>
14
+ {% endif %}
15
+ {% if phase.steps %}
16
+ <div class="mt-2">
17
+ <strong>Steps:</strong>
18
+ <ul class="mb-0">
19
+ {% for step in phase.steps %}
20
+ <li class="{{ 'text-danger' if step.run_error else '' }}">
21
+ {{ step.method_name }}
22
+ <small class="text-muted">
23
+ ({{ step.start_time.strftime('%H:%M:%S') if step.start_time else 'N/A' }} –
24
+ {{ step.end_time.strftime('%H:%M:%S') if step.end_time else 'N/A' }})
25
+ </small>
26
+ </li>
27
+ {% endfor %}
28
+ </ul>
29
+ </div>
30
+ {% endif %}
31
+ {% if phase.outputs %}
32
+ <div class="mt-1">
33
+ <strong>Outputs:</strong>
34
+ <ul class="mb-0">
35
+ {% for key, value in phase.outputs.items() %}
36
+ <li>{{ key }}: {{ value }}</li>
37
+ {% endfor %}
38
+ </ul>
39
+ </div>
40
+ {% endif %}
41
+ </div>
42
+ </div>