Commit
·
68e6de8
1
Parent(s):
007efe1
add websocket warning
Browse files- demo_code_plugin/plugin.py +0 -3
- demo_code_plugin/templates/example.html +8 -0
- ivoryos/__init__.py +0 -210
- ivoryos/config.py +0 -66
- ivoryos/routes/__init__.py +0 -0
- ivoryos/routes/auth/__init__.py +0 -0
- ivoryos/routes/auth/auth.py +0 -97
- ivoryos/routes/auth/templates/auth/login.html +0 -25
- ivoryos/routes/auth/templates/auth/signup.html +0 -32
- ivoryos/routes/control/__init__.py +0 -0
- ivoryos/routes/control/control.py +0 -429
- ivoryos/routes/control/templates/control/controllers.html +0 -78
- ivoryos/routes/control/templates/control/controllers_home.html +0 -55
- ivoryos/routes/control/templates/control/controllers_new.html +0 -89
- ivoryos/routes/database/__init__.py +0 -0
- ivoryos/routes/database/database.py +0 -306
- ivoryos/routes/database/templates/database/scripts_database.html +0 -83
- ivoryos/routes/database/templates/database/step_card.html +0 -7
- ivoryos/routes/database/templates/database/workflow_database.html +0 -103
- ivoryos/routes/database/templates/database/workflow_view.html +0 -130
- ivoryos/routes/design/__init__.py +0 -0
- ivoryos/routes/design/design.py +0 -792
- ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
- ivoryos/routes/design/templates/design/experiment_run.html +0 -558
- ivoryos/routes/main/__init__.py +0 -0
- ivoryos/routes/main/main.py +0 -42
- ivoryos/routes/main/templates/main/help.html +0 -141
- ivoryos/routes/main/templates/main/home.html +0 -103
- ivoryos/static/favicon.ico +0 -0
- ivoryos/static/js/overlay.js +0 -12
- ivoryos/static/js/socket_handler.js +0 -125
- ivoryos/static/js/sortable_card.js +0 -24
- ivoryos/static/js/sortable_design.js +0 -105
- ivoryos/static/logo.webp +0 -0
- ivoryos/static/style.css +0 -211
- ivoryos/templates/base.html +0 -157
- ivoryos/utils/__init__.py +0 -0
- ivoryos/utils/bo_campaign.py +0 -87
- ivoryos/utils/client_proxy.py +0 -57
- ivoryos/utils/db_models.py +0 -700
- ivoryos/utils/form.py +0 -560
- ivoryos/utils/global_config.py +0 -87
- ivoryos/utils/input_types.md +0 -14
- ivoryos/utils/llm_agent.py +0 -183
- ivoryos/utils/script_runner.py +0 -336
- ivoryos/utils/task_runner.py +0 -81
- ivoryos/utils/utils.py +0 -422
- ivoryos/version.py +0 -1
- requirements.txt +1 -11
demo_code_plugin/plugin.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
| 1 |
-
import eventlet
|
| 2 |
-
eventlet.monkey_patch()
|
| 3 |
-
|
| 4 |
from flask import render_template, Blueprint, current_app
|
| 5 |
import os
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from flask import render_template, Blueprint, current_app
|
| 2 |
import os
|
| 3 |
|
demo_code_plugin/templates/example.html
CHANGED
|
@@ -79,6 +79,14 @@
|
|
| 79 |
For authentication, user passwords are securely hashed using <code>bcrypt</code> before being stored.
|
| 80 |
Use this demo to explore IvoryOS features!
|
| 81 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
<h2>Source Code</h2>
|
| 83 |
<pre><code class="language-python">{{ code|e }}</code></pre>
|
| 84 |
</div>
|
|
|
|
| 79 |
For authentication, user passwords are securely hashed using <code>bcrypt</code> before being stored.
|
| 80 |
Use this demo to explore IvoryOS features!
|
| 81 |
</p>
|
| 82 |
+
<div id="progress-warning" style="border: 1px solid #f5c2c7; background-color: #f8d7da; color: #842029; padding: 1rem; border-radius: 0.5rem; margin: 1rem 0; font-size: 0.95rem;">
|
| 83 |
+
⚠️ <strong>Note:</strong> Real-time progress tracking via <code>SocketIO</code> is currently <strong>not working</strong> on Hugging Face Spaces.
|
| 84 |
+
<br>
|
| 85 |
+
This is due to limited WebSocket support in the Hugging Face hosting environment.
|
| 86 |
+
<br>
|
| 87 |
+
Workflow will still work, results can be tracked via the <strong>Data</strong> tab.
|
| 88 |
+
<br>
|
| 89 |
+
</div>
|
| 90 |
<h2>Source Code</h2>
|
| 91 |
<pre><code class="language-python">{{ code|e }}</code></pre>
|
| 92 |
</div>
|
ivoryos/__init__.py
DELETED
|
@@ -1,210 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
import sys
|
| 3 |
-
from typing import Union
|
| 4 |
-
import eventlet
|
| 5 |
-
eventlet.monkey_patch()
|
| 6 |
-
|
| 7 |
-
from flask import Flask, redirect, url_for, g, Blueprint
|
| 8 |
-
|
| 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.database.database import database
|
| 13 |
-
from ivoryos.routes.design.design import design, socketio
|
| 14 |
-
from ivoryos.routes.main.main import main
|
| 15 |
-
# from ivoryos.routes.monitor.monitor import monitor
|
| 16 |
-
from ivoryos.utils import utils
|
| 17 |
-
from ivoryos.utils.db_models import db, User
|
| 18 |
-
from ivoryos.utils.global_config import GlobalConfig
|
| 19 |
-
from ivoryos.utils.script_runner import ScriptRunner
|
| 20 |
-
from ivoryos.version import __version__ as ivoryos_version
|
| 21 |
-
from importlib.metadata import entry_points
|
| 22 |
-
|
| 23 |
-
global_config = GlobalConfig()
|
| 24 |
-
from sqlalchemy import event
|
| 25 |
-
from sqlalchemy.engine import Engine
|
| 26 |
-
import sqlite3
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
@event.listens_for(Engine, "connect")
|
| 30 |
-
def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
|
| 31 |
-
if isinstance(dbapi_connection, sqlite3.Connection):
|
| 32 |
-
cursor = dbapi_connection.cursor()
|
| 33 |
-
cursor.execute("PRAGMA foreign_keys=ON")
|
| 34 |
-
cursor.close()
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
|
| 38 |
-
app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
|
| 39 |
-
app.register_blueprint(main, url_prefix=url_prefix)
|
| 40 |
-
app.register_blueprint(auth, url_prefix=url_prefix)
|
| 41 |
-
app.register_blueprint(control, url_prefix=url_prefix)
|
| 42 |
-
app.register_blueprint(design, url_prefix=url_prefix)
|
| 43 |
-
app.register_blueprint(database, url_prefix=url_prefix)
|
| 44 |
-
|
| 45 |
-
@login_manager.user_loader
|
| 46 |
-
def load_user(user_id):
|
| 47 |
-
"""
|
| 48 |
-
This function is called by Flask-Login on every request to get the
|
| 49 |
-
current user object from the user ID stored in the session.
|
| 50 |
-
"""
|
| 51 |
-
# The correct implementation is to fetch the user from the database.
|
| 52 |
-
return db.session.get(User, user_id)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def create_app(config_class=None):
|
| 56 |
-
"""
|
| 57 |
-
create app, init database
|
| 58 |
-
"""
|
| 59 |
-
app.config.from_object(config_class or 'config.get_config()')
|
| 60 |
-
os.makedirs(app.config["OUTPUT_FOLDER"], exist_ok=True)
|
| 61 |
-
# Initialize extensions
|
| 62 |
-
socketio.init_app(app, cookie=None, logger=True, engineio_logger=True)
|
| 63 |
-
login_manager.init_app(app)
|
| 64 |
-
login_manager.login_view = "auth.login"
|
| 65 |
-
db.init_app(app)
|
| 66 |
-
|
| 67 |
-
# Create database tables
|
| 68 |
-
with app.app_context():
|
| 69 |
-
db.create_all()
|
| 70 |
-
|
| 71 |
-
# Additional setup
|
| 72 |
-
utils.create_gui_dir(app.config['OUTPUT_FOLDER'])
|
| 73 |
-
|
| 74 |
-
# logger_list = app.config["LOGGERS"]
|
| 75 |
-
logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
|
| 76 |
-
logger = utils.start_logger(socketio, 'gui_logger', logger_path)
|
| 77 |
-
|
| 78 |
-
@app.before_request
|
| 79 |
-
def before_request():
|
| 80 |
-
"""
|
| 81 |
-
Called before
|
| 82 |
-
|
| 83 |
-
"""
|
| 84 |
-
g.logger = logger
|
| 85 |
-
g.socketio = socketio
|
| 86 |
-
|
| 87 |
-
@app.route('/')
|
| 88 |
-
def redirect_to_prefix():
|
| 89 |
-
return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
|
| 90 |
-
|
| 91 |
-
return app
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, model=None,
|
| 95 |
-
config: Config = None,
|
| 96 |
-
logger: Union[str, list] = None,
|
| 97 |
-
logger_output_name: str = None,
|
| 98 |
-
enable_design: bool = True,
|
| 99 |
-
blueprint_plugins: Union[list, Blueprint] = [],
|
| 100 |
-
exclude_names: list = [],
|
| 101 |
-
):
|
| 102 |
-
"""
|
| 103 |
-
Start ivoryOS app server.
|
| 104 |
-
|
| 105 |
-
:param module: module name, __name__ for current module
|
| 106 |
-
:param host: host address, defaults to 0.0.0.0
|
| 107 |
-
:param port: port, defaults to None, and will use 8000
|
| 108 |
-
:param debug: debug mode, defaults to None (True)
|
| 109 |
-
:param llm_server: llm server, defaults to None.
|
| 110 |
-
:param model: llm model, defaults to None. If None, app will run without text-to-code feature
|
| 111 |
-
:param config: config class, defaults to None
|
| 112 |
-
:param logger: logger name of list of logger names, defaults to None
|
| 113 |
-
:param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
|
| 114 |
-
:param enable_design: enable design canvas, database and workflow execution
|
| 115 |
-
:param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
|
| 116 |
-
:param exclude_names: list[str] module names to exclude from parsing
|
| 117 |
-
"""
|
| 118 |
-
app = create_app(config_class=config or get_config()) # Create app instance using factory function
|
| 119 |
-
|
| 120 |
-
# plugins = load_installed_plugins(app, socketio)
|
| 121 |
-
plugins = []
|
| 122 |
-
if blueprint_plugins:
|
| 123 |
-
config_plugins = load_plugins(blueprint_plugins, app, socketio)
|
| 124 |
-
plugins.extend(config_plugins)
|
| 125 |
-
|
| 126 |
-
def inject_nav_config():
|
| 127 |
-
"""Make NAV_CONFIG available globally to all templates."""
|
| 128 |
-
return dict(
|
| 129 |
-
enable_design=enable_design,
|
| 130 |
-
plugins=plugins,
|
| 131 |
-
)
|
| 132 |
-
|
| 133 |
-
app.context_processor(inject_nav_config)
|
| 134 |
-
port = port or int(os.environ.get("PORT", 8000))
|
| 135 |
-
debug = debug if debug is not None else app.config.get('DEBUG', True)
|
| 136 |
-
|
| 137 |
-
app.config["LOGGERS"] = logger
|
| 138 |
-
app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
|
| 139 |
-
logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
|
| 140 |
-
dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
|
| 141 |
-
|
| 142 |
-
if module:
|
| 143 |
-
app.config["MODULE"] = module
|
| 144 |
-
app.config["OFF_LINE"] = False
|
| 145 |
-
global_config.deck = sys.modules[module]
|
| 146 |
-
global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
|
| 147 |
-
output_path=dummy_deck_path,
|
| 148 |
-
save=True,
|
| 149 |
-
exclude_names=exclude_names
|
| 150 |
-
)
|
| 151 |
-
else:
|
| 152 |
-
app.config["OFF_LINE"] = True
|
| 153 |
-
if model:
|
| 154 |
-
app.config["ENABLE_LLM"] = True
|
| 155 |
-
app.config["LLM_MODEL"] = model
|
| 156 |
-
app.config["LLM_SERVER"] = llm_server
|
| 157 |
-
utils.install_and_import('openai')
|
| 158 |
-
from ivoryos.utils.llm_agent import LlmAgent
|
| 159 |
-
global_config.agent = LlmAgent(host=llm_server, model=model,
|
| 160 |
-
output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
|
| 161 |
-
else:
|
| 162 |
-
app.config["ENABLE_LLM"] = False
|
| 163 |
-
if logger and type(logger) is str:
|
| 164 |
-
utils.start_logger(socketio, log_filename=logger_path, logger_name=logger)
|
| 165 |
-
elif type(logger) is list:
|
| 166 |
-
for log in logger:
|
| 167 |
-
utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
|
| 168 |
-
|
| 169 |
-
# in case Python 3.12 or higher doesn't log URL
|
| 170 |
-
if sys.version_info >= (3, 12):
|
| 171 |
-
ip = utils.get_ip_address()
|
| 172 |
-
print(f"Server running at http://localhost:{port}")
|
| 173 |
-
if not ip == "127.0.0.1":
|
| 174 |
-
print(f"Server running at http://{ip}:{port}")
|
| 175 |
-
socketio.run(app, host=host, port=port, debug=debug, use_reloader=False, allow_unsafe_werkzeug=True)
|
| 176 |
-
# return app
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
def load_installed_plugins(app, socketio):
|
| 180 |
-
"""
|
| 181 |
-
Dynamically load installed plugins and attach Flask-SocketIO.
|
| 182 |
-
"""
|
| 183 |
-
plugin_names = []
|
| 184 |
-
for entry_point in entry_points().get("ivoryos.plugins", []):
|
| 185 |
-
plugin = entry_point.load()
|
| 186 |
-
|
| 187 |
-
# If the plugin has an `init_socketio()` function, pass socketio
|
| 188 |
-
if hasattr(plugin, 'init_socketio'):
|
| 189 |
-
plugin.init_socketio(socketio)
|
| 190 |
-
|
| 191 |
-
plugin_names.append(entry_point.name)
|
| 192 |
-
app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
|
| 193 |
-
|
| 194 |
-
return plugin_names
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
|
| 198 |
-
"""
|
| 199 |
-
Dynamically load installed plugins and attach Flask-SocketIO.
|
| 200 |
-
"""
|
| 201 |
-
plugin_names = []
|
| 202 |
-
if not isinstance(blueprints, list):
|
| 203 |
-
blueprints = [blueprints]
|
| 204 |
-
for blueprint in blueprints:
|
| 205 |
-
# If the plugin has an `init_socketio()` function, pass socketio
|
| 206 |
-
if hasattr(blueprint, 'init_socketio'):
|
| 207 |
-
blueprint.init_socketio(socketio)
|
| 208 |
-
plugin_names.append(blueprint.name)
|
| 209 |
-
app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
|
| 210 |
-
return plugin_names
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/config.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from dotenv import load_dotenv
|
| 3 |
-
|
| 4 |
-
# Load environment variables from .env file
|
| 5 |
-
load_dotenv()
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
class Config:
|
| 9 |
-
SECRET_KEY = os.getenv('SECRET_KEY', 'default_secret_key')
|
| 10 |
-
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY', None)
|
| 11 |
-
|
| 12 |
-
OUTPUT_FOLDER = os.path.join(os.path.abspath(os.curdir), 'ivoryos_data')
|
| 13 |
-
CSV_FOLDER = os.path.join(OUTPUT_FOLDER, 'config_csv/')
|
| 14 |
-
SCRIPT_FOLDER = os.path.join(OUTPUT_FOLDER, 'scripts/')
|
| 15 |
-
DATA_FOLDER = os.path.join(OUTPUT_FOLDER, 'results/')
|
| 16 |
-
DUMMY_DECK = os.path.join(OUTPUT_FOLDER, 'pseudo_deck/')
|
| 17 |
-
LLM_OUTPUT = os.path.join(OUTPUT_FOLDER, 'llm_output/')
|
| 18 |
-
DECK_HISTORY = os.path.join(OUTPUT_FOLDER, 'deck_history.txt')
|
| 19 |
-
LOGGERS_PATH = "default.log"
|
| 20 |
-
|
| 21 |
-
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(OUTPUT_FOLDER, 'ivoryos.db')}"
|
| 22 |
-
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
| 23 |
-
|
| 24 |
-
ENABLE_LLM = True if OPENAI_API_KEY else False
|
| 25 |
-
OFF_LINE = True
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
class DevelopmentConfig(Config):
|
| 29 |
-
DEBUG = True
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
class ProductionConfig(Config):
|
| 33 |
-
DEBUG = False
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
class TestingConfig(Config):
|
| 37 |
-
DEBUG = True
|
| 38 |
-
TESTING = True
|
| 39 |
-
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' # Use an in-memory SQLite database for tests
|
| 40 |
-
WTF_CSRF_ENABLED = False # Disable CSRF for testing forms
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
class DemoConfig(Config):
|
| 45 |
-
DEBUG = False
|
| 46 |
-
SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
|
| 47 |
-
OUTPUT_FOLDER = os.path.join(os.path.abspath(os.curdir), '/tmp/ivoryos_data')
|
| 48 |
-
CSV_FOLDER = os.path.join(OUTPUT_FOLDER, 'config_csv/')
|
| 49 |
-
SCRIPT_FOLDER = os.path.join(OUTPUT_FOLDER, 'scripts/')
|
| 50 |
-
DATA_FOLDER = os.path.join(OUTPUT_FOLDER, 'results/')
|
| 51 |
-
DUMMY_DECK = os.path.join(OUTPUT_FOLDER, 'pseudo_deck/')
|
| 52 |
-
LLM_OUTPUT = os.path.join(OUTPUT_FOLDER, 'llm_output/')
|
| 53 |
-
DECK_HISTORY = os.path.join(OUTPUT_FOLDER, 'deck_history.txt')
|
| 54 |
-
# session and cookies
|
| 55 |
-
SESSION_COOKIE_SECURE = True
|
| 56 |
-
SESSION_COOKIE_SAMESITE = "None"
|
| 57 |
-
SESSION_COOKIE_HTTPONLY = True
|
| 58 |
-
|
| 59 |
-
def get_config(env='dev'):
|
| 60 |
-
if env == 'production':
|
| 61 |
-
return ProductionConfig()
|
| 62 |
-
elif env == 'testing':
|
| 63 |
-
return TestingConfig()
|
| 64 |
-
elif env == 'demo':
|
| 65 |
-
return DemoConfig()
|
| 66 |
-
return DevelopmentConfig()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/__init__.py
DELETED
|
File without changes
|
ivoryos/routes/auth/__init__.py
DELETED
|
File without changes
|
ivoryos/routes/auth/auth.py
DELETED
|
@@ -1,97 +0,0 @@
|
|
| 1 |
-
from flask import Blueprint, redirect, url_for, flash, request, render_template, session
|
| 2 |
-
from flask_login import login_required, login_user, logout_user, LoginManager
|
| 3 |
-
import bcrypt
|
| 4 |
-
|
| 5 |
-
from ivoryos.utils.db_models import Script, User, db
|
| 6 |
-
from ivoryos.utils.utils import post_script_file
|
| 7 |
-
login_manager = LoginManager()
|
| 8 |
-
|
| 9 |
-
auth = Blueprint('auth', __name__, template_folder='templates/auth')
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@auth.route('/auth/login', methods=['GET', 'POST'])
|
| 13 |
-
def login():
|
| 14 |
-
"""
|
| 15 |
-
.. :quickref: User; login user
|
| 16 |
-
|
| 17 |
-
.. http:get:: /login
|
| 18 |
-
|
| 19 |
-
load user login form.
|
| 20 |
-
|
| 21 |
-
.. http:post:: /login
|
| 22 |
-
|
| 23 |
-
:form username: username
|
| 24 |
-
:form password: password
|
| 25 |
-
:status 302: and then redirects to homepage
|
| 26 |
-
:status 401: incorrect password, redirects to :http:get:`/ivoryos/login`
|
| 27 |
-
"""
|
| 28 |
-
if request.method == 'POST':
|
| 29 |
-
username = request.form.get('username')
|
| 30 |
-
password = request.form.get('password')
|
| 31 |
-
|
| 32 |
-
# session.query(User, User.name).all()
|
| 33 |
-
user = db.session.query(User).filter(User.username == username).first()
|
| 34 |
-
input_password = password.encode('utf-8')
|
| 35 |
-
# if user and bcrypt.checkpw(input_password, user.hashPassword.encode('utf-8')):
|
| 36 |
-
if user and bcrypt.checkpw(input_password, user.hashPassword):
|
| 37 |
-
# password.encode("utf-8")
|
| 38 |
-
# user = User(username, password.encode("utf-8"))
|
| 39 |
-
login_user(user)
|
| 40 |
-
session['user'] = username
|
| 41 |
-
script_file = Script(author=username)
|
| 42 |
-
session["script"] = script_file.as_dict()
|
| 43 |
-
session['hidden_functions'], session['card_order'], session['prompt'] = {}, {}, {}
|
| 44 |
-
session['autofill'] = False
|
| 45 |
-
post_script_file(script_file)
|
| 46 |
-
return redirect(url_for('main.index'))
|
| 47 |
-
else:
|
| 48 |
-
flash("Incorrect username or password")
|
| 49 |
-
return redirect(url_for('auth.login')), 401
|
| 50 |
-
return render_template('login.html')
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
@auth.route('/auth/signup', methods=['GET', 'POST'])
|
| 54 |
-
def signup():
|
| 55 |
-
"""
|
| 56 |
-
.. :quickref: User; signup for a new account
|
| 57 |
-
|
| 58 |
-
.. http:get:: /signup
|
| 59 |
-
|
| 60 |
-
load user sighup
|
| 61 |
-
|
| 62 |
-
.. http:post:: /signup
|
| 63 |
-
|
| 64 |
-
:form username: username
|
| 65 |
-
:form password: password
|
| 66 |
-
:status 302: and then redirects to :http:get:`/ivoryos/login`
|
| 67 |
-
:status 409: when user already exists, redirects to :http:get:`/ivoryos/signup`
|
| 68 |
-
"""
|
| 69 |
-
if request.method == 'POST':
|
| 70 |
-
username = request.form.get('username')
|
| 71 |
-
password = request.form.get('password')
|
| 72 |
-
|
| 73 |
-
# Query the database to see if the user already exists.
|
| 74 |
-
existing_user = User.query.filter_by(username=username).first()
|
| 75 |
-
|
| 76 |
-
if existing_user:
|
| 77 |
-
flash("User already exists :(", "error")
|
| 78 |
-
return render_template('signup.html'), 409
|
| 79 |
-
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
|
| 80 |
-
user = User(username, hashed)
|
| 81 |
-
db.session.add(user)
|
| 82 |
-
db.session.commit()
|
| 83 |
-
return redirect(url_for('auth.login'))
|
| 84 |
-
return render_template('signup.html')
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
@auth.route("/auth/logout")
|
| 88 |
-
@login_required
|
| 89 |
-
def logout():
|
| 90 |
-
"""
|
| 91 |
-
.. :quickref: User; logout the user
|
| 92 |
-
|
| 93 |
-
logout the current user, clear session info, and redirect to the login page.
|
| 94 |
-
"""
|
| 95 |
-
logout_user()
|
| 96 |
-
session.clear()
|
| 97 |
-
return redirect(url_for('auth.login'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/auth/templates/auth/login.html
DELETED
|
@@ -1,25 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Login{% endblock %}
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
{% block body %}
|
| 6 |
-
<div class= "login">
|
| 7 |
-
<div class="bg-white rounded shadow-sm flex-fill">
|
| 8 |
-
<div class="p-4" style="align-items: center">
|
| 9 |
-
<h5>Log in</h5>
|
| 10 |
-
<form role="form" method='POST' name="login" action="{{ url_for('auth.login') }}">
|
| 11 |
-
<div class="input-group mb-3">
|
| 12 |
-
<label class="input-group-text" for="username">Username</label>
|
| 13 |
-
<input class="form-control" type="text" id="username" name="username">
|
| 14 |
-
</div>
|
| 15 |
-
<div class="input-group mb-3">
|
| 16 |
-
<label class="input-group-text" for="password">Password</label>
|
| 17 |
-
<input class="form-control" type="password" id="password" name="password">
|
| 18 |
-
</div>
|
| 19 |
-
<button type="submit" class="btn btn-secondary" name="login" style="width: 100%;">login</button>
|
| 20 |
-
</form>
|
| 21 |
-
<p class="message">Not registered? <a href="{{ url_for('auth.signup') }}">Create a new account</a>
|
| 22 |
-
</div>
|
| 23 |
-
</div>
|
| 24 |
-
</div>
|
| 25 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/auth/templates/auth/signup.html
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Signup{% endblock %}
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
{% block body %}
|
| 6 |
-
<div class= "login">
|
| 7 |
-
<div class="bg-white rounded shadow-sm flex-fill">
|
| 8 |
-
<div class="p-4" style="align: center">
|
| 9 |
-
<h5>Create a new account</h5>
|
| 10 |
-
<form role="form" method='POST' name="signup" action="{{ url_for('auth.signup') }}">
|
| 11 |
-
|
| 12 |
-
<div class="input-group mb-3">
|
| 13 |
-
<label class="input-group-text" for="username">Username</label>
|
| 14 |
-
<input class="form-control" type="text" id="username" name="username" required>
|
| 15 |
-
</div>
|
| 16 |
-
<div class="input-group mb-3">
|
| 17 |
-
<label class="input-group-text" for="password">Password</label>
|
| 18 |
-
<input class="form-control" type="password" id="password" name="password" required>
|
| 19 |
-
</div>
|
| 20 |
-
{# <div class="input-group mb-3">#}
|
| 21 |
-
{# <label class="input-group-text" for="confirm_password">Confirm Password</label>#}
|
| 22 |
-
{# <input class="form-control" type="confirm_password" id="confirm_password" name="confirm_password">#}
|
| 23 |
-
{# </div>#}
|
| 24 |
-
|
| 25 |
-
<button type="submit" class="btn btn-secondary" name="login" style="width: 100%;">Sign up</button>
|
| 26 |
-
</form>
|
| 27 |
-
<p class="message" >Already registered? <a href="{{ url_for('auth.login') }}">Sign In</a></p>
|
| 28 |
-
</div>
|
| 29 |
-
</div>
|
| 30 |
-
</div>
|
| 31 |
-
|
| 32 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/control/__init__.py
DELETED
|
File without changes
|
ivoryos/routes/control/control.py
DELETED
|
@@ -1,429 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify, \
|
| 4 |
-
send_file
|
| 5 |
-
from flask_login import login_required
|
| 6 |
-
|
| 7 |
-
from ivoryos.utils.client_proxy import export_to_python, create_function
|
| 8 |
-
from ivoryos.utils.global_config import GlobalConfig
|
| 9 |
-
from ivoryos.utils import utils
|
| 10 |
-
from ivoryos.utils.form import create_form_from_module, format_name
|
| 11 |
-
from ivoryos.utils.task_runner import TaskRunner
|
| 12 |
-
|
| 13 |
-
global_config = GlobalConfig()
|
| 14 |
-
runner = TaskRunner()
|
| 15 |
-
|
| 16 |
-
control = Blueprint('control', __name__, template_folder='templates/control')
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@control.route("/control/home/deck", strict_slashes=False)
|
| 20 |
-
@login_required
|
| 21 |
-
def deck_controllers():
|
| 22 |
-
"""
|
| 23 |
-
.. :quickref: Direct Control; controls home interface
|
| 24 |
-
|
| 25 |
-
deck control home interface for listing all deck instruments
|
| 26 |
-
|
| 27 |
-
.. http:get:: /control/home/deck
|
| 28 |
-
"""
|
| 29 |
-
deck_variables = global_config.deck_snapshot.keys()
|
| 30 |
-
deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
|
| 31 |
-
return render_template('controllers_home.html', defined_variables=deck_variables, deck=True, history=deck_list)
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
@control.route("/control/new/", strict_slashes=False)
|
| 35 |
-
@control.route("/control/new/<instrument>", methods=['GET', 'POST'])
|
| 36 |
-
@login_required
|
| 37 |
-
def new_controller(instrument=None):
|
| 38 |
-
"""
|
| 39 |
-
.. :quickref: Direct Control; connect to a new device
|
| 40 |
-
|
| 41 |
-
interface for connecting a new <instrument>
|
| 42 |
-
|
| 43 |
-
.. http:get:: /control/new/
|
| 44 |
-
|
| 45 |
-
:param instrument: instrument name
|
| 46 |
-
:type instrument: str
|
| 47 |
-
|
| 48 |
-
.. http:post:: /control/new/
|
| 49 |
-
|
| 50 |
-
:form device_name: module instance name (e.g. my_instance = MyClass())
|
| 51 |
-
:form kwargs: dynamic module initialization kwargs fields
|
| 52 |
-
|
| 53 |
-
"""
|
| 54 |
-
device = None
|
| 55 |
-
args = None
|
| 56 |
-
if instrument:
|
| 57 |
-
|
| 58 |
-
device = find_instrument_by_name(instrument)
|
| 59 |
-
args = utils.inspect.signature(device.__init__)
|
| 60 |
-
|
| 61 |
-
if request.method == 'POST':
|
| 62 |
-
device_name = request.form.get("device_name", "")
|
| 63 |
-
if device_name and device_name in globals():
|
| 64 |
-
flash("Device name is defined. Try another name, or leave it as blank to auto-configure")
|
| 65 |
-
return render_template('controllers_new.html', instrument=instrument,
|
| 66 |
-
api_variables=global_config.api_variables,
|
| 67 |
-
device=device, args=args, defined_variables=global_config.defined_variables)
|
| 68 |
-
if device_name == "":
|
| 69 |
-
device_name = device.__name__.lower() + "_"
|
| 70 |
-
num = 1
|
| 71 |
-
while device_name + str(num) in global_config.defined_variables:
|
| 72 |
-
num += 1
|
| 73 |
-
device_name = device_name + str(num)
|
| 74 |
-
kwargs = request.form.to_dict()
|
| 75 |
-
kwargs.pop("device_name")
|
| 76 |
-
for i in kwargs:
|
| 77 |
-
if kwargs[i] in global_config.defined_variables:
|
| 78 |
-
kwargs[i] = global_config.defined_variables[kwargs[i]]
|
| 79 |
-
try:
|
| 80 |
-
utils.convert_config_type(kwargs, device.__init__.__annotations__, is_class=True)
|
| 81 |
-
except Exception as e:
|
| 82 |
-
flash(e)
|
| 83 |
-
try:
|
| 84 |
-
global_config.defined_variables[device_name] = device(**kwargs)
|
| 85 |
-
# global_config.defined_variables.add(device_name)
|
| 86 |
-
return redirect(url_for('control.controllers_home'))
|
| 87 |
-
except Exception as e:
|
| 88 |
-
flash(e)
|
| 89 |
-
return render_template('controllers_new.html', instrument=instrument, api_variables=global_config.api_variables,
|
| 90 |
-
device=device, args=args, defined_variables=global_config.defined_variables)
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
@control.route("/control/home/temp", strict_slashes=False)
|
| 94 |
-
@login_required
|
| 95 |
-
def controllers_home():
|
| 96 |
-
"""
|
| 97 |
-
.. :quickref: Direct Control; temp control home interface
|
| 98 |
-
|
| 99 |
-
temporarily connected devices home interface for listing all instruments
|
| 100 |
-
|
| 101 |
-
.. http:get:: /control/home/temp
|
| 102 |
-
|
| 103 |
-
"""
|
| 104 |
-
# defined_variables = parse_deck(deck)
|
| 105 |
-
defined_variables = global_config.defined_variables.keys()
|
| 106 |
-
return render_template('controllers_home.html', defined_variables=defined_variables)
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
@control.route("/control/<instrument>/methods", methods=['GET', 'POST'])
|
| 110 |
-
@login_required
|
| 111 |
-
def controllers(instrument: str):
|
| 112 |
-
"""
|
| 113 |
-
.. :quickref: Direct Control; control interface
|
| 114 |
-
|
| 115 |
-
control interface for selected <instrument>
|
| 116 |
-
|
| 117 |
-
.. http:get:: /control/<instrument>/methods
|
| 118 |
-
|
| 119 |
-
:param instrument: instrument name
|
| 120 |
-
:type instrument: str
|
| 121 |
-
|
| 122 |
-
.. http:post:: /control/<instrument>/methods
|
| 123 |
-
|
| 124 |
-
:form hidden_name: function name (hidden field)
|
| 125 |
-
:form kwargs: dynamic kwargs field
|
| 126 |
-
|
| 127 |
-
"""
|
| 128 |
-
inst_object = find_instrument_by_name(instrument)
|
| 129 |
-
_forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
| 130 |
-
functions = list(_forms.keys())
|
| 131 |
-
|
| 132 |
-
order = get_session_by_instrument('card_order', instrument)
|
| 133 |
-
hidden_functions = get_session_by_instrument('hide_function', instrument)
|
| 134 |
-
|
| 135 |
-
for function in functions:
|
| 136 |
-
if function not in hidden_functions and function not in order:
|
| 137 |
-
order.append(function)
|
| 138 |
-
post_session_by_instrument('card_order', instrument, order)
|
| 139 |
-
forms = {name: _forms[name] for name in order if name in _forms}
|
| 140 |
-
if request.method == 'POST':
|
| 141 |
-
all_kwargs = request.form.copy()
|
| 142 |
-
method_name = all_kwargs.pop("hidden_name", None)
|
| 143 |
-
# if method_name is not None:
|
| 144 |
-
form = forms.get(method_name)
|
| 145 |
-
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
| 146 |
-
function_executable = getattr(inst_object, method_name)
|
| 147 |
-
if form and form.validate_on_submit():
|
| 148 |
-
try:
|
| 149 |
-
kwargs.pop("hidden_name")
|
| 150 |
-
output = runner.run_single_step(instrument, method_name, kwargs, wait=True,
|
| 151 |
-
current_app=current_app._get_current_object())
|
| 152 |
-
# output = function_executable(**kwargs)
|
| 153 |
-
flash(f"\nRun Success! Output value: {output}.")
|
| 154 |
-
except Exception as e:
|
| 155 |
-
flash(e.__str__())
|
| 156 |
-
else:
|
| 157 |
-
flash(form.errors)
|
| 158 |
-
return render_template('controllers.html', instrument=instrument, forms=forms, format_name=format_name)
|
| 159 |
-
|
| 160 |
-
@control.route("/control/download", strict_slashes=False)
|
| 161 |
-
@login_required
|
| 162 |
-
def download_proxy():
|
| 163 |
-
"""
|
| 164 |
-
.. :quickref: Direct Control; download proxy interface
|
| 165 |
-
|
| 166 |
-
download proxy interface
|
| 167 |
-
|
| 168 |
-
.. http:get:: /control/download
|
| 169 |
-
"""
|
| 170 |
-
snapshot = global_config.deck_snapshot.copy()
|
| 171 |
-
class_definitions = {}
|
| 172 |
-
# Iterate through each instrument in the snapshot
|
| 173 |
-
for instrument_key, instrument_data in snapshot.items():
|
| 174 |
-
# Iterate through each function associated with the current instrument
|
| 175 |
-
for function_key, function_data in instrument_data.items():
|
| 176 |
-
# Convert the function signature to a string representation
|
| 177 |
-
function_data['signature'] = str(function_data['signature'])
|
| 178 |
-
class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
|
| 179 |
-
class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
|
| 180 |
-
# Export the generated class definitions to a .py script
|
| 181 |
-
export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
|
| 182 |
-
filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
|
| 183 |
-
return send_file(os.path.abspath(filepath), as_attachment=True)
|
| 184 |
-
|
| 185 |
-
@control.route("/api/control/", strict_slashes=False, methods=['GET'])
|
| 186 |
-
@control.route("/api/control/<instrument>", methods=['POST'])
|
| 187 |
-
def backend_control(instrument: str=None):
|
| 188 |
-
"""
|
| 189 |
-
.. :quickref: Backend Control; backend control
|
| 190 |
-
|
| 191 |
-
backend control through http requests
|
| 192 |
-
|
| 193 |
-
.. http:get:: /api/control/
|
| 194 |
-
|
| 195 |
-
:param instrument: instrument name
|
| 196 |
-
:type instrument: str
|
| 197 |
-
|
| 198 |
-
.. http:post:: /api/control/
|
| 199 |
-
|
| 200 |
-
"""
|
| 201 |
-
if instrument:
|
| 202 |
-
inst_object = find_instrument_by_name(instrument)
|
| 203 |
-
forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
| 204 |
-
|
| 205 |
-
if request.method == 'POST':
|
| 206 |
-
method_name = request.form.get("hidden_name", None)
|
| 207 |
-
form = forms.get(method_name, None)
|
| 208 |
-
if form:
|
| 209 |
-
kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
|
| 210 |
-
wait = request.form.get("hidden_wait", "true") == "true"
|
| 211 |
-
output = runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
|
| 212 |
-
current_app=current_app._get_current_object())
|
| 213 |
-
return jsonify(output), 200
|
| 214 |
-
|
| 215 |
-
snapshot = global_config.deck_snapshot.copy()
|
| 216 |
-
# Iterate through each instrument in the snapshot
|
| 217 |
-
for instrument_key, instrument_data in snapshot.items():
|
| 218 |
-
# Iterate through each function associated with the current instrument
|
| 219 |
-
for function_key, function_data in instrument_data.items():
|
| 220 |
-
# Convert the function signature to a string representation
|
| 221 |
-
function_data['signature'] = str(function_data['signature'])
|
| 222 |
-
return jsonify(snapshot), 200
|
| 223 |
-
|
| 224 |
-
# @control.route("/api/control", strict_slashes=False, methods=['GET'])
|
| 225 |
-
# def backend_client():
|
| 226 |
-
# """
|
| 227 |
-
# .. :quickref: Backend Control; get snapshot
|
| 228 |
-
#
|
| 229 |
-
# backend control through http requests
|
| 230 |
-
#
|
| 231 |
-
# .. http:get:: /api/control/summary
|
| 232 |
-
# """
|
| 233 |
-
# # Create a snapshot of the current deck configuration
|
| 234 |
-
# snapshot = global_config.deck_snapshot.copy()
|
| 235 |
-
#
|
| 236 |
-
# # Iterate through each instrument in the snapshot
|
| 237 |
-
# for instrument_key, instrument_data in snapshot.items():
|
| 238 |
-
# # Iterate through each function associated with the current instrument
|
| 239 |
-
# for function_key, function_data in instrument_data.items():
|
| 240 |
-
# # Convert the function signature to a string representation
|
| 241 |
-
# function_data['signature'] = str(function_data['signature'])
|
| 242 |
-
# return jsonify(snapshot), 200
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
@control.route("/control/import/module", methods=['POST'])
|
| 246 |
-
def import_api():
|
| 247 |
-
"""
|
| 248 |
-
.. :quickref: Advanced Features; Manually import API module(s)
|
| 249 |
-
|
| 250 |
-
importing other Python modules
|
| 251 |
-
|
| 252 |
-
.. http:post:: /control/import/module
|
| 253 |
-
|
| 254 |
-
:form filepath: API (Python class) module filepath
|
| 255 |
-
|
| 256 |
-
import the module and redirect to :http:get:`/ivoryos/control/new/`
|
| 257 |
-
|
| 258 |
-
"""
|
| 259 |
-
filepath = request.form.get('filepath')
|
| 260 |
-
# filepath.replace('\\', '/')
|
| 261 |
-
name = os.path.split(filepath)[-1].split('.')[0]
|
| 262 |
-
try:
|
| 263 |
-
spec = utils.importlib.util.spec_from_file_location(name, filepath)
|
| 264 |
-
module = utils.importlib.util.module_from_spec(spec)
|
| 265 |
-
spec.loader.exec_module(module)
|
| 266 |
-
classes = utils.inspect.getmembers(module, utils.inspect.isclass)
|
| 267 |
-
if len(classes) == 0:
|
| 268 |
-
flash("Invalid import: no class found in the path")
|
| 269 |
-
return redirect(url_for("control.controllers_home"))
|
| 270 |
-
for i in classes:
|
| 271 |
-
globals()[i[0]] = i[1]
|
| 272 |
-
global_config.api_variables.add(i[0])
|
| 273 |
-
# should handle path error and file type error
|
| 274 |
-
except Exception as e:
|
| 275 |
-
flash(e.__str__())
|
| 276 |
-
return redirect(url_for("control.new_controller"))
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
# @control.route("/disconnect", methods=["GET"])
|
| 280 |
-
# @control.route("/disconnect/<device_name>", methods=["GET"])
|
| 281 |
-
# def disconnect(device_name=None):
|
| 282 |
-
# """TODO handle disconnect device"""
|
| 283 |
-
# if device_name:
|
| 284 |
-
# try:
|
| 285 |
-
# exec(device_name + ".disconnect()")
|
| 286 |
-
# except Exception:
|
| 287 |
-
# pass
|
| 288 |
-
# global_config.defined_variables.remove(device_name)
|
| 289 |
-
# globals().pop(device_name)
|
| 290 |
-
# return redirect(url_for('control.controllers_home'))
|
| 291 |
-
#
|
| 292 |
-
# deck_variables = ["deck." + var for var in set(dir(deck))
|
| 293 |
-
# if not (var.startswith("_") or var[0].isupper() or var.startswith("repackage"))
|
| 294 |
-
# and not type(eval("deck." + var)).__module__ == 'builtins']
|
| 295 |
-
# for i in deck_variables:
|
| 296 |
-
# try:
|
| 297 |
-
# exec(i + ".disconnect()")
|
| 298 |
-
# except Exception:
|
| 299 |
-
# pass
|
| 300 |
-
# globals()["deck"] = None
|
| 301 |
-
# return redirect(url_for('control.deck_controllers'))
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
@control.route("/control/import/deck", methods=['POST'])
|
| 305 |
-
def import_deck():
|
| 306 |
-
"""
|
| 307 |
-
.. :quickref: Advanced Features; Manually import a deck
|
| 308 |
-
|
| 309 |
-
.. http:post:: /control/import_deck
|
| 310 |
-
|
| 311 |
-
:form filepath: deck module filepath
|
| 312 |
-
|
| 313 |
-
import the module and redirect to the previous page
|
| 314 |
-
|
| 315 |
-
"""
|
| 316 |
-
script = utils.get_script_file()
|
| 317 |
-
filepath = request.form.get('filepath')
|
| 318 |
-
session['dismiss'] = request.form.get('dismiss')
|
| 319 |
-
update = request.form.get('update')
|
| 320 |
-
back = request.referrer
|
| 321 |
-
if session['dismiss']:
|
| 322 |
-
return redirect(back)
|
| 323 |
-
name = os.path.split(filepath)[-1].split('.')[0]
|
| 324 |
-
try:
|
| 325 |
-
module = utils.import_module_by_filepath(filepath=filepath, name=name)
|
| 326 |
-
utils.save_to_history(filepath, current_app.config["DECK_HISTORY"])
|
| 327 |
-
module_sigs = utils.create_deck_snapshot(module, save=update, output_path=current_app.config["DUMMY_DECK"])
|
| 328 |
-
if not len(module_sigs) > 0:
|
| 329 |
-
flash("Invalid hardware deck, connect instruments in deck script", "error")
|
| 330 |
-
return redirect(url_for("control.deck_controllers"))
|
| 331 |
-
global_config.deck = module
|
| 332 |
-
global_config.deck_snapshot = module_sigs
|
| 333 |
-
|
| 334 |
-
if script.deck is None:
|
| 335 |
-
script.deck = module.__name__
|
| 336 |
-
# file path error exception
|
| 337 |
-
except Exception as e:
|
| 338 |
-
flash(e.__str__())
|
| 339 |
-
return redirect(back)
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
@control.route('/control/<instrument>/save-order', methods=['POST'])
|
| 343 |
-
def save_order(instrument: str):
|
| 344 |
-
"""
|
| 345 |
-
.. :quickref: Control Customization; Save functions' order
|
| 346 |
-
|
| 347 |
-
.. http:post:: /control/save-order
|
| 348 |
-
|
| 349 |
-
save function drag and drop order for the given <instrument>
|
| 350 |
-
|
| 351 |
-
"""
|
| 352 |
-
# Save the new order for the specified group to session
|
| 353 |
-
data = request.json
|
| 354 |
-
post_session_by_instrument('card_order', instrument, data['order'])
|
| 355 |
-
return '', 204
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
@control.route('/control/<instrument>/<function>/hide')
|
| 359 |
-
def hide_function(instrument, function):
|
| 360 |
-
"""
|
| 361 |
-
.. :quickref: Control Customization; Hide function
|
| 362 |
-
|
| 363 |
-
.. http:get:: //control/<instrument>/<function>/hide
|
| 364 |
-
|
| 365 |
-
Hide the given <instrument> and <function>
|
| 366 |
-
|
| 367 |
-
"""
|
| 368 |
-
back = request.referrer
|
| 369 |
-
functions = get_session_by_instrument("hidden_functions", instrument)
|
| 370 |
-
order = get_session_by_instrument("card_order", instrument)
|
| 371 |
-
if function not in functions:
|
| 372 |
-
functions.append(function)
|
| 373 |
-
order.remove(function)
|
| 374 |
-
post_session_by_instrument('hidden_functions', instrument, functions)
|
| 375 |
-
post_session_by_instrument('card_order', instrument, order)
|
| 376 |
-
return redirect(back)
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
@control.route('/control/<instrument>/<function>/unhide')
|
| 380 |
-
def remove_hidden(instrument: str, function: str):
|
| 381 |
-
"""
|
| 382 |
-
.. :quickref: Control Customization; Remove a hidden function
|
| 383 |
-
|
| 384 |
-
.. http:get:: /control/<instrument>/<function>/unhide
|
| 385 |
-
|
| 386 |
-
Un-hide the given <instrument> and <function>
|
| 387 |
-
|
| 388 |
-
"""
|
| 389 |
-
back = request.referrer
|
| 390 |
-
functions = get_session_by_instrument("hidden_functions", instrument)
|
| 391 |
-
order = get_session_by_instrument("card_order", instrument)
|
| 392 |
-
if function in functions:
|
| 393 |
-
functions.remove(function)
|
| 394 |
-
order.append(function)
|
| 395 |
-
post_session_by_instrument('hidden_functions', instrument, functions)
|
| 396 |
-
post_session_by_instrument('card_order', instrument, order)
|
| 397 |
-
return redirect(back)
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
def get_session_by_instrument(session_name, instrument):
|
| 401 |
-
"""get data from session by instrument"""
|
| 402 |
-
session_object = session.get(session_name, {})
|
| 403 |
-
functions = session_object.get(instrument, [])
|
| 404 |
-
return functions
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
def post_session_by_instrument(session_name, instrument, data):
|
| 408 |
-
"""
|
| 409 |
-
save new data to session by instrument
|
| 410 |
-
:param session_name: "card_order" or "hidden_functions"
|
| 411 |
-
:param instrument: function name of class object
|
| 412 |
-
:param data: order list or hidden function list
|
| 413 |
-
"""
|
| 414 |
-
session_object = session.get(session_name, {})
|
| 415 |
-
session_object[instrument] = data
|
| 416 |
-
session[session_name] = session_object
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
def find_instrument_by_name(name: str):
|
| 420 |
-
"""
|
| 421 |
-
find instrument class object by instance name
|
| 422 |
-
"""
|
| 423 |
-
if name.startswith("deck"):
|
| 424 |
-
name = name.replace("deck.", "")
|
| 425 |
-
return getattr(global_config.deck, name)
|
| 426 |
-
elif name in global_config.defined_variables:
|
| 427 |
-
return global_config.defined_variables[name]
|
| 428 |
-
elif name in globals():
|
| 429 |
-
return globals()[name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/control/templates/control/controllers.html
DELETED
|
@@ -1,78 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Controller for {{instrument}}{% endblock %}
|
| 3 |
-
{% block body %}
|
| 4 |
-
<div id="overlay" class="overlay">
|
| 5 |
-
<div>
|
| 6 |
-
<h3 id="overlay-text"></h3>
|
| 7 |
-
<div class="spinner-border" role="status"></div>
|
| 8 |
-
</div>
|
| 9 |
-
</div>
|
| 10 |
-
<h1>{{instrument}} controller</h1>
|
| 11 |
-
{% set hidden = session.get('hidden_functions', {}) %}
|
| 12 |
-
<div class="grid-container" id="sortable-grid">
|
| 13 |
-
{% for function, form in forms.items() %}
|
| 14 |
-
|
| 15 |
-
{% set hidden_instrument = hidden.get(instrument, []) %}
|
| 16 |
-
{% if function not in hidden_instrument %}
|
| 17 |
-
<div class="card" id="{{function}}">
|
| 18 |
-
<div class="bg-white rounded shadow-sm flex-fill">
|
| 19 |
-
<i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title='{{ form.hidden_name.description or "Docstring is not available" }}' ></i>
|
| 20 |
-
<a style="float: right" aria-label="Close" href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"><i class="bi bi-eye-slash-fill"></i></a>
|
| 21 |
-
<div class="form-control" style="border: none">
|
| 22 |
-
<form role="form" method='POST' name="{{function}}" id="{{function}}">
|
| 23 |
-
<div class="form-group">
|
| 24 |
-
{{ form.hidden_tag() }}
|
| 25 |
-
{% for field in form %}
|
| 26 |
-
{% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
|
| 27 |
-
<div class="input-group mb-3">
|
| 28 |
-
<label class="input-group-text">{{ field.label.text }}</label>
|
| 29 |
-
{% if field.type == "SubmitField" %}
|
| 30 |
-
{{ field(class="btn btn-dark") }}
|
| 31 |
-
{% elif field.type == "BooleanField" %}
|
| 32 |
-
{{ field(class="form-check-input") }}
|
| 33 |
-
{% else %}
|
| 34 |
-
{{ field(class="form-control") }}
|
| 35 |
-
{% endif %}
|
| 36 |
-
</div>
|
| 37 |
-
{% endif %}
|
| 38 |
-
{% endfor %}
|
| 39 |
-
</div>
|
| 40 |
-
<div class="input-group mb-3">
|
| 41 |
-
<button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">{{format_name(function)}} </button>
|
| 42 |
-
|
| 43 |
-
</div>
|
| 44 |
-
|
| 45 |
-
</form>
|
| 46 |
-
</div>
|
| 47 |
-
</div>
|
| 48 |
-
</div>
|
| 49 |
-
{% endif %}
|
| 50 |
-
{% endfor %}
|
| 51 |
-
</div>
|
| 52 |
-
<div class="accordion accordion-flush" id="accordionActions" >
|
| 53 |
-
<div class="accordion-item">
|
| 54 |
-
<h4 class="accordion-header">
|
| 55 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#hidden">
|
| 56 |
-
Hidden functions
|
| 57 |
-
</button>
|
| 58 |
-
</h4>
|
| 59 |
-
</div>
|
| 60 |
-
<div id="hidden" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
|
| 61 |
-
<div class="accordion-body">
|
| 62 |
-
{% set hidden_instrument = hidden.get(instrument, []) %}
|
| 63 |
-
{% for function in hidden_instrument %}
|
| 64 |
-
<div>
|
| 65 |
-
{{ function }} <a href="{{ url_for('control.remove_hidden', instrument=instrument, function=function) }}"><i class="bi bi-eye-fill"></i></a>
|
| 66 |
-
</div>
|
| 67 |
-
{% endfor %}
|
| 68 |
-
</div>
|
| 69 |
-
</div>
|
| 70 |
-
</div>
|
| 71 |
-
|
| 72 |
-
<script>
|
| 73 |
-
const saveOrderUrl = `{{ url_for('control.save_order', instrument=instrument) }}`;
|
| 74 |
-
const buttonIds = {{ session['card_order'][instrument] | tojson }};
|
| 75 |
-
</script>
|
| 76 |
-
<script src="{{ url_for('static', filename='js/sortable_card.js') }}"></script>
|
| 77 |
-
<script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
|
| 78 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/control/templates/control/controllers_home.html
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Devices{% endblock %}
|
| 3 |
-
{% block body %}
|
| 4 |
-
<div class="row">
|
| 5 |
-
{% if defined_variables %}
|
| 6 |
-
{% for instrument in defined_variables %}
|
| 7 |
-
<div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
|
| 8 |
-
<div class="bg-white rounded shadow-sm position-relative">
|
| 9 |
-
{% if deck %}
|
| 10 |
-
{# <a href="{{ url_for('control.disconnect', instrument=instrument) }}" class="stretched-link controller-card" style="float: right;color: red; position: relative;">Disconnect <i class="bi bi-x-square"></i></a>#}
|
| 11 |
-
<div class="p-4 controller-card">
|
| 12 |
-
<h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument.split(".")[1]}}</a></h5>
|
| 13 |
-
</div>
|
| 14 |
-
{% else %}
|
| 15 |
-
<div class="p-4 controller-card">
|
| 16 |
-
<h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a></h5>
|
| 17 |
-
</div>
|
| 18 |
-
{% endif %}
|
| 19 |
-
</div>
|
| 20 |
-
</div>
|
| 21 |
-
{% endfor %}
|
| 22 |
-
<div class="d-flex mb-3">
|
| 23 |
-
<a href="{{ url_for('control.download_proxy', filetype='proxy') }}" class="btn btn-outline-primary">
|
| 24 |
-
<i class="bi bi-download"></i> Download remote control script
|
| 25 |
-
</a>
|
| 26 |
-
</div>
|
| 27 |
-
{% if not deck %}
|
| 28 |
-
<div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
|
| 29 |
-
<div class="bg-white rounded shadow-sm position-relative">
|
| 30 |
-
<div class="p-4 controller-card" >
|
| 31 |
-
{% if deck %}
|
| 32 |
-
{# todo disconnect for imported deck #}
|
| 33 |
-
{# <h5> <a href="{{ url_for("disconnect") }}" class="stretched-link" style="color: orangered">Disconnect deck</a></h5>#}
|
| 34 |
-
{% else %}
|
| 35 |
-
<h5><a href="{{ url_for('control.new_controller') }}" style="color: orange" class="stretched-link">New connection</a></h5>
|
| 36 |
-
{% endif %}
|
| 37 |
-
</div>
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
-
{% endif %}
|
| 41 |
-
{% else %}
|
| 42 |
-
<div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
|
| 43 |
-
<div class="bg-white rounded shadow-sm position-relative">
|
| 44 |
-
<div class="p-4 controller-card" >
|
| 45 |
-
{% if deck %}
|
| 46 |
-
<h5><a data-bs-toggle="modal" href="#importModal" class="stretched-link"><i class="bi bi-folder-plus"></i> Import deck </a></h5>
|
| 47 |
-
{% else %}
|
| 48 |
-
<h5><a href="{{ url_for('control.new_controller') }}" style="color: orange" class="stretched-link">New connection</a></h5>
|
| 49 |
-
{% endif %}
|
| 50 |
-
</div>
|
| 51 |
-
</div>
|
| 52 |
-
</div>
|
| 53 |
-
{% endif %}
|
| 54 |
-
</div>
|
| 55 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/control/templates/control/controllers_new.html
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | New devices{% endblock %}
|
| 3 |
-
|
| 4 |
-
{% block body %}
|
| 5 |
-
<div class="row">
|
| 6 |
-
<div class="col-xl-4 col-lg-4 col-md-6 mb-4 ">
|
| 7 |
-
<h5>Available Python API</h5>
|
| 8 |
-
<hr>
|
| 9 |
-
{% for instrument in api_variables %}
|
| 10 |
-
<div class="bg-white rounded shadow-sm position-relative">
|
| 11 |
-
<h5 class="p-3 controller-card">
|
| 12 |
-
<a href="{{ url_for('control.new_controller', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a>
|
| 13 |
-
</h5>
|
| 14 |
-
</div>
|
| 15 |
-
{% endfor %}
|
| 16 |
-
<div class="bg-white rounded shadow-sm position-relative">
|
| 17 |
-
<h5 class="p-3 controller-card">
|
| 18 |
-
<a data-bs-toggle="modal" href="#importAPI" class="stretched-link"><i class="bi bi-folder-plus"></i> Import API</a>
|
| 19 |
-
</h5>
|
| 20 |
-
</div>
|
| 21 |
-
</div>
|
| 22 |
-
<div class="col-xl-5 col-lg-5 col-md-6 mb-4 ">
|
| 23 |
-
{% if device %}
|
| 24 |
-
<h5>Connecting</h5><hr>
|
| 25 |
-
<form role="form" method='POST' name="init" action="{{ url_for('control.new_controller', instrument=instrument) }}">
|
| 26 |
-
<div class="form-group">
|
| 27 |
-
<div class="input-group mb-3">
|
| 28 |
-
<span class="input-group-text" >Name this device</span>
|
| 29 |
-
<input class="form-control" type="text" id="device_name" name="device_name" aria-labelledby="nameHelpBlock" placeholder="e.g. {{device.__name__}}_1" >
|
| 30 |
-
<div id="nameHelpBlock" class="form-text">
|
| 31 |
-
Name your instrument, avoid names that are defined on the right
|
| 32 |
-
</div>
|
| 33 |
-
</div>
|
| 34 |
-
{% for arg in device.__init__.__annotations__ %}
|
| 35 |
-
{% if not arg == "return" %}
|
| 36 |
-
<div class="input-group mb-3">
|
| 37 |
-
<span class="input-group-text" >{{arg}}</span>
|
| 38 |
-
<input class="form-control" type="text" id="{{arg}}" name="{{arg}}"
|
| 39 |
-
placeholder="{{device.__init__.__annotations__[arg].__name__}}"
|
| 40 |
-
value="{{args.parameters[arg].default if not args.parameters[arg].default.__name__ == '_empty' else ''}}">
|
| 41 |
-
{% if device.__init__.__annotations__[arg].__module__ is not in ["builtins", "typing"] %}
|
| 42 |
-
<a role="button" href="{{ url_for('control.new_controller', instrument=device.__init__.__annotations__[arg].__name__) }}" class="btn btn-secondary">initialize {{device.__init__.__annotations__[arg].__name__}} first</a>
|
| 43 |
-
{% endif %}
|
| 44 |
-
</div>
|
| 45 |
-
{% endif %}
|
| 46 |
-
{% endfor %}
|
| 47 |
-
<button type="submit" class="btn btn-dark">Connect</button>
|
| 48 |
-
</div>
|
| 49 |
-
</form>
|
| 50 |
-
{% endif %}
|
| 51 |
-
</div>
|
| 52 |
-
<div class="col-xl-3 col-lg-3 col-md-6 mb-4">
|
| 53 |
-
<h5>Defined Instruments</h5><hr>
|
| 54 |
-
{% if defined_variables %}
|
| 55 |
-
<ul class="list-group">
|
| 56 |
-
{% for instrument in defined_variables %}
|
| 57 |
-
<li class="list-group-item">{{instrument}}</li>
|
| 58 |
-
{% endfor %}
|
| 59 |
-
</ul>
|
| 60 |
-
{% endif %}
|
| 61 |
-
</div>
|
| 62 |
-
</div>
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
<div class="modal fade" id="importAPI" tabindex="-1" aria-labelledby="importModal" aria-hidden="true" >
|
| 66 |
-
<div class="modal-dialog">
|
| 67 |
-
<div class="modal-content">
|
| 68 |
-
<div class="modal-header">
|
| 69 |
-
<h1 class="modal-title fs-5" id="importModal">Import API by file path</h1>
|
| 70 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 71 |
-
</div>
|
| 72 |
-
<form method="POST" action="{{ url_for('control.import_api') }}" enctype="multipart/form-data">
|
| 73 |
-
<div class="modal-body">
|
| 74 |
-
<h5>input manually</h5>
|
| 75 |
-
<div class="input-group mb-3">
|
| 76 |
-
<label class="input-group-text" for="filepath">File Path:</label>
|
| 77 |
-
<input type="text" class="form-control" name="filepath" id="filepath">
|
| 78 |
-
</div>
|
| 79 |
-
<div class="modal-footer">
|
| 80 |
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
|
| 81 |
-
<button type="submit" class="btn btn-primary"> Save </button>
|
| 82 |
-
</div>
|
| 83 |
-
</div>
|
| 84 |
-
</form>
|
| 85 |
-
</div>
|
| 86 |
-
</div>
|
| 87 |
-
</div>
|
| 88 |
-
|
| 89 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/database/__init__.py
DELETED
|
File without changes
|
ivoryos/routes/database/database.py
DELETED
|
@@ -1,306 +0,0 @@
|
|
| 1 |
-
from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
|
| 2 |
-
from flask_login import login_required
|
| 3 |
-
|
| 4 |
-
from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
|
| 5 |
-
from ivoryos.utils.utils import get_script_file, post_script_file
|
| 6 |
-
|
| 7 |
-
database = Blueprint('database', __name__, template_folder='templates/database')
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
@database.route("/database/scripts/edit/<script_name>")
|
| 12 |
-
@login_required
|
| 13 |
-
def edit_workflow(script_name:str):
|
| 14 |
-
"""
|
| 15 |
-
.. :quickref: Database; load workflow script to canvas
|
| 16 |
-
|
| 17 |
-
load the selected workflow to the design canvas
|
| 18 |
-
|
| 19 |
-
.. http:get:: /database/scripts/edit/<script_name>
|
| 20 |
-
|
| 21 |
-
:param script_name: script name
|
| 22 |
-
:type script_name: str
|
| 23 |
-
:status 302: redirect to :http:get:`/ivoryos/design/script/`
|
| 24 |
-
"""
|
| 25 |
-
row = Script.query.get(script_name)
|
| 26 |
-
script = Script(**row.as_dict())
|
| 27 |
-
post_script_file(script)
|
| 28 |
-
pseudo_name = session.get("pseudo_deck", "")
|
| 29 |
-
off_line = current_app.config["OFF_LINE"]
|
| 30 |
-
if off_line and pseudo_name and not script.deck == pseudo_name:
|
| 31 |
-
flash(f"Choose the deck with name {script.deck}")
|
| 32 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 33 |
-
return jsonify({
|
| 34 |
-
"script": script.as_dict(),
|
| 35 |
-
"python_script": script.compile(),
|
| 36 |
-
})
|
| 37 |
-
return redirect(url_for('design.experiment_builder'))
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
@database.route("/database/scripts/delete/<script_name>")
|
| 41 |
-
@login_required
|
| 42 |
-
def delete_workflow(script_name: str):
|
| 43 |
-
"""
|
| 44 |
-
.. :quickref: Database; delete workflow
|
| 45 |
-
|
| 46 |
-
delete workflow from database
|
| 47 |
-
|
| 48 |
-
.. http:get:: /database/scripts/delete/<script_name>
|
| 49 |
-
|
| 50 |
-
:param script_name: workflow name
|
| 51 |
-
:type script_name: str
|
| 52 |
-
:status 302: redirect to :http:get:`/ivoryos/database/scripts/`
|
| 53 |
-
|
| 54 |
-
"""
|
| 55 |
-
Script.query.filter(Script.name == script_name).delete()
|
| 56 |
-
db.session.commit()
|
| 57 |
-
return redirect(url_for('database.load_from_database'))
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
@database.route("/database/scripts/save")
|
| 61 |
-
@login_required
|
| 62 |
-
def publish():
|
| 63 |
-
"""
|
| 64 |
-
.. :quickref: Database; save workflow to database
|
| 65 |
-
|
| 66 |
-
save workflow to database
|
| 67 |
-
|
| 68 |
-
.. http:get:: /database/scripts/save
|
| 69 |
-
|
| 70 |
-
:status 302: redirect to :http:get:`/ivoryos/experiment/build/`
|
| 71 |
-
"""
|
| 72 |
-
script = get_script_file()
|
| 73 |
-
if not script.name or not script.deck:
|
| 74 |
-
flash("Deck cannot be empty, try to re-submit deck configuration on the left panel")
|
| 75 |
-
row = Script.query.get(script.name)
|
| 76 |
-
if row and row.status == "finalized":
|
| 77 |
-
flash("This is a protected script, use save as to rename.")
|
| 78 |
-
elif row and not session['user'] == row.author:
|
| 79 |
-
flash("You are not the author, use save as to rename.")
|
| 80 |
-
else:
|
| 81 |
-
db.session.merge(script)
|
| 82 |
-
db.session.commit()
|
| 83 |
-
flash("Saved!")
|
| 84 |
-
return redirect(url_for('design.experiment_builder'))
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
@database.route("/database/scripts/finalize")
|
| 88 |
-
@login_required
|
| 89 |
-
def finalize():
|
| 90 |
-
"""
|
| 91 |
-
.. :quickref: Database; finalize the workflow
|
| 92 |
-
|
| 93 |
-
[protected workflow] prevent saving edited workflow to the same workflow name
|
| 94 |
-
|
| 95 |
-
.. http:get:: /finalize
|
| 96 |
-
|
| 97 |
-
:status 302: redirect to :http:get:`/ivoryos/experiment/build/`
|
| 98 |
-
|
| 99 |
-
"""
|
| 100 |
-
script = get_script_file()
|
| 101 |
-
script.finalize()
|
| 102 |
-
if script.name:
|
| 103 |
-
db.session.merge(script)
|
| 104 |
-
db.session.commit()
|
| 105 |
-
post_script_file(script)
|
| 106 |
-
return redirect(url_for('design.experiment_builder'))
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
@database.route("/database/scripts/", strict_slashes=False)
|
| 110 |
-
@database.route("/database/scripts/<deck_name>")
|
| 111 |
-
@login_required
|
| 112 |
-
def load_from_database(deck_name=None):
|
| 113 |
-
"""
|
| 114 |
-
.. :quickref: Database; database page
|
| 115 |
-
|
| 116 |
-
backend control through http requests
|
| 117 |
-
|
| 118 |
-
.. http:get:: /database/scripts/<deck_name>
|
| 119 |
-
|
| 120 |
-
:param deck_name: filter for deck name
|
| 121 |
-
:type deck_name: str
|
| 122 |
-
|
| 123 |
-
"""
|
| 124 |
-
session.pop('edit_action', None) # reset cache
|
| 125 |
-
query = Script.query
|
| 126 |
-
search_term = request.args.get("keyword", None)
|
| 127 |
-
if search_term:
|
| 128 |
-
query = query.filter(Script.name.like(f'%{search_term}%'))
|
| 129 |
-
if deck_name is None:
|
| 130 |
-
temp = Script.query.with_entities(Script.deck).distinct().all()
|
| 131 |
-
deck_list = [i[0] for i in temp]
|
| 132 |
-
else:
|
| 133 |
-
query = query.filter(Script.deck == deck_name)
|
| 134 |
-
deck_list = ["ALL"]
|
| 135 |
-
page = request.args.get('page', default=1, type=int)
|
| 136 |
-
per_page = 10
|
| 137 |
-
|
| 138 |
-
scripts = query.paginate(page=page, per_page=per_page, error_out=False)
|
| 139 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 140 |
-
scripts = query.all()
|
| 141 |
-
script_names = [script.name for script in scripts]
|
| 142 |
-
return jsonify({
|
| 143 |
-
"workflows": script_names,
|
| 144 |
-
})
|
| 145 |
-
else:
|
| 146 |
-
# return HTML
|
| 147 |
-
return render_template("scripts_database.html", scripts=scripts, deck_list=deck_list, deck_name=deck_name)
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
@database.route("/database/scripts/rename", methods=['POST'])
|
| 151 |
-
@login_required
|
| 152 |
-
def edit_run_name():
|
| 153 |
-
"""
|
| 154 |
-
.. :quickref: Database; edit workflow name
|
| 155 |
-
|
| 156 |
-
edit the name of the current workflow, won't save to the database
|
| 157 |
-
|
| 158 |
-
.. http:post:: database/scripts/rename
|
| 159 |
-
|
| 160 |
-
: form run_name: new workflow name
|
| 161 |
-
:status 302: redirect to :http:get:`/ivoryos/experiment/build/`
|
| 162 |
-
|
| 163 |
-
"""
|
| 164 |
-
if request.method == "POST":
|
| 165 |
-
run_name = request.form.get("run_name")
|
| 166 |
-
exist_script = Script.query.get(run_name)
|
| 167 |
-
if not exist_script:
|
| 168 |
-
script = get_script_file()
|
| 169 |
-
script.save_as(run_name)
|
| 170 |
-
post_script_file(script)
|
| 171 |
-
else:
|
| 172 |
-
flash("Script name is already exist in database")
|
| 173 |
-
return redirect(url_for("design.experiment_builder"))
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
@database.route("/database/scripts/save_as", methods=['POST'])
|
| 177 |
-
@login_required
|
| 178 |
-
def save_as():
|
| 179 |
-
"""
|
| 180 |
-
.. :quickref: Database; save the run name as
|
| 181 |
-
|
| 182 |
-
save the current workflow script as
|
| 183 |
-
|
| 184 |
-
.. http:post:: /database/scripts/save_as
|
| 185 |
-
|
| 186 |
-
: form run_name: new workflow name
|
| 187 |
-
:status 302: redirect to :http:get:`/ivoryos/experiment/build/`
|
| 188 |
-
|
| 189 |
-
"""
|
| 190 |
-
if request.method == "POST":
|
| 191 |
-
run_name = request.form.get("run_name")
|
| 192 |
-
register_workflow = request.form.get("register_workflow")
|
| 193 |
-
exist_script = Script.query.get(run_name)
|
| 194 |
-
if not exist_script:
|
| 195 |
-
script = get_script_file()
|
| 196 |
-
script.save_as(run_name)
|
| 197 |
-
script.registered = register_workflow == "on"
|
| 198 |
-
script.author = session.get('user')
|
| 199 |
-
post_script_file(script)
|
| 200 |
-
publish()
|
| 201 |
-
else:
|
| 202 |
-
flash("Script name is already exist in database")
|
| 203 |
-
return redirect(url_for("design.experiment_builder"))
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# -----------------------------------------------------------
|
| 207 |
-
# ------------------ Workflow logs -----------------------
|
| 208 |
-
# -----------------------------------------------------------
|
| 209 |
-
@database.route('/database/workflows/')
|
| 210 |
-
def list_workflows():
|
| 211 |
-
"""
|
| 212 |
-
.. :quickref: Database; list all workflow logs
|
| 213 |
-
|
| 214 |
-
list all workflow logs
|
| 215 |
-
|
| 216 |
-
.. http:get:: /database/workflows/
|
| 217 |
-
|
| 218 |
-
"""
|
| 219 |
-
query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
|
| 220 |
-
search_term = request.args.get("keyword", None)
|
| 221 |
-
if search_term:
|
| 222 |
-
query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
|
| 223 |
-
page = request.args.get('page', default=1, type=int)
|
| 224 |
-
per_page = 10
|
| 225 |
-
|
| 226 |
-
workflows = query.paginate(page=page, per_page=per_page, error_out=False)
|
| 227 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 228 |
-
workflows = query.all()
|
| 229 |
-
workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
|
| 230 |
-
return jsonify({
|
| 231 |
-
"workflow_data": workflow_data,
|
| 232 |
-
})
|
| 233 |
-
else:
|
| 234 |
-
return render_template('workflow_database.html', workflows=workflows)
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
@database.route("/database/workflows/<int:workflow_id>")
|
| 238 |
-
def get_workflow_steps(workflow_id:int):
|
| 239 |
-
"""
|
| 240 |
-
.. :quickref: Database; list all workflow logs
|
| 241 |
-
|
| 242 |
-
list all workflow logs
|
| 243 |
-
|
| 244 |
-
.. http:get:: /database/workflows/<int:workflow_id>
|
| 245 |
-
|
| 246 |
-
"""
|
| 247 |
-
workflow = db.session.get(WorkflowRun, workflow_id)
|
| 248 |
-
steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
|
| 249 |
-
|
| 250 |
-
# Use full objects for template rendering
|
| 251 |
-
grouped = {
|
| 252 |
-
"prep": [],
|
| 253 |
-
"script": {},
|
| 254 |
-
"cleanup": [],
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
# Use dicts for JSON response
|
| 258 |
-
grouped_json = {
|
| 259 |
-
"prep": [],
|
| 260 |
-
"script": {},
|
| 261 |
-
"cleanup": [],
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
for step in steps:
|
| 265 |
-
step_dict = step.as_dict()
|
| 266 |
-
|
| 267 |
-
if step.phase == "prep":
|
| 268 |
-
grouped["prep"].append(step)
|
| 269 |
-
grouped_json["prep"].append(step_dict)
|
| 270 |
-
|
| 271 |
-
elif step.phase == "script":
|
| 272 |
-
grouped["script"].setdefault(step.repeat_index, []).append(step)
|
| 273 |
-
grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
|
| 274 |
-
|
| 275 |
-
elif step.phase == "cleanup" or step.method_name == "stop":
|
| 276 |
-
grouped["cleanup"].append(step)
|
| 277 |
-
grouped_json["cleanup"].append(step_dict)
|
| 278 |
-
|
| 279 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 280 |
-
return jsonify({
|
| 281 |
-
"workflow_info": workflow.as_dict(),
|
| 282 |
-
"steps": grouped_json,
|
| 283 |
-
})
|
| 284 |
-
else:
|
| 285 |
-
return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
@database.route("/database/workflows/delete/<int:workflow_id>")
|
| 289 |
-
@login_required
|
| 290 |
-
def delete_workflow_data(workflow_id: int):
|
| 291 |
-
"""
|
| 292 |
-
.. :quickref: Database; delete experiment data from database
|
| 293 |
-
|
| 294 |
-
delete workflow data from database
|
| 295 |
-
|
| 296 |
-
.. http:get:: /database/workflows/delete/<int:workflow_id>
|
| 297 |
-
|
| 298 |
-
:param workflow_id: workflow id
|
| 299 |
-
:type workflow_id: int
|
| 300 |
-
:status 302: redirect to :http:get:`/ivoryos/database/workflows/`
|
| 301 |
-
|
| 302 |
-
"""
|
| 303 |
-
run = WorkflowRun.query.get(workflow_id)
|
| 304 |
-
db.session.delete(run)
|
| 305 |
-
db.session.commit()
|
| 306 |
-
return redirect(url_for('database.list_workflows'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/database/templates/database/scripts_database.html
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
|
| 3 |
-
{% block title %}IvoryOS | Design Database{% endblock %}
|
| 4 |
-
{% block body %}
|
| 5 |
-
<div class="database-filter">
|
| 6 |
-
{% for deck_name in deck_list %}
|
| 7 |
-
{% if deck_name == "ALL" %}<a class="btn btn-secondary" href="{{url_for('database.load_from_database')}}">Back</a>
|
| 8 |
-
{% else %}<a class="btn btn-secondary" href="{{url_for('database.load_from_database',deck_name=deck_name)}}">{{deck_name}}</a>
|
| 9 |
-
{% endif %}
|
| 10 |
-
{% endfor %}
|
| 11 |
-
|
| 12 |
-
<form id="search" style="display: inline-block;float: right;" action="{{url_for('database.load_from_database',deck_name=deck_name)}}" method="GET">
|
| 13 |
-
<div class="input-group">
|
| 14 |
-
<div class="form-outline">
|
| 15 |
-
<input type="search" name="keyword" id="keyword" class="form-control" placeholder="Search workflows...">
|
| 16 |
-
</div>
|
| 17 |
-
<button type="submit" class="btn btn-primary">
|
| 18 |
-
<i class="bi bi-search"></i>
|
| 19 |
-
</button>
|
| 20 |
-
</div>
|
| 21 |
-
</form>
|
| 22 |
-
</div>
|
| 23 |
-
|
| 24 |
-
<table class="table table-hover" id="workflowLibrary">
|
| 25 |
-
<thead>
|
| 26 |
-
<tr>
|
| 27 |
-
<th scope="col">Workflow name</th>
|
| 28 |
-
<th scope="col">Deck </th>
|
| 29 |
-
<th scope="col">Editing</th>
|
| 30 |
-
<th scope="col">Time created</th>
|
| 31 |
-
<th scope="col">Last modified</th>
|
| 32 |
-
<th scope="col">Author</th>
|
| 33 |
-
{# <th scope="col">Registered</th>#}
|
| 34 |
-
<th scope="col"></th>
|
| 35 |
-
</tr>
|
| 36 |
-
</thead>
|
| 37 |
-
<tbody>
|
| 38 |
-
{% for script in scripts %}
|
| 39 |
-
<tr>
|
| 40 |
-
<td><a href="{{ url_for('database.edit_workflow', script_name=script.name) }}">{{ script.name }}</a></td>
|
| 41 |
-
<td>{{ script.deck }}</td>
|
| 42 |
-
<td>{{ script.status }}</td>
|
| 43 |
-
<td>{{ script.time_created }}</td>
|
| 44 |
-
<td>{{ script.last_modified }}</td>
|
| 45 |
-
<td>{{ script.author }}</td>
|
| 46 |
-
{# <td>{{ workflow.registered }}</td>#}
|
| 47 |
-
<td>
|
| 48 |
-
{#not workflow.status == "finalized" or#}
|
| 49 |
-
{% if session['user'] == 'admin' or session['user'] == script.author %}
|
| 50 |
-
<a href="{{ url_for('database.delete_workflow', script_name=script.name) }}">delete</a>
|
| 51 |
-
{% else %}
|
| 52 |
-
<a class="disabled-link">delete</a>
|
| 53 |
-
{% endif %}
|
| 54 |
-
<td>
|
| 55 |
-
</tr>
|
| 56 |
-
{% endfor %}
|
| 57 |
-
</tbody>
|
| 58 |
-
</table>
|
| 59 |
-
|
| 60 |
-
{# paging#}
|
| 61 |
-
<div class="pagination justify-content-center">
|
| 62 |
-
<div class="page-item {{ 'disabled' if not scripts.has_prev else '' }}">
|
| 63 |
-
<a class="page-link" href="{{ url_for('database.load_from_database', page=scripts.prev_num) }}">Previous</a>
|
| 64 |
-
</div>
|
| 65 |
-
|
| 66 |
-
{% for num in scripts.iter_pages() %}
|
| 67 |
-
{% if num %}
|
| 68 |
-
<div class="page-item {{ 'active' if num == scripts.page else '' }}">
|
| 69 |
-
<a class="page-link" href="{{ url_for('database.load_from_database', page=num) }}">{{ num }}</a>
|
| 70 |
-
</div>
|
| 71 |
-
{% else %}
|
| 72 |
-
<div class="page-item disabled">
|
| 73 |
-
<span class="page-link">…</span>
|
| 74 |
-
</div>
|
| 75 |
-
{% endif %}
|
| 76 |
-
{% endfor %}
|
| 77 |
-
|
| 78 |
-
<div class="page-item {{ 'disabled' if not scripts.has_next else '' }}">
|
| 79 |
-
<a class="page-link" href="{{ url_for('database.load_from_database', page=scripts.next_num) }}">Next</a>
|
| 80 |
-
</div>
|
| 81 |
-
</div>
|
| 82 |
-
|
| 83 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/database/templates/database/step_card.html
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
<div class="card mb-2 {{ 'border-danger text-danger bg-light' if step.run_error else 'border-secondary' }}">
|
| 2 |
-
<div class="card-body p-2">
|
| 3 |
-
<strong>{{ step.method_name }}</strong>
|
| 4 |
-
<small>Start: {{ step.start_time }}</small>
|
| 5 |
-
<small>End: {{ step.end_time }}</small>
|
| 6 |
-
</div>
|
| 7 |
-
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/database/templates/database/workflow_database.html
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
|
| 3 |
-
{% block title %}IvoryOS | Design Database{% endblock %}
|
| 4 |
-
{% block body %}
|
| 5 |
-
<div class="div">
|
| 6 |
-
<form id="search" style="display: inline-block;float: right;" action="{{url_for('database.list_workflows',deck_name=deck_name)}}" method="GET">
|
| 7 |
-
<div class="input-group">
|
| 8 |
-
<div class="form-outline">
|
| 9 |
-
<input type="search" name="keyword" id="keyword" class="form-control" placeholder="Search workflows...">
|
| 10 |
-
</div>
|
| 11 |
-
<button type="submit" class="btn btn-primary">
|
| 12 |
-
<i class="bi bi-search"></i>
|
| 13 |
-
</button>
|
| 14 |
-
</div>
|
| 15 |
-
</form>
|
| 16 |
-
</div>
|
| 17 |
-
|
| 18 |
-
<table class="table table-hover" id="workflowResultLibrary">
|
| 19 |
-
<thead>
|
| 20 |
-
<tr>
|
| 21 |
-
<th scope="col">Workflow name</th>
|
| 22 |
-
<th scope="col">Workflow ID</th>
|
| 23 |
-
<th scope="col">Start time</th>
|
| 24 |
-
<th scope="col">End time</th>
|
| 25 |
-
<th scope="col">Data</th>
|
| 26 |
-
</tr>
|
| 27 |
-
</thead>
|
| 28 |
-
<tbody>
|
| 29 |
-
{% for workflow in workflows %}
|
| 30 |
-
<tr>
|
| 31 |
-
<td><a href="{{ url_for('database.get_workflow_steps', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
|
| 32 |
-
<td>{{ workflow.id }}</td>
|
| 33 |
-
<td>{{ workflow.start_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.start_time else '' }}</td>
|
| 34 |
-
<td>{{ workflow.end_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.end_time else '' }}</td>
|
| 35 |
-
|
| 36 |
-
<td>
|
| 37 |
-
{% if workflow.data_path %}
|
| 38 |
-
<a href="{{ url_for('design.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
|
| 39 |
-
{% endif %}
|
| 40 |
-
</td>
|
| 41 |
-
<td>
|
| 42 |
-
{% if session['user'] == 'admin' or session['user'] == workflow.author %}
|
| 43 |
-
<a href="{{ url_for('database.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>
|
| 44 |
-
{% else %}
|
| 45 |
-
<a class="disabled-link">delete</a>
|
| 46 |
-
{% endif %}
|
| 47 |
-
</td>
|
| 48 |
-
</tr>
|
| 49 |
-
{% endfor %}
|
| 50 |
-
</tbody>
|
| 51 |
-
</table>
|
| 52 |
-
|
| 53 |
-
{# paging#}
|
| 54 |
-
<div class="pagination justify-content-center">
|
| 55 |
-
<div class="page-item {{ 'disabled' if not workflows.has_prev else '' }}">
|
| 56 |
-
<a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.prev_num) }}">Previous</a>
|
| 57 |
-
</div>
|
| 58 |
-
|
| 59 |
-
{% for num in workflows.iter_pages() %}
|
| 60 |
-
{% if num %}
|
| 61 |
-
<div class="page-item {{ 'active' if num == workflows.page else '' }}">
|
| 62 |
-
<a class="page-link" href="{{ url_for('database.list_workflows', page=num) }}">{{ num }}</a>
|
| 63 |
-
</div>
|
| 64 |
-
{% else %}
|
| 65 |
-
<div class="page-item disabled">
|
| 66 |
-
<span class="page-link">…</span>
|
| 67 |
-
</div>
|
| 68 |
-
{% endif %}
|
| 69 |
-
{% endfor %}
|
| 70 |
-
|
| 71 |
-
<div class="page-item {{ 'disabled' if not workflows.has_next else '' }}">
|
| 72 |
-
<a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.next_num) }}">Next</a>
|
| 73 |
-
</div>
|
| 74 |
-
</div>
|
| 75 |
-
|
| 76 |
-
<div id="steps-container"></div>
|
| 77 |
-
|
| 78 |
-
<script>
|
| 79 |
-
function showSteps(workflowId) {
|
| 80 |
-
fetch(`/workflow_steps/${workflowId}`)
|
| 81 |
-
.then(response => response.json())
|
| 82 |
-
.then(data => {
|
| 83 |
-
const container = document.getElementById('steps-container');
|
| 84 |
-
container.innerHTML = ''; // Clear previous content
|
| 85 |
-
const stepsList = document.createElement('ul');
|
| 86 |
-
|
| 87 |
-
data.steps.forEach(step => {
|
| 88 |
-
const li = document.createElement('li');
|
| 89 |
-
li.innerHTML = `
|
| 90 |
-
<strong>Step: </strong> ${step.method_name} <br>
|
| 91 |
-
<strong>Start Time:</strong> ${step.start_time} <br>
|
| 92 |
-
<strong>End Time:</strong> ${step.end_time} <br>
|
| 93 |
-
<strong>Human Intervention:</strong> ${step.run_error ? 'Yes' : 'No'}
|
| 94 |
-
`;
|
| 95 |
-
stepsList.appendChild(li);
|
| 96 |
-
});
|
| 97 |
-
|
| 98 |
-
container.appendChild(stepsList);
|
| 99 |
-
});
|
| 100 |
-
}
|
| 101 |
-
</script>
|
| 102 |
-
|
| 103 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/database/templates/database/workflow_view.html
DELETED
|
@@ -1,130 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
|
| 3 |
-
{% block title %}IvoryOS | Experiment Results{% endblock %}
|
| 4 |
-
|
| 5 |
-
{% block body %}
|
| 6 |
-
<style>
|
| 7 |
-
.vis-time-axis .vis-text.vis-minor,
|
| 8 |
-
.vis-time-axis .vis-text.vis-major {
|
| 9 |
-
color: #666;
|
| 10 |
-
}
|
| 11 |
-
.vis-item.stop {
|
| 12 |
-
background-color: red;
|
| 13 |
-
color: white;
|
| 14 |
-
border: none;
|
| 15 |
-
font-weight: bold;
|
| 16 |
-
}
|
| 17 |
-
</style>
|
| 18 |
-
|
| 19 |
-
<div id="timeline"></div>
|
| 20 |
-
|
| 21 |
-
<script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
| 22 |
-
<link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
|
| 23 |
-
|
| 24 |
-
<h1>Experiment Step View</h1>
|
| 25 |
-
|
| 26 |
-
<div id="visualization"></div>
|
| 27 |
-
|
| 28 |
-
<script type="text/javascript">
|
| 29 |
-
var container = document.getElementById('visualization');
|
| 30 |
-
|
| 31 |
-
const items = [
|
| 32 |
-
{% if grouped.prep %}
|
| 33 |
-
{
|
| 34 |
-
id: 'prep',
|
| 35 |
-
content: 'Prep Phase',
|
| 36 |
-
start: '{{ grouped.prep[0].start_time }}',
|
| 37 |
-
end: '{{ grouped.prep[-1].end_time }}',
|
| 38 |
-
className: 'prep',
|
| 39 |
-
group: 'prep'
|
| 40 |
-
},
|
| 41 |
-
{% endif %}
|
| 42 |
-
|
| 43 |
-
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
| 44 |
-
{
|
| 45 |
-
id: 'iter{{ repeat_index }}',
|
| 46 |
-
content: 'Iteration {{ repeat_index }}',
|
| 47 |
-
start: '{{ step_list[0].start_time }}',
|
| 48 |
-
end: '{{ step_list[-1].end_time }}',
|
| 49 |
-
className: 'script',
|
| 50 |
-
group: 'iter{{ repeat_index }}'
|
| 51 |
-
},
|
| 52 |
-
{% for step in step_list %}
|
| 53 |
-
{% if step.method_name == "stop" %}
|
| 54 |
-
{
|
| 55 |
-
id: 'stop-{{ step.id }}',
|
| 56 |
-
content: '🛑 Stop',
|
| 57 |
-
start: '{{ step.start_time }}',
|
| 58 |
-
type: 'point',
|
| 59 |
-
className: 'stop',
|
| 60 |
-
group: 'iter{{ repeat_index }}'
|
| 61 |
-
},
|
| 62 |
-
{% endif %}
|
| 63 |
-
{% endfor %}
|
| 64 |
-
{% endfor %}
|
| 65 |
-
|
| 66 |
-
{% if grouped.cleanup %}
|
| 67 |
-
{
|
| 68 |
-
id: 'cleanup',
|
| 69 |
-
content: 'Cleanup Phase',
|
| 70 |
-
start: '{{ grouped.cleanup[0].start_time }}',
|
| 71 |
-
end: '{{ grouped.cleanup[-1].end_time }}',
|
| 72 |
-
className: 'cleanup',
|
| 73 |
-
group: 'cleanup'
|
| 74 |
-
|
| 75 |
-
},
|
| 76 |
-
{% endif %}
|
| 77 |
-
];
|
| 78 |
-
|
| 79 |
-
const groups = [
|
| 80 |
-
{% if grouped.prep %}{ id: 'prep', content: 'Prep' },{% endif %}
|
| 81 |
-
{% for repeat_index in grouped.script.keys()|sort %}{ id: 'iter{{ repeat_index }}', content: 'Iteration {{ repeat_index }}' },{% endfor %}
|
| 82 |
-
{% if grouped.cleanup %}{ id: 'cleanup', content: 'Cleanup' },{% endif %}
|
| 83 |
-
];
|
| 84 |
-
|
| 85 |
-
var options = {
|
| 86 |
-
clickToUse: true,
|
| 87 |
-
stack: false, // important to keep point within group row
|
| 88 |
-
horizontalScroll: true,
|
| 89 |
-
zoomKey: 'ctrlKey'
|
| 90 |
-
};
|
| 91 |
-
|
| 92 |
-
// Initialize your timeline with the sorted groups
|
| 93 |
-
const timeline = new vis.Timeline(container, items, groups, options);
|
| 94 |
-
|
| 95 |
-
timeline.on('select', function (props) {
|
| 96 |
-
const id = props.items[0];
|
| 97 |
-
if (id && id.startsWith('iter')) {
|
| 98 |
-
const card = document.getElementById('card-' + id);
|
| 99 |
-
if (card) {
|
| 100 |
-
const yOffset = -80;
|
| 101 |
-
const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
| 102 |
-
window.scrollTo({ top: y, behavior: 'smooth' });
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
});
|
| 106 |
-
</script>
|
| 107 |
-
|
| 108 |
-
<h2>Workflow: {{ workflow.name }}</h2>
|
| 109 |
-
|
| 110 |
-
{% if grouped.prep %}
|
| 111 |
-
<h4 class="mt-4">Prep Phase</h4>
|
| 112 |
-
{% for step in grouped.prep %}
|
| 113 |
-
{% include "step_card.html" %}
|
| 114 |
-
{% endfor %}
|
| 115 |
-
{% endif %}
|
| 116 |
-
|
| 117 |
-
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
| 118 |
-
<h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
|
| 119 |
-
{% for step in step_list %}
|
| 120 |
-
{% include "step_card.html" %}
|
| 121 |
-
{% endfor %}
|
| 122 |
-
{% endfor %}
|
| 123 |
-
|
| 124 |
-
{% if grouped.cleanup %}
|
| 125 |
-
<h4 class="mt-4">Cleanup Phase</h4>
|
| 126 |
-
{% for step in grouped.cleanup %}
|
| 127 |
-
{% include "step_card.html" %}
|
| 128 |
-
{% endfor %}
|
| 129 |
-
{% endif %}
|
| 130 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/design/__init__.py
DELETED
|
File without changes
|
ivoryos/routes/design/design.py
DELETED
|
@@ -1,792 +0,0 @@
|
|
| 1 |
-
import csv
|
| 2 |
-
import json
|
| 3 |
-
import os
|
| 4 |
-
import pickle
|
| 5 |
-
import sys
|
| 6 |
-
import time
|
| 7 |
-
import eventlet
|
| 8 |
-
eventlet.monkey_patch()
|
| 9 |
-
|
| 10 |
-
from flask import Blueprint, redirect, url_for, flash, jsonify, send_file, request, render_template, session, \
|
| 11 |
-
current_app, g
|
| 12 |
-
from flask_login import login_required
|
| 13 |
-
from flask_socketio import SocketIO
|
| 14 |
-
from werkzeug.utils import secure_filename
|
| 15 |
-
|
| 16 |
-
from ivoryos.utils import utils
|
| 17 |
-
from ivoryos.utils.global_config import GlobalConfig
|
| 18 |
-
from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
|
| 19 |
-
create_form_from_action, create_all_builtin_forms
|
| 20 |
-
from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
|
| 21 |
-
from ivoryos.utils.script_runner import ScriptRunner
|
| 22 |
-
# from ivoryos.utils.utils import load_workflows
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
socketio = SocketIO(
|
| 26 |
-
async_mode="eventlet",
|
| 27 |
-
cors_allowed_origins="*"
|
| 28 |
-
)
|
| 29 |
-
design = Blueprint('design', __name__, template_folder='templates/design')
|
| 30 |
-
|
| 31 |
-
global_config = GlobalConfig()
|
| 32 |
-
runner = ScriptRunner()
|
| 33 |
-
|
| 34 |
-
def abort_pending():
|
| 35 |
-
runner.abort_pending()
|
| 36 |
-
socketio.emit('log', {'message': "aborted pending iterations"})
|
| 37 |
-
|
| 38 |
-
def abort_current():
|
| 39 |
-
runner.stop_execution()
|
| 40 |
-
socketio.emit('log', {'message': "stopped next task"})
|
| 41 |
-
|
| 42 |
-
def pause():
|
| 43 |
-
runner.retry = False
|
| 44 |
-
msg = runner.toggle_pause()
|
| 45 |
-
socketio.emit('log', {'message': msg})
|
| 46 |
-
return msg
|
| 47 |
-
|
| 48 |
-
def retry():
|
| 49 |
-
runner.retry = True
|
| 50 |
-
msg = runner.toggle_pause()
|
| 51 |
-
socketio.emit('log', {'message': msg})
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# ---- Socket.IO Event Handlers ----
|
| 55 |
-
|
| 56 |
-
@socketio.on('abort_pending')
|
| 57 |
-
def handle_abort_pending():
|
| 58 |
-
abort_pending()
|
| 59 |
-
|
| 60 |
-
@socketio.on('abort_current')
|
| 61 |
-
def handle_abort_current():
|
| 62 |
-
abort_current()
|
| 63 |
-
|
| 64 |
-
@socketio.on('pause')
|
| 65 |
-
def handle_pause():
|
| 66 |
-
pause()
|
| 67 |
-
|
| 68 |
-
@socketio.on('retry')
|
| 69 |
-
def handle_retry():
|
| 70 |
-
retry()
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
@socketio.on('connect')
|
| 74 |
-
def handle_abort_action():
|
| 75 |
-
# Fetch log messages from local file
|
| 76 |
-
filename = os.path.join(current_app.config["OUTPUT_FOLDER"], current_app.config["LOGGERS_PATH"])
|
| 77 |
-
with open(filename, 'r') as log_file:
|
| 78 |
-
log_history = log_file.readlines()
|
| 79 |
-
for message in log_history[-10:]:
|
| 80 |
-
socketio.emit('log', {'message': message})
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
@design.route("/design/script/", methods=['GET', 'POST'])
|
| 84 |
-
@design.route("/design/script/<instrument>/", methods=['GET', 'POST'])
|
| 85 |
-
@login_required
|
| 86 |
-
def experiment_builder(instrument=None):
|
| 87 |
-
"""
|
| 88 |
-
.. :quickref: Workflow Design; Build experiment workflow
|
| 89 |
-
|
| 90 |
-
**Experiment Builder**
|
| 91 |
-
|
| 92 |
-
This route allows users to build and edit experiment workflows. Users can interact with available instruments,
|
| 93 |
-
define variables, and manage experiment scripts.
|
| 94 |
-
|
| 95 |
-
.. http:get:: /design/script
|
| 96 |
-
|
| 97 |
-
Load the experiment builder interface.
|
| 98 |
-
|
| 99 |
-
:param instrument: The specific instrument for which to load functions and forms.
|
| 100 |
-
:type instrument: str
|
| 101 |
-
:status 200: Experiment builder loaded successfully.
|
| 102 |
-
|
| 103 |
-
.. http:post:: /design/script
|
| 104 |
-
|
| 105 |
-
Submit form data to add or modify actions in the experiment script.
|
| 106 |
-
|
| 107 |
-
**Adding action to canvas**
|
| 108 |
-
|
| 109 |
-
:form return: (optional) The name of the function or method to add to the script.
|
| 110 |
-
:form dynamic: depend on the selected instrument and its metadata.
|
| 111 |
-
|
| 112 |
-
:status 200: Action added or modified successfully.
|
| 113 |
-
:status 400: Validation errors in submitted form data.
|
| 114 |
-
:status 302: Toggles autofill or redirects to refresh the page.
|
| 115 |
-
|
| 116 |
-
**Toggle auto parameter name fill**:
|
| 117 |
-
|
| 118 |
-
:status 200: autofill toggled successfully
|
| 119 |
-
|
| 120 |
-
"""
|
| 121 |
-
deck = global_config.deck
|
| 122 |
-
script = utils.get_script_file()
|
| 123 |
-
# load_workflows(script)
|
| 124 |
-
# registered_workflows = global_config.registered_workflows
|
| 125 |
-
|
| 126 |
-
if deck and script.deck is None:
|
| 127 |
-
script.deck = os.path.splitext(os.path.basename(deck.__file__))[
|
| 128 |
-
0] if deck.__name__ == "__main__" else deck.__name__
|
| 129 |
-
# script.sort_actions()
|
| 130 |
-
|
| 131 |
-
pseudo_deck_name = session.get('pseudo_deck', '')
|
| 132 |
-
pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
|
| 133 |
-
off_line = current_app.config["OFF_LINE"]
|
| 134 |
-
enable_llm = current_app.config["ENABLE_LLM"]
|
| 135 |
-
autofill = session.get('autofill')
|
| 136 |
-
|
| 137 |
-
# autofill is not allowed for prep and cleanup
|
| 138 |
-
autofill = autofill if script.editing_type == "script" else False
|
| 139 |
-
forms = None
|
| 140 |
-
pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
|
| 141 |
-
if off_line and pseudo_deck is None:
|
| 142 |
-
flash("Choose available deck below.")
|
| 143 |
-
|
| 144 |
-
deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
|
| 145 |
-
|
| 146 |
-
functions = {}
|
| 147 |
-
if deck:
|
| 148 |
-
deck_variables = list(global_config.deck_snapshot.keys())
|
| 149 |
-
# deck_variables.insert(0, "registered_workflows")
|
| 150 |
-
deck_variables.insert(0, "flow_control")
|
| 151 |
-
|
| 152 |
-
else:
|
| 153 |
-
deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
|
| 154 |
-
deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
|
| 155 |
-
edit_action_info = session.get("edit_action")
|
| 156 |
-
if edit_action_info:
|
| 157 |
-
forms = create_form_from_action(edit_action_info, script=script)
|
| 158 |
-
elif instrument:
|
| 159 |
-
# if instrument in ['if', 'while', 'variable', 'wait', 'repeat']:
|
| 160 |
-
# forms = create_builtin_form(instrument, script=script)
|
| 161 |
-
if instrument == 'flow_control':
|
| 162 |
-
forms = create_all_builtin_forms(script=script)
|
| 163 |
-
# elif instrument == 'registered_workflows':
|
| 164 |
-
# functions = utils._inspect_class(registered_workflows)
|
| 165 |
-
# # forms = create_workflow_forms(script=script)
|
| 166 |
-
# forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
|
| 167 |
-
elif instrument in global_config.defined_variables.keys():
|
| 168 |
-
_object = global_config.defined_variables.get(instrument)
|
| 169 |
-
functions = utils._inspect_class(_object)
|
| 170 |
-
forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
|
| 171 |
-
else:
|
| 172 |
-
if deck:
|
| 173 |
-
functions = global_config.deck_snapshot.get(instrument, {})
|
| 174 |
-
elif pseudo_deck:
|
| 175 |
-
functions = pseudo_deck.get(instrument, {})
|
| 176 |
-
forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
|
| 177 |
-
if request.method == 'POST' and "hidden_name" in request.form:
|
| 178 |
-
# all_kwargs = request.form.copy()
|
| 179 |
-
method_name = request.form.get("hidden_name", None)
|
| 180 |
-
# if method_name is not None:
|
| 181 |
-
form = forms.get(method_name)
|
| 182 |
-
insert_position = request.form.get("drop_target_id", None)
|
| 183 |
-
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
| 184 |
-
if form and form.validate_on_submit():
|
| 185 |
-
function_name = kwargs.pop("hidden_name")
|
| 186 |
-
save_data = kwargs.pop('return', '')
|
| 187 |
-
|
| 188 |
-
primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
|
| 189 |
-
|
| 190 |
-
script.eval_list(kwargs, primitive_arg_types)
|
| 191 |
-
kwargs = script.validate_variables(kwargs)
|
| 192 |
-
action = {"instrument": instrument, "action": function_name,
|
| 193 |
-
"args": kwargs,
|
| 194 |
-
"return": save_data,
|
| 195 |
-
'arg_types': primitive_arg_types}
|
| 196 |
-
script.add_action(action=action, insert_position=insert_position)
|
| 197 |
-
else:
|
| 198 |
-
flash(form.errors)
|
| 199 |
-
|
| 200 |
-
elif request.method == 'POST' and "builtin_name" in request.form:
|
| 201 |
-
function_name = request.form.get("builtin_name")
|
| 202 |
-
form = forms.get(function_name)
|
| 203 |
-
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
| 204 |
-
insert_position = request.form.get("drop_target_id", None)
|
| 205 |
-
|
| 206 |
-
if form.validate_on_submit():
|
| 207 |
-
# print(kwargs)
|
| 208 |
-
logic_type = kwargs.pop('builtin_name')
|
| 209 |
-
if 'variable' in kwargs:
|
| 210 |
-
try:
|
| 211 |
-
script.add_variable(insert_position=insert_position, **kwargs)
|
| 212 |
-
except ValueError:
|
| 213 |
-
flash("Invalid variable type")
|
| 214 |
-
else:
|
| 215 |
-
script.add_logic_action(logic_type=logic_type, insert_position=insert_position, **kwargs)
|
| 216 |
-
else:
|
| 217 |
-
flash(form.errors)
|
| 218 |
-
elif request.method == 'POST' and "workflow_name" in request.form:
|
| 219 |
-
workflow_name = request.form.get("workflow_name")
|
| 220 |
-
form = forms.get(workflow_name)
|
| 221 |
-
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
| 222 |
-
insert_position = request.form.get("drop_target_id", None)
|
| 223 |
-
|
| 224 |
-
if form.validate_on_submit():
|
| 225 |
-
# workflow_name = kwargs.pop('workflow_name')
|
| 226 |
-
save_data = kwargs.pop('return', '')
|
| 227 |
-
|
| 228 |
-
primitive_arg_types = utils.get_arg_type(kwargs, functions[workflow_name])
|
| 229 |
-
|
| 230 |
-
script.eval_list(kwargs, primitive_arg_types)
|
| 231 |
-
kwargs = script.validate_variables(kwargs)
|
| 232 |
-
action = {"instrument": instrument, "action": workflow_name,
|
| 233 |
-
"args": kwargs,
|
| 234 |
-
"return": save_data,
|
| 235 |
-
'arg_types': primitive_arg_types}
|
| 236 |
-
script.add_action(action=action, insert_position=insert_position)
|
| 237 |
-
script.add_workflow(**kwargs, insert_position=insert_position)
|
| 238 |
-
else:
|
| 239 |
-
flash(form.errors)
|
| 240 |
-
|
| 241 |
-
# toggle autofill, autofill doesn't apply to control flow ops
|
| 242 |
-
elif request.method == 'POST' and "autofill" in request.form:
|
| 243 |
-
autofill = not autofill
|
| 244 |
-
session['autofill'] = autofill
|
| 245 |
-
if not instrument == 'flow_control':
|
| 246 |
-
forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
|
| 247 |
-
|
| 248 |
-
utils.post_script_file(script)
|
| 249 |
-
|
| 250 |
-
exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
|
| 251 |
-
session['python_code'] = exec_string
|
| 252 |
-
|
| 253 |
-
design_buttons = create_action_button(script)
|
| 254 |
-
return render_template('experiment_builder.html', off_line=off_line, instrument=instrument, history=deck_list,
|
| 255 |
-
script=script, defined_variables=deck_variables,
|
| 256 |
-
local_variables=global_config.defined_variables,
|
| 257 |
-
forms=forms, buttons=design_buttons, format_name=format_name,
|
| 258 |
-
use_llm=enable_llm)
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
@design.route("/design/generate_code", methods=['POST'])
|
| 262 |
-
@login_required
|
| 263 |
-
def generate_code():
|
| 264 |
-
"""
|
| 265 |
-
.. :quickref: Text to Code; Generate code from user input and update the design canvas.
|
| 266 |
-
|
| 267 |
-
.. http:post:: /design/generate_code
|
| 268 |
-
|
| 269 |
-
:form prompt: user's prompt
|
| 270 |
-
:status 200: and then redirects to :http:get:`/experiment/build`
|
| 271 |
-
:status 400: failed to initialize the AI agent redirects to :http:get:`/design/script`
|
| 272 |
-
|
| 273 |
-
"""
|
| 274 |
-
agent = global_config.agent
|
| 275 |
-
enable_llm = current_app.config["ENABLE_LLM"]
|
| 276 |
-
instrument = request.form.get("instrument")
|
| 277 |
-
|
| 278 |
-
if request.method == 'POST' and "clear" in request.form:
|
| 279 |
-
session['prompt'][instrument] = ''
|
| 280 |
-
if request.method == 'POST' and "gen" in request.form:
|
| 281 |
-
prompt = request.form.get("prompt")
|
| 282 |
-
session['prompt'][instrument] = prompt
|
| 283 |
-
# sdl_module = utils.parse_functions(find_instrument_by_name(f'deck.{instrument}'), doc_string=True)
|
| 284 |
-
sdl_module = global_config.deck_snapshot.get(instrument, {})
|
| 285 |
-
empty_script = Script(author=session.get('user'))
|
| 286 |
-
if enable_llm and agent is None:
|
| 287 |
-
try:
|
| 288 |
-
model = current_app.config["LLM_MODEL"]
|
| 289 |
-
server = current_app.config["LLM_SERVER"]
|
| 290 |
-
module = current_app.config["MODULE"]
|
| 291 |
-
from ivoryos.utils.llm_agent import LlmAgent
|
| 292 |
-
agent = LlmAgent(host=server, model=model, output_path=os.path.dirname(os.path.abspath(module)))
|
| 293 |
-
except Exception as e:
|
| 294 |
-
flash(e.__str__())
|
| 295 |
-
return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True)), 400
|
| 296 |
-
action_list = agent.generate_code(sdl_module, prompt)
|
| 297 |
-
for action in action_list:
|
| 298 |
-
action['instrument'] = instrument
|
| 299 |
-
action['return'] = ''
|
| 300 |
-
if "args" not in action:
|
| 301 |
-
action['args'] = {}
|
| 302 |
-
if "arg_types" not in action:
|
| 303 |
-
action['arg_types'] = {}
|
| 304 |
-
empty_script.add_action(action)
|
| 305 |
-
utils.post_script_file(empty_script)
|
| 306 |
-
return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True))
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
@design.route("/design/campaign", methods=['GET', 'POST'])
|
| 310 |
-
@login_required
|
| 311 |
-
def experiment_run():
|
| 312 |
-
"""
|
| 313 |
-
.. :quickref: Workflow Execution; Execute/iterate the workflow
|
| 314 |
-
|
| 315 |
-
.. http:get:: /design/campaign
|
| 316 |
-
|
| 317 |
-
Compile the workflow and load the experiment execution interface.
|
| 318 |
-
|
| 319 |
-
.. http:post:: /design/campaign
|
| 320 |
-
|
| 321 |
-
Start workflow execution
|
| 322 |
-
|
| 323 |
-
"""
|
| 324 |
-
deck = global_config.deck
|
| 325 |
-
script = utils.get_script_file()
|
| 326 |
-
|
| 327 |
-
# script.sort_actions() # handled in update list
|
| 328 |
-
off_line = current_app.config["OFF_LINE"]
|
| 329 |
-
deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
|
| 330 |
-
# if not off_line and deck is None:
|
| 331 |
-
# # print("loading deck")
|
| 332 |
-
# module = current_app.config.get('MODULE', '')
|
| 333 |
-
# deck = sys.modules[module] if module else None
|
| 334 |
-
# script.deck = os.path.splitext(os.path.basename(deck.__file__))[0]
|
| 335 |
-
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
| 336 |
-
config_preview = []
|
| 337 |
-
config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
|
| 338 |
-
try:
|
| 339 |
-
# todo
|
| 340 |
-
exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
|
| 341 |
-
# exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
| 342 |
-
# print(exec_string)
|
| 343 |
-
except Exception as e:
|
| 344 |
-
flash(e.__str__())
|
| 345 |
-
# handle api request
|
| 346 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 347 |
-
return jsonify({"error": e.__str__()})
|
| 348 |
-
else:
|
| 349 |
-
return redirect(url_for("design.experiment_builder"))
|
| 350 |
-
|
| 351 |
-
config_file = request.args.get("filename")
|
| 352 |
-
config = []
|
| 353 |
-
if config_file:
|
| 354 |
-
session['config_file'] = config_file
|
| 355 |
-
filename = session.get("config_file")
|
| 356 |
-
if filename:
|
| 357 |
-
# config_preview = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
|
| 358 |
-
config = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
|
| 359 |
-
config_preview = config[1:]
|
| 360 |
-
arg_type = config.pop(0) # first entry is types
|
| 361 |
-
try:
|
| 362 |
-
for key, func_str in exec_string.items():
|
| 363 |
-
exec(func_str)
|
| 364 |
-
line_collection = script.convert_to_lines(exec_string)
|
| 365 |
-
|
| 366 |
-
except Exception:
|
| 367 |
-
flash(f"Please check {key} syntax!!")
|
| 368 |
-
return redirect(url_for("design.experiment_builder"))
|
| 369 |
-
# runner.globals_dict.update(globals())
|
| 370 |
-
run_name = script.name if script.name else "untitled"
|
| 371 |
-
|
| 372 |
-
dismiss = session.get("dismiss", None)
|
| 373 |
-
script = utils.get_script_file()
|
| 374 |
-
no_deck_warning = False
|
| 375 |
-
|
| 376 |
-
_, return_list = script.config_return()
|
| 377 |
-
config_list, config_type_list = script.config("script")
|
| 378 |
-
# config = script.config("script")
|
| 379 |
-
data_list = os.listdir(current_app.config['DATA_FOLDER'])
|
| 380 |
-
data_list.remove(".gitkeep") if ".gitkeep" in data_list else data_list
|
| 381 |
-
if deck is None:
|
| 382 |
-
no_deck_warning = True
|
| 383 |
-
flash(f"No deck is found, import {script.deck}")
|
| 384 |
-
elif script.deck:
|
| 385 |
-
is_deck_match = script.deck == deck.__name__ or script.deck == \
|
| 386 |
-
os.path.splitext(os.path.basename(deck.__file__))[0]
|
| 387 |
-
if not is_deck_match:
|
| 388 |
-
flash(f"This script is not compatible with current deck, import {script.deck}")
|
| 389 |
-
if request.method == "POST":
|
| 390 |
-
bo_args = None
|
| 391 |
-
compiled = False
|
| 392 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 393 |
-
payload_json = request.get_json()
|
| 394 |
-
compiled = True
|
| 395 |
-
if "kwargs" in payload_json:
|
| 396 |
-
config = payload_json["kwargs"]
|
| 397 |
-
elif "parameters" in payload_json:
|
| 398 |
-
bo_args = payload_json
|
| 399 |
-
repeat = payload_json.pop("repeat", None)
|
| 400 |
-
else:
|
| 401 |
-
if "bo" in request.form:
|
| 402 |
-
bo_args = request.form.to_dict()
|
| 403 |
-
if "online-config" in request.form:
|
| 404 |
-
config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
|
| 405 |
-
repeat = request.form.get('repeat', None)
|
| 406 |
-
|
| 407 |
-
try:
|
| 408 |
-
datapath = current_app.config["DATA_FOLDER"]
|
| 409 |
-
run_name = script.validate_function_name(run_name)
|
| 410 |
-
runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
|
| 411 |
-
logger=g.logger, socketio=g.socketio, repeat_count=repeat,
|
| 412 |
-
output_path=datapath, compiled=compiled,
|
| 413 |
-
current_app=current_app._get_current_object()
|
| 414 |
-
)
|
| 415 |
-
if utils.check_config_duplicate(config):
|
| 416 |
-
flash(f"WARNING: Duplicate in config entries.")
|
| 417 |
-
except Exception as e:
|
| 418 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 419 |
-
return jsonify({"error": e.__str__()})
|
| 420 |
-
else:
|
| 421 |
-
flash(e)
|
| 422 |
-
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
| 423 |
-
# wait to get a workflow ID
|
| 424 |
-
while not global_config.runner_status:
|
| 425 |
-
time.sleep(1)
|
| 426 |
-
return jsonify({"status": "task started", "task_id": global_config.runner_status.get("id")})
|
| 427 |
-
else:
|
| 428 |
-
return render_template('experiment_run.html', script=script.script_dict, filename=filename,
|
| 429 |
-
dot_py=exec_string, line_collection=line_collection,
|
| 430 |
-
return_list=return_list, config_list=config_list, config_file_list=config_file_list,
|
| 431 |
-
config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
|
| 432 |
-
no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
|
| 433 |
-
history=deck_list, pause_status=runner.pause_status())
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
@design.route("/design/script/toggle/<stype>")
|
| 437 |
-
@login_required
|
| 438 |
-
def toggle_script_type(stype=None):
|
| 439 |
-
"""
|
| 440 |
-
.. :quickref: Workflow Design; toggle the experimental phase for design canvas.
|
| 441 |
-
|
| 442 |
-
.. http:get:: /design/script/toggle/<stype>
|
| 443 |
-
|
| 444 |
-
:status 200: and then redirects to :http:get:`/design/script`
|
| 445 |
-
|
| 446 |
-
"""
|
| 447 |
-
script = utils.get_script_file()
|
| 448 |
-
script.editing_type = stype
|
| 449 |
-
utils.post_script_file(script)
|
| 450 |
-
return redirect(url_for('design.experiment_builder'))
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
@design.route("/updateList", methods=['POST'])
|
| 454 |
-
@login_required
|
| 455 |
-
def update_list():
|
| 456 |
-
order = request.form['order']
|
| 457 |
-
script = utils.get_script_file()
|
| 458 |
-
script.currently_editing_order = order.split(",", len(script.currently_editing_script))
|
| 459 |
-
script.sort_actions()
|
| 460 |
-
exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
| 461 |
-
utils.post_script_file(script)
|
| 462 |
-
session['python_code'] = exec_string
|
| 463 |
-
|
| 464 |
-
return jsonify({'success': True})
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
@design.route("/toggle_show_code", methods=["POST"])
|
| 468 |
-
def toggle_show_code():
|
| 469 |
-
session["show_code"] = not session.get("show_code", False)
|
| 470 |
-
return redirect(request.referrer or url_for("design.experiment_builder"))
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
# --------------------handle all the import/export and download/upload--------------------------
|
| 474 |
-
@design.route("/design/clear")
|
| 475 |
-
@login_required
|
| 476 |
-
def clear():
|
| 477 |
-
"""
|
| 478 |
-
.. :quickref: Workflow Design; clear the design canvas.
|
| 479 |
-
|
| 480 |
-
.. http:get:: /design/clear
|
| 481 |
-
|
| 482 |
-
:form prompt: user's prompt
|
| 483 |
-
:status 200: clear canvas and then redirects to :http:get:`/design/script`
|
| 484 |
-
"""
|
| 485 |
-
deck = global_config.deck
|
| 486 |
-
pseudo_name = session.get("pseudo_deck", "")
|
| 487 |
-
if deck:
|
| 488 |
-
deck_name = os.path.splitext(os.path.basename(deck.__file__))[
|
| 489 |
-
0] if deck.__name__ == "__main__" else deck.__name__
|
| 490 |
-
elif pseudo_name:
|
| 491 |
-
deck_name = pseudo_name
|
| 492 |
-
else:
|
| 493 |
-
deck_name = ''
|
| 494 |
-
script = Script(deck=deck_name, author=session.get('username'))
|
| 495 |
-
utils.post_script_file(script)
|
| 496 |
-
return redirect(url_for("design.experiment_builder"))
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
@design.route("/design/import/pseudo", methods=['POST'])
|
| 500 |
-
@login_required
|
| 501 |
-
def import_pseudo():
|
| 502 |
-
"""
|
| 503 |
-
.. :quickref: Workflow Design; Import pseudo deck from deck history
|
| 504 |
-
|
| 505 |
-
.. http:post:: /design/import/pseudo
|
| 506 |
-
|
| 507 |
-
:form pkl_name: pseudo deck name
|
| 508 |
-
:status 302: load pseudo deck and then redirects to :http:get:`/design/script`
|
| 509 |
-
"""
|
| 510 |
-
pkl_name = request.form.get('pkl_name')
|
| 511 |
-
script = utils.get_script_file()
|
| 512 |
-
session['pseudo_deck'] = pkl_name
|
| 513 |
-
|
| 514 |
-
if script.deck is None or script.isEmpty():
|
| 515 |
-
script.deck = pkl_name.split('.')[0]
|
| 516 |
-
utils.post_script_file(script)
|
| 517 |
-
elif script.deck and not script.deck == pkl_name.split('.')[0]:
|
| 518 |
-
flash(f"Choose the deck with name {script.deck}")
|
| 519 |
-
return redirect(url_for("design.experiment_builder"))
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
@design.route('/design/uploads', methods=['POST'])
|
| 523 |
-
@login_required
|
| 524 |
-
def upload():
|
| 525 |
-
"""
|
| 526 |
-
.. :quickref: Workflow Execution; upload a workflow config file (.CSV)
|
| 527 |
-
|
| 528 |
-
.. http:post:: /design/uploads
|
| 529 |
-
|
| 530 |
-
:form file: workflow CSV config file
|
| 531 |
-
:status 302: save csv file and then redirects to :http:get:`/design/campaign`
|
| 532 |
-
"""
|
| 533 |
-
if request.method == "POST":
|
| 534 |
-
f = request.files['file']
|
| 535 |
-
if 'file' not in request.files:
|
| 536 |
-
flash('No file part')
|
| 537 |
-
if f.filename.split('.')[-1] == "csv":
|
| 538 |
-
filename = secure_filename(f.filename)
|
| 539 |
-
f.save(os.path.join(current_app.config['CSV_FOLDER'], filename))
|
| 540 |
-
session['config_file'] = filename
|
| 541 |
-
return redirect(url_for("design.experiment_run"))
|
| 542 |
-
else:
|
| 543 |
-
flash("Config file is in csv format")
|
| 544 |
-
return redirect(url_for("design.experiment_run"))
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
@design.route('/design/workflow/download/<filename>')
|
| 548 |
-
@login_required
|
| 549 |
-
def download_results(filename):
|
| 550 |
-
"""
|
| 551 |
-
.. :quickref: Workflow Design; download a workflow data file
|
| 552 |
-
|
| 553 |
-
.. http:get:: /design/workflow/download/<filename>
|
| 554 |
-
|
| 555 |
-
"""
|
| 556 |
-
filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
|
| 557 |
-
return send_file(os.path.abspath(filepath), as_attachment=True)
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
@design.route('/design/load_json', methods=['POST'])
|
| 561 |
-
@login_required
|
| 562 |
-
def load_json():
|
| 563 |
-
"""
|
| 564 |
-
.. :quickref: Workflow Design Ext; upload a workflow design file (.JSON)
|
| 565 |
-
|
| 566 |
-
.. http:post:: /load_json
|
| 567 |
-
|
| 568 |
-
:form file: workflow design JSON file
|
| 569 |
-
:status 302: load pseudo deck and then redirects to :http:get:`/design/script`
|
| 570 |
-
"""
|
| 571 |
-
if request.method == "POST":
|
| 572 |
-
f = request.files['file']
|
| 573 |
-
if 'file' not in request.files:
|
| 574 |
-
flash('No file part')
|
| 575 |
-
if f.filename.endswith("json"):
|
| 576 |
-
script_dict = json.load(f)
|
| 577 |
-
utils.post_script_file(script_dict, is_dict=True)
|
| 578 |
-
else:
|
| 579 |
-
flash("Script file need to be JSON file")
|
| 580 |
-
return redirect(url_for("design.experiment_builder"))
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
@design.route('/design/script/download/<filetype>')
|
| 584 |
-
@login_required
|
| 585 |
-
def download(filetype):
|
| 586 |
-
"""
|
| 587 |
-
.. :quickref: Workflow Design Ext; download a workflow design file
|
| 588 |
-
|
| 589 |
-
.. http:get:: /design/script/download/<filetype>
|
| 590 |
-
|
| 591 |
-
"""
|
| 592 |
-
script = utils.get_script_file()
|
| 593 |
-
run_name = script.name if script.name else "untitled"
|
| 594 |
-
if filetype == "configure":
|
| 595 |
-
filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}_config.csv")
|
| 596 |
-
with open(filepath, 'w', newline='') as f:
|
| 597 |
-
writer = csv.writer(f)
|
| 598 |
-
cfg, cfg_types = script.config("script")
|
| 599 |
-
writer.writerow(cfg)
|
| 600 |
-
writer.writerow(list(cfg_types.values()))
|
| 601 |
-
elif filetype == "script":
|
| 602 |
-
script.sort_actions()
|
| 603 |
-
json_object = json.dumps(script.as_dict())
|
| 604 |
-
filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}.json")
|
| 605 |
-
with open(filepath, "w") as outfile:
|
| 606 |
-
outfile.write(json_object)
|
| 607 |
-
elif filetype == "python":
|
| 608 |
-
filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
|
| 609 |
-
else:
|
| 610 |
-
return "Unsupported file type", 400
|
| 611 |
-
return send_file(os.path.abspath(filepath), as_attachment=True)
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
@design.route("/design/step/edit/<uuid>", methods=['GET', 'POST'])
|
| 615 |
-
@login_required
|
| 616 |
-
def edit_action(uuid: str):
|
| 617 |
-
"""
|
| 618 |
-
.. :quickref: Workflow Design; edit parameters of an action step on canvas
|
| 619 |
-
|
| 620 |
-
.. http:get:: /design/step/edit/<uuid>
|
| 621 |
-
|
| 622 |
-
Load parameter form of an action step
|
| 623 |
-
|
| 624 |
-
.. http:post:: /design/step/edit/<uuid>
|
| 625 |
-
|
| 626 |
-
:param uuid: The step's uuid
|
| 627 |
-
:type uuid: str
|
| 628 |
-
|
| 629 |
-
:form dynamic form: workflow step dynamic inputs
|
| 630 |
-
:status 302: save changes and then redirects to :http:get:`/design/script`
|
| 631 |
-
"""
|
| 632 |
-
script = utils.get_script_file()
|
| 633 |
-
action = script.find_by_uuid(uuid)
|
| 634 |
-
session['edit_action'] = action
|
| 635 |
-
|
| 636 |
-
if request.method == "POST" and action is not None:
|
| 637 |
-
forms = create_form_from_action(action, script=script)
|
| 638 |
-
if "back" not in request.form:
|
| 639 |
-
kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
|
| 640 |
-
# print(kwargs)
|
| 641 |
-
if forms and forms.validate_on_submit():
|
| 642 |
-
save_as = kwargs.pop('return', '')
|
| 643 |
-
kwargs = script.validate_variables(kwargs)
|
| 644 |
-
# try:
|
| 645 |
-
script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
|
| 646 |
-
# except Exception as e:
|
| 647 |
-
else:
|
| 648 |
-
flash(forms.errors)
|
| 649 |
-
session.pop('edit_action')
|
| 650 |
-
return redirect(url_for('design.experiment_builder'))
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
@design.route("/design/step/delete/<id>")
|
| 654 |
-
@login_required
|
| 655 |
-
def delete_action(id: int):
|
| 656 |
-
"""
|
| 657 |
-
.. :quickref: Workflow Design; delete an action step on canvas
|
| 658 |
-
|
| 659 |
-
.. http:get:: /design/step/delete/<id>
|
| 660 |
-
|
| 661 |
-
:param id: The step number id
|
| 662 |
-
:type id: int
|
| 663 |
-
|
| 664 |
-
:status 302: save changes and then redirects to :http:get:`/design/script`
|
| 665 |
-
"""
|
| 666 |
-
back = request.referrer
|
| 667 |
-
script = utils.get_script_file()
|
| 668 |
-
script.delete_action(id)
|
| 669 |
-
utils.post_script_file(script)
|
| 670 |
-
return redirect(back)
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
@design.route("/design/step/duplicate/<id>")
|
| 674 |
-
@login_required
|
| 675 |
-
def duplicate_action(id: int):
|
| 676 |
-
"""
|
| 677 |
-
.. :quickref: Workflow Design; duplicate an action step on canvas
|
| 678 |
-
|
| 679 |
-
.. http:get:: /design/step/duplicate/<id>
|
| 680 |
-
|
| 681 |
-
:param id: The step number id
|
| 682 |
-
:type id: int
|
| 683 |
-
|
| 684 |
-
:status 302: save changes and then redirects to :http:get:`/design/script`
|
| 685 |
-
"""
|
| 686 |
-
back = request.referrer
|
| 687 |
-
script = utils.get_script_file()
|
| 688 |
-
script.duplicate_action(id)
|
| 689 |
-
utils.post_script_file(script)
|
| 690 |
-
return redirect(back)
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
# ---- HTTP API Endpoints ----
|
| 694 |
-
|
| 695 |
-
@design.route("/api/runner/status", methods=["GET"])
|
| 696 |
-
def runner_status():
|
| 697 |
-
"""
|
| 698 |
-
.. :quickref: Workflow Design; get the execution status
|
| 699 |
-
|
| 700 |
-
.. http:get:: /api/runner/status
|
| 701 |
-
|
| 702 |
-
:status 200: status
|
| 703 |
-
"""
|
| 704 |
-
runner_busy = global_config.runner_lock.locked()
|
| 705 |
-
status = {"busy": runner_busy}
|
| 706 |
-
task_status = global_config.runner_status
|
| 707 |
-
current_step = {}
|
| 708 |
-
# print(task_status)
|
| 709 |
-
if task_status is not None:
|
| 710 |
-
task_type = task_status["type"]
|
| 711 |
-
task_id = task_status["id"]
|
| 712 |
-
if task_type == "task":
|
| 713 |
-
step = SingleStep.query.get(task_id)
|
| 714 |
-
current_step = step.as_dict()
|
| 715 |
-
if task_type == "workflow":
|
| 716 |
-
workflow = WorkflowRun.query.get(task_id)
|
| 717 |
-
if workflow is not None:
|
| 718 |
-
latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(WorkflowStep.start_time.desc()).first()
|
| 719 |
-
if latest_step is not None:
|
| 720 |
-
current_step = latest_step.as_dict()
|
| 721 |
-
status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
|
| 722 |
-
status["current_task"] = current_step
|
| 723 |
-
return jsonify(status), 200
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
@design.route("/api/runner/abort_pending", methods=["POST"])
|
| 728 |
-
def api_abort_pending():
|
| 729 |
-
"""
|
| 730 |
-
.. :quickref: Workflow Design; abort pending action(s) during execution
|
| 731 |
-
|
| 732 |
-
.. http:get:: /api/runner/abort_pending
|
| 733 |
-
|
| 734 |
-
:status 200: {"status": "ok"}
|
| 735 |
-
"""
|
| 736 |
-
abort_pending()
|
| 737 |
-
return jsonify({"status": "ok"}), 200
|
| 738 |
-
|
| 739 |
-
@design.route("/api/runner/abort_current", methods=["POST"])
|
| 740 |
-
def api_abort_current():
|
| 741 |
-
"""
|
| 742 |
-
.. :quickref: Workflow Design; abort right after current action during execution
|
| 743 |
-
|
| 744 |
-
.. http:get:: /api/runner/abort_current
|
| 745 |
-
|
| 746 |
-
:status 200: {"status": "ok"}
|
| 747 |
-
"""
|
| 748 |
-
abort_current()
|
| 749 |
-
return jsonify({"status": "ok"}), 200
|
| 750 |
-
|
| 751 |
-
@design.route("/api/runner/pause", methods=["POST"])
|
| 752 |
-
def api_pause():
|
| 753 |
-
"""
|
| 754 |
-
.. :quickref: Workflow Design; pause during execution
|
| 755 |
-
|
| 756 |
-
.. http:get:: /api/runner/pause
|
| 757 |
-
|
| 758 |
-
:status 200: {"status": "ok"}
|
| 759 |
-
"""
|
| 760 |
-
msg = pause()
|
| 761 |
-
return jsonify({"status": "ok", "pause_status": msg}), 200
|
| 762 |
-
|
| 763 |
-
@design.route("/api/runner/retry", methods=["POST"])
|
| 764 |
-
def api_retry():
|
| 765 |
-
"""
|
| 766 |
-
.. :quickref: Workflow Design; retry when error occur during execution
|
| 767 |
-
|
| 768 |
-
.. http:get:: /api/runner/retry
|
| 769 |
-
|
| 770 |
-
:status 200: {"status": "ok"}
|
| 771 |
-
"""
|
| 772 |
-
retry()
|
| 773 |
-
return jsonify({"status": "ok, retrying failed step"}), 200
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
@design.route("/api/design/submit", methods=["POST"])
|
| 777 |
-
def submit_script():
|
| 778 |
-
"""
|
| 779 |
-
.. :quickref: Workflow Design; submit script
|
| 780 |
-
|
| 781 |
-
.. http:get:: /api/design/submit
|
| 782 |
-
|
| 783 |
-
:status 200: {"status": "ok"}
|
| 784 |
-
"""
|
| 785 |
-
deck = global_config.deck
|
| 786 |
-
deck_name = os.path.splitext(os.path.basename(deck.__file__))[0] if deck.__name__ == "__main__" else deck.__name__
|
| 787 |
-
script = Script(author=session.get('user'), deck=deck_name)
|
| 788 |
-
script_collection = request.get_json()
|
| 789 |
-
script.python_script = script_collection
|
| 790 |
-
# todo check script format
|
| 791 |
-
utils.post_script_file(script)
|
| 792 |
-
return jsonify({"status": "ok"}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/design/templates/design/experiment_builder.html
DELETED
|
@@ -1,521 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Design{% endblock %}
|
| 3 |
-
|
| 4 |
-
{% block body %}
|
| 5 |
-
{# overlay block for text-to-code gen #}
|
| 6 |
-
<style>
|
| 7 |
-
.code-overlay {
|
| 8 |
-
position: absolute;
|
| 9 |
-
top: 0;
|
| 10 |
-
right: -50%;
|
| 11 |
-
height: 100%;
|
| 12 |
-
width: 50%;
|
| 13 |
-
z-index: 100;
|
| 14 |
-
background: #f8f9fa;
|
| 15 |
-
box-shadow: -2px 0 6px rgba(0, 0, 0, 0.1);
|
| 16 |
-
transition: right 0.3s ease;
|
| 17 |
-
overflow-y: auto;
|
| 18 |
-
}
|
| 19 |
-
.code-overlay.show {
|
| 20 |
-
right: 0;
|
| 21 |
-
}
|
| 22 |
-
</style>
|
| 23 |
-
<div id="overlay" class="overlay">
|
| 24 |
-
<div>
|
| 25 |
-
<h3 id="overlay-text">Generating design, please wait...</h3>
|
| 26 |
-
<div class="spinner-border" role="status"></div>
|
| 27 |
-
</div>
|
| 28 |
-
</div>
|
| 29 |
-
|
| 30 |
-
<div class="row">
|
| 31 |
-
<div class="col-md-3 scroll-column" >
|
| 32 |
-
|
| 33 |
-
{# select deck if this is online#}
|
| 34 |
-
{% if off_line %}
|
| 35 |
-
<form id="select-deck" method="POST" action="{{ url_for('design.import_pseudo') }}" enctype="multipart/form-data">
|
| 36 |
-
<div class="input-group mb-3">
|
| 37 |
-
{# <label for="pkl_name" class="form-label">Choose/Change deck:</label>#}
|
| 38 |
-
<select class="form-select" name="pkl_name" id="pkl_name" required onchange="document.getElementById('select-deck').submit();">
|
| 39 |
-
<option {{ '' if 'pseudo_deck' in session else 'selected' }} disabled hidden style="overflow-wrap: break-word;" name="pkl_name" id="pkl_name" value=""> -- choose deck --</option>
|
| 40 |
-
{% for connection in history %}
|
| 41 |
-
<option {{ 'selected' if session['pseudo_deck']==connection else '' }} style="overflow-wrap: break-word;" name="pkl_name" id="pkl_name" value="{{connection}}">{{connection.split('.')[0]}}</option>
|
| 42 |
-
{% endfor %}
|
| 43 |
-
</select>
|
| 44 |
-
</div>
|
| 45 |
-
</form>
|
| 46 |
-
<hr>
|
| 47 |
-
{% endif %}
|
| 48 |
-
|
| 49 |
-
{# edit action #}
|
| 50 |
-
{% if session["edit_action"] %}
|
| 51 |
-
{% with action = session["edit_action"] %}
|
| 52 |
-
<h5> {{ format_name(action['action']) }} </h5>
|
| 53 |
-
<form role="form" method='POST' name="{{instrument}}" action="{{ url_for('design.edit_action', uuid=session["edit_action"]['uuid']) }}">
|
| 54 |
-
{% if not action['args'] == None %}
|
| 55 |
-
<div class="form-group">
|
| 56 |
-
{% if not action['args'].__class__.__name__ == 'dict' %}
|
| 57 |
-
<div class="input-group mb-3">
|
| 58 |
-
<label class="input-group-text">{{ action['action'] }}</label>
|
| 59 |
-
<input class="form-control" type="text" id="arg" name="arg" placeholder="{{ action['arg_types']}}" value="{{ action['args'] }}" aria-labelledby="variableHelpBlock">
|
| 60 |
-
</div>
|
| 61 |
-
{% else %}
|
| 62 |
-
{# {% for arg in action['args'] %}#}
|
| 63 |
-
{# <div class="input-group mb-3">#}
|
| 64 |
-
{# <label class="input-group-text">{{ format_name(arg) }}</label>#}
|
| 65 |
-
{# <input class="form-control" type="text" id="{{ arg }}" name="{{ arg }}" placeholder="{{ action['arg_types'][arg] }}" value="{{ action['args'][arg] }}" aria-labelledby="variableHelpBlock">#}
|
| 66 |
-
{# </div>#}
|
| 67 |
-
{# {% endfor %}#}
|
| 68 |
-
{# <div class="input-group mb-3">#}
|
| 69 |
-
{# <label class="input-group-text">Save Output?</label>#}
|
| 70 |
-
{# <input class="form-control" type="text" id="return" name="return" value="{{ action['return'] }}" aria-labelledby="variableHelpBlock">#}
|
| 71 |
-
{# </div>#}
|
| 72 |
-
{{ forms.hidden_tag() }}
|
| 73 |
-
{% for field in forms %}
|
| 74 |
-
{% if field.type not in ['CSRFTokenField'] %}
|
| 75 |
-
<div class="input-group mb-3">
|
| 76 |
-
<label class="input-group-text">{{ field.label.text }}</label>
|
| 77 |
-
{{ field(class="form-control") }}
|
| 78 |
-
<div class="form-text">{{ field.description }} </div>
|
| 79 |
-
</div>
|
| 80 |
-
{% endif %}
|
| 81 |
-
{% endfor %}
|
| 82 |
-
{% endif %}
|
| 83 |
-
</div>
|
| 84 |
-
{% endif %}
|
| 85 |
-
<button class="btn btn-primary" type="submit">Save</button>
|
| 86 |
-
<button class="btn btn-primary" type="submit" name="back" id="back" value="back">Back</button>
|
| 87 |
-
</form>
|
| 88 |
-
{% endwith %}
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
{% elif instrument %}
|
| 92 |
-
<div>
|
| 93 |
-
<div class="d-flex justify-content-between align-items-center " style="margin-bottom: 1vh;margin-top: 1vh;">
|
| 94 |
-
<a class="btn btn-primary" role="button" type="button" href="{{url_for('design.experiment_builder')}}"><i class="bi bi-arrow-return-left"></i></a>
|
| 95 |
-
{{ format_name(instrument) }}
|
| 96 |
-
</div>
|
| 97 |
-
|
| 98 |
-
{% if script.editing_type == "script" %}
|
| 99 |
-
{# Auto Fill Toggle #}
|
| 100 |
-
<div class="d-flex justify-content-between align-items-center " style="margin-bottom: 1vh;margin-top: 1vh;">
|
| 101 |
-
<div></div>
|
| 102 |
-
<form role="form" method='POST' name="autoFill" id="autoFill">
|
| 103 |
-
<div class="form-check form-switch">
|
| 104 |
-
<input type="hidden" id="autofill" name="autofill" value="temp_value">
|
| 105 |
-
<input class="form-check-input" type="checkbox" id="autoFillCheck" name="autoFillCheck" onchange="document.getElementById('autoFill').submit();"
|
| 106 |
-
value="temp_value"
|
| 107 |
-
{{ "checked" if session["autofill"] else "" }}>
|
| 108 |
-
<label class="form-check-label" for="autoFillCheck">Auto fill</label>
|
| 109 |
-
</div>
|
| 110 |
-
<button type="submit" class="btn btn-default" style="display: none;">Auto fill </button>
|
| 111 |
-
</form>
|
| 112 |
-
</div>
|
| 113 |
-
{% endif %}
|
| 114 |
-
|
| 115 |
-
{# according for instrument #}
|
| 116 |
-
<div class="accordion accordion-flush" id="accordionActions" >
|
| 117 |
-
{% if use_llm and not instrument == "flow_control" %}
|
| 118 |
-
<div class="accordion-item text-to-code">
|
| 119 |
-
<h2 class="accordion-header">
|
| 120 |
-
<button class="accordion-button text-to-code" type="button" data-bs-toggle="collapse" data-bs-target="#text-to-code" aria-expanded="false" aria-controls="collapseExample">
|
| 121 |
-
Text-to-Code
|
| 122 |
-
</button>
|
| 123 |
-
</h2>
|
| 124 |
-
<div id="text-to-code" class="accordion-collapse collapse show" data-bs-parent="#accordionActions">
|
| 125 |
-
<div class="accordion-body">
|
| 126 |
-
<form role="form" method='POST' name="generate" id="generate" action="{{url_for('design.generate_code')}}">
|
| 127 |
-
<input type="hidden" id="instrument" name="instrument" value="{{instrument}}">
|
| 128 |
-
<textarea class="form-control" id="prompt" name="prompt" rows="6" aria-describedby="promptHelpBlock">{{ session['prompt'][instrument] if instrument in session['prompt'] else '' }}</textarea>
|
| 129 |
-
<div id="promptHelpBlock" class="form-text">
|
| 130 |
-
This will overwrite current design.
|
| 131 |
-
</div>
|
| 132 |
-
<!-- <button type="submit" class="btn btn-dark" id="clear" name="clear" onclick="submitForm('generate')">Clear</button>-->
|
| 133 |
-
<button type="submit" class="btn btn-dark" id="gen" name="gen">Generate</button>
|
| 134 |
-
</form>
|
| 135 |
-
</div>
|
| 136 |
-
</div>
|
| 137 |
-
</div>
|
| 138 |
-
{% endif %}
|
| 139 |
-
|
| 140 |
-
{% for name, form in forms.items() %}
|
| 141 |
-
<div class="accordion-item design-control" draggable="true">
|
| 142 |
-
<h2 class="accordion-header">
|
| 143 |
-
<button class="accordion-button collapsed draggable-action"
|
| 144 |
-
type="button" data-bs-toggle="collapse"
|
| 145 |
-
data-bs-target="#{{name}}" aria-expanded="false"
|
| 146 |
-
aria-controls="collapseExample"
|
| 147 |
-
data-action="{{ name }}">
|
| 148 |
-
{{ format_name(name) }}
|
| 149 |
-
</button>
|
| 150 |
-
</h2>
|
| 151 |
-
<div id="{{name}}" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
|
| 152 |
-
<div class="accordion-body">
|
| 153 |
-
<form role="form" method='POST' name="add" id="add-{{name}}">
|
| 154 |
-
<div class="form-group">
|
| 155 |
-
{{ form.hidden_tag() }}
|
| 156 |
-
{% for field in form %}
|
| 157 |
-
{% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
|
| 158 |
-
<div class="input-group mb-3">
|
| 159 |
-
<label class="input-group-text">{{ field.label.text }}</label>
|
| 160 |
-
{% if field.type == "SubmitField" %}
|
| 161 |
-
{{ field(class="btn btn-dark") }}
|
| 162 |
-
{% elif field.type == "BooleanField" %}
|
| 163 |
-
{{ field(class="form-check-input") }}
|
| 164 |
-
{% elif field.type == "FlexibleEnumField" %}
|
| 165 |
-
<input type="text" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.data }}"
|
| 166 |
-
list="{{ field.id }}_options" placeholder="{{ field.render_kw.placeholder if field.render_kw and field.render_kw.placeholder }}"
|
| 167 |
-
class="form-control">
|
| 168 |
-
<datalist id="{{ field.id }}_options">
|
| 169 |
-
{% for key in field.choices %}
|
| 170 |
-
<option value="{{ key }}">{{ key }}</option>
|
| 171 |
-
{% endfor %}
|
| 172 |
-
</datalist>
|
| 173 |
-
|
| 174 |
-
{% else %}
|
| 175 |
-
{{ field(class="form-control") }}
|
| 176 |
-
{% endif %}
|
| 177 |
-
</div>
|
| 178 |
-
{% endif %}
|
| 179 |
-
{% endfor %}
|
| 180 |
-
</div>
|
| 181 |
-
<button type="submit" class="btn btn-dark">Add</button>
|
| 182 |
-
{% if 'hidden_name' in form %}
|
| 183 |
-
<i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top"
|
| 184 |
-
title='{{ form.hidden_name.description or "Docstring is not available" }}'>
|
| 185 |
-
</i>
|
| 186 |
-
{% else %}
|
| 187 |
-
<!-- handle info tooltip for flow control / workflows -->
|
| 188 |
-
{% endif %}
|
| 189 |
-
|
| 190 |
-
</form>
|
| 191 |
-
</div>
|
| 192 |
-
</div>
|
| 193 |
-
</div>
|
| 194 |
-
{% endfor %}
|
| 195 |
-
</div>
|
| 196 |
-
</div>
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
{# according for all actions #}
|
| 200 |
-
{% else %}
|
| 201 |
-
<div style="margin-bottom: 4vh;"></div>
|
| 202 |
-
<div class="accordion accordion-flush">
|
| 203 |
-
|
| 204 |
-
<div class="accordion-item design-control">
|
| 205 |
-
<h5 class="accordion-header">
|
| 206 |
-
<button class="accordion-button" data-bs-toggle="collapse" data-bs-target="#deck" role="button" aria-expanded="false" aria-controls="collapseExample">
|
| 207 |
-
Operations
|
| 208 |
-
</button>
|
| 209 |
-
</h5>
|
| 210 |
-
<div class="accordion-collapse collapse show" id="deck">
|
| 211 |
-
<ul class="list-group">
|
| 212 |
-
{% for instrument in defined_variables %}
|
| 213 |
-
<form role="form" method='GET' name="{{instrument}}" action="{{url_for('design.experiment_builder',instrument=instrument)}}">
|
| 214 |
-
<div>
|
| 215 |
-
<button class="list-group-item list-group-item-action" type="submit">{{format_name(instrument)}}</button>
|
| 216 |
-
</div>
|
| 217 |
-
</form>
|
| 218 |
-
{% endfor %}
|
| 219 |
-
</ul>
|
| 220 |
-
</div>
|
| 221 |
-
</div>
|
| 222 |
-
|
| 223 |
-
{% if local_variables %}
|
| 224 |
-
<div class="accordion-item design-control">
|
| 225 |
-
<h5 class="accordion-header">
|
| 226 |
-
<button class="accordion-button" data-bs-toggle="collapse" data-bs-target="#local" role="button" aria-expanded="false" aria-controls="collapseExample">
|
| 227 |
-
Local Operations
|
| 228 |
-
</button>
|
| 229 |
-
</h5>
|
| 230 |
-
<div class="accordion-collapse collapse show" id="local">
|
| 231 |
-
<ul class="list-group">
|
| 232 |
-
{% for instrument in local_variables %}
|
| 233 |
-
<form role="form" method='GET' name="{{instrument}}" action="{{url_for('design.experiment_builder',instrument=instrument)}}">
|
| 234 |
-
<div>
|
| 235 |
-
<button class="list-group-item list-group-item-action" type="submit" name="device" value="{{instrument}}" >{{instrument}}</button>
|
| 236 |
-
</div>
|
| 237 |
-
</form>
|
| 238 |
-
{% endfor%}
|
| 239 |
-
</ul>
|
| 240 |
-
</div>
|
| 241 |
-
</div>
|
| 242 |
-
{% endif %}
|
| 243 |
-
</div>
|
| 244 |
-
{% endif %}
|
| 245 |
-
</div>
|
| 246 |
-
|
| 247 |
-
{# canvas #}
|
| 248 |
-
<div class="col-md-9 scroll-column">
|
| 249 |
-
<div class="d-flex align-items-center ">
|
| 250 |
-
{# file dropdown menu #}
|
| 251 |
-
<ul class="nav nav-tabs">
|
| 252 |
-
<li class="nav-item dropdown">
|
| 253 |
-
<a class="nav-link dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">File tools <i class="bi bi-tools"></i></a>
|
| 254 |
-
<ul class="dropdown-menu">
|
| 255 |
-
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#newScriptModal">New</button>
|
| 256 |
-
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#jsonModal">Import (.json <i class="bi bi-filetype-json"></i>)</button>
|
| 257 |
-
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#renameModal">Rename</button>
|
| 258 |
-
<a role="button" class="dropdown-item {{'disabled' if not script.name or script.status == 'finalized'}}" type="button" href="{{url_for('database.publish')}}">Save</a></li>
|
| 259 |
-
<button class="dropdown-item" type="button" data-bs-toggle="modal" data-bs-target="#saveasModal">Save as</button>
|
| 260 |
-
{% if not script.status == 'finalized' %}
|
| 261 |
-
<a role="button" class="dropdown-item" type="button" href="{{url_for('database.finalize')}}">Disable editing</a>
|
| 262 |
-
{% endif %}
|
| 263 |
-
<a class="dropdown-item" role="button" type="button" href="{{url_for('design.download', filetype='script')}}">Export (.json <i class="bi bi-filetype-json"></i>)</a>
|
| 264 |
-
</ul>
|
| 265 |
-
</li>
|
| 266 |
-
|
| 267 |
-
<li class="nav-item"><a class="nav-link" aria-current="page" data-bs-toggle="collapse" href="#info">Info</a></li>
|
| 268 |
-
<li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='prep' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='prep') }}">Prep</a></li>
|
| 269 |
-
<li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='script' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='script') }}">Experiment</a></li>
|
| 270 |
-
<li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='cleanup' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='cleanup') }}">Clean up</a></li>
|
| 271 |
-
</ul>
|
| 272 |
-
<form method="POST" action="{{ url_for('design.toggle_show_code') }}" class="ms-3">
|
| 273 |
-
<div class="form-check form-switch">
|
| 274 |
-
<input class="form-check-input" type="checkbox" id="showPythonCodeSwitch" name="show_code"
|
| 275 |
-
onchange="this.form.submit()" {% if session.get('show_code') %}checked{% endif %}>
|
| 276 |
-
<label class="form-check-label" for="showPythonCodeSwitch">Show Python Code</label>
|
| 277 |
-
</div>
|
| 278 |
-
</form>
|
| 279 |
-
|
| 280 |
-
<div class="form-check form-switch ms-auto">
|
| 281 |
-
<input class="form-check-input" type="checkbox" id="toggleLineNumbers" onchange="toggleLineNumbers()">
|
| 282 |
-
<label class="form-check-label" for="toggleLineNumbers">Show Line Numbers</label>
|
| 283 |
-
</div>
|
| 284 |
-
|
| 285 |
-
</div>
|
| 286 |
-
<div class="canvas-wrapper position-relative">
|
| 287 |
-
<div class="canvas" droppable="true">
|
| 288 |
-
<div class="collapse" id="info">
|
| 289 |
-
<table class="table script-table">
|
| 290 |
-
<tbody>
|
| 291 |
-
<tr><th scope="row">Deck Name</th><td>{{script.deck}}</td></tr>
|
| 292 |
-
<tr><th scope="row">Script Name</th><td>{{ script.name }}</td></tr>
|
| 293 |
-
<tr>
|
| 294 |
-
<th scope="row">Editing status <a role="button" data-bs-toggle="popover" data-bs-title="How to use:" data-bs-content="You can choose to disable editing, so the script is finalized and cannot be edited. Use save as to rename the script"><i class="bi bi-info-circle"></i></a></th>
|
| 295 |
-
<td>{{script.status}}</td>
|
| 296 |
-
</tr>
|
| 297 |
-
<tr>
|
| 298 |
-
<th scope="row">Output Values <a role="button" data-bs-toggle="popover" data-bs-title="How to use:" data-bs-content="This will be your output data. If the return data is not a value, it will save as None is the result file"><i class="bi bi-info-circle"></i></a></th>
|
| 299 |
-
<td>
|
| 300 |
-
{% for i in script.config_return()[1] %}
|
| 301 |
-
<input type="checkbox">{{i}}
|
| 302 |
-
{% endfor %}
|
| 303 |
-
</td>
|
| 304 |
-
</tr>
|
| 305 |
-
<tr>
|
| 306 |
-
<th scope="row">Config Variables <a role="button" data-bs-toggle="popover" data-bs-title="How to use:" data-bs-content="This shows variables you want to configure later using .csv file"><i class="bi bi-info-circle"></i></a></th>
|
| 307 |
-
<td>
|
| 308 |
-
<ul>
|
| 309 |
-
{% for i in script.config("script")[0] %}
|
| 310 |
-
<li>{{i}}</li>
|
| 311 |
-
{% endfor %}
|
| 312 |
-
</ul>
|
| 313 |
-
</td>
|
| 314 |
-
</tr>
|
| 315 |
-
</tbody>
|
| 316 |
-
</table>
|
| 317 |
-
</div>
|
| 318 |
-
|
| 319 |
-
<div class="list-group" id="list" style="margin-top: 20px">
|
| 320 |
-
<ul class="reorder">
|
| 321 |
-
{% for button in buttons %}
|
| 322 |
-
<li id="{{ button['id'] }}" style="list-style-type: none;">
|
| 323 |
-
<span class="line-number d-none">{{ button['id'] }}.</span>
|
| 324 |
-
<a href="{{ url_for('design.edit_action', uuid=button['uuid']) }}" type="button" class="btn btn-light" style="{{ button['style'] }}">{{ button['label'] }}</a>
|
| 325 |
-
{% if not button["instrument"] in ["if","while","repeat"] %}
|
| 326 |
-
<a href="{{ url_for('design.duplicate_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-copy"></span></a>
|
| 327 |
-
{% endif %}
|
| 328 |
-
<a href="{{ url_for('design.delete_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-trash"></span></a>
|
| 329 |
-
</li>
|
| 330 |
-
{% endfor %}
|
| 331 |
-
</ul>
|
| 332 |
-
|
| 333 |
-
<!-- Python Code Overlay -->
|
| 334 |
-
<!-- Right side: Python code (conditionally shown) -->
|
| 335 |
-
<!-- Python Code Slide-Over Panel -->
|
| 336 |
-
{% if session.get('show_code') %}
|
| 337 |
-
<div id="pythonCodeOverlay" class="code-overlay bg-light border-start show">
|
| 338 |
-
<div class="overlay-header d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
|
| 339 |
-
<strong>Python Code</strong>
|
| 340 |
-
<button class="btn btn-sm btn-outline-secondary" onclick="toggleCodeOverlay(false)">
|
| 341 |
-
<i class="bi bi-x-lg"></i>
|
| 342 |
-
</button>
|
| 343 |
-
</div>
|
| 344 |
-
<div class="overlay-content p-3">
|
| 345 |
-
{% for stype, script in session['python_code'].items() %}
|
| 346 |
-
<pre><code class="language-python">{{ script }}</code></pre>
|
| 347 |
-
{% endfor %}
|
| 348 |
-
<a href="{{ url_for('design.download', filetype='python') }}">Download <i class="bi bi-download"></i></a>
|
| 349 |
-
</div>
|
| 350 |
-
</div>
|
| 351 |
-
{% endif %}
|
| 352 |
-
</div>
|
| 353 |
-
</div>
|
| 354 |
-
<div>
|
| 355 |
-
<a class="btn btn-dark {{ 'disabled' if not script.name or script.status == "finalized" else ''}}" href="{{url_for('database.publish')}}">Quick Save</a>
|
| 356 |
-
<a class="btn btn-dark " href="{{ url_for('design.experiment_run') }}">Compile and Run</a>
|
| 357 |
-
</div>
|
| 358 |
-
</div>
|
| 359 |
-
</div>
|
| 360 |
-
|
| 361 |
-
{# modals #}
|
| 362 |
-
<div class="modal fade" id="newScriptModal" tabindex="-1" aria-labelledby="newScriptModalLabel" aria-hidden="true">
|
| 363 |
-
<div class="modal-dialog">
|
| 364 |
-
<div class="modal-content">
|
| 365 |
-
<div class="modal-header">
|
| 366 |
-
<h1 class="modal-title fs-5" id="newScriptModalLabel">Save your current editing!</h1>
|
| 367 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 368 |
-
</div>
|
| 369 |
-
<div class="modal-body">
|
| 370 |
-
The current editing won't be saved. Are you sure you want to proceed?
|
| 371 |
-
</div>
|
| 372 |
-
<div class="modal-footer">
|
| 373 |
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Continue editing </button>
|
| 374 |
-
<a role="button" class="btn btn-primary" href="{{url_for('design.clear')}}"> Already saved, clear all </a>
|
| 375 |
-
</div>
|
| 376 |
-
</div>
|
| 377 |
-
</div>
|
| 378 |
-
</div>
|
| 379 |
-
<div class="modal fade" id="saveasModal" tabindex="-1" aria-labelledby="saveasModal" aria-hidden="true" >
|
| 380 |
-
<div class="modal-dialog">
|
| 381 |
-
<div class="modal-content">
|
| 382 |
-
<div class="modal-header">
|
| 383 |
-
<h1 class="modal-title fs-5" id="saveasModal">Save your script as </h1>
|
| 384 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 385 |
-
</div>
|
| 386 |
-
<form method="POST" name="run_name" action="{{ url_for('database.save_as') }}">
|
| 387 |
-
<div class="modal-body">
|
| 388 |
-
<div class="input-group mb-3">
|
| 389 |
-
<label class="input-group-text" for="run_name">Run Name</label>
|
| 390 |
-
<input class="form-control" type="text" name="run_name" id="run_name" placeholder="{{script['name']}}" required="required">
|
| 391 |
-
</div>
|
| 392 |
-
<div class="form-check form-switch">
|
| 393 |
-
<input class="form-check-input" type="checkbox" name="register_workflow" id="register_workflow">
|
| 394 |
-
<label class="input-group-label" for="register_workflow">Register this workflow</label>
|
| 395 |
-
</div>
|
| 396 |
-
</div>
|
| 397 |
-
<div class="modal-footer">
|
| 398 |
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
|
| 399 |
-
<button type="submit" class="btn btn-primary"> Save </button>
|
| 400 |
-
</div>
|
| 401 |
-
</form>
|
| 402 |
-
</div>
|
| 403 |
-
</div>
|
| 404 |
-
</div>
|
| 405 |
-
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModal" aria-hidden="true" >
|
| 406 |
-
<div class="modal-dialog">
|
| 407 |
-
<div class="modal-content">
|
| 408 |
-
<div class="modal-header">
|
| 409 |
-
<h1 class="modal-title fs-5" id="renameModal">Rename your script</h1>
|
| 410 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 411 |
-
</div>
|
| 412 |
-
<form method="POST" name="run_name" action="{{ url_for('database.edit_run_name') }}">
|
| 413 |
-
<div class="modal-body">
|
| 414 |
-
<div class="input-group mb-3">
|
| 415 |
-
<label class="input-group-text" for="run_name">Run Name</label>
|
| 416 |
-
<input class="form-control" type="text" name="run_name" id="run_name" placeholder="{{script['name']}}" required="required">
|
| 417 |
-
</div>
|
| 418 |
-
</div>
|
| 419 |
-
<div class="modal-footer">
|
| 420 |
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
|
| 421 |
-
<button type="submit" class="btn btn-primary"> Save </button>
|
| 422 |
-
</div>
|
| 423 |
-
</form>
|
| 424 |
-
</div>
|
| 425 |
-
</div>
|
| 426 |
-
</div>
|
| 427 |
-
<div class="modal fade" id="jsonModal" tabindex="-1" aria-labelledby="jsonModal" aria-hidden="true" >
|
| 428 |
-
<div class="modal-dialog">
|
| 429 |
-
<div class="modal-content">
|
| 430 |
-
<div class="modal-header">
|
| 431 |
-
<h1 class="modal-title fs-5" id="jsonModal">Import from JSON</h1>
|
| 432 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 433 |
-
</div>
|
| 434 |
-
<form method="POST" action="{{ url_for('design.load_json') }}" enctype="multipart/form-data">
|
| 435 |
-
<div class="modal-body">
|
| 436 |
-
<div class="input-group mb-3">
|
| 437 |
-
<input class="form-control" type="file" name="file" required="required">
|
| 438 |
-
</div>
|
| 439 |
-
</div>
|
| 440 |
-
<div class="modal-footer">
|
| 441 |
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
|
| 442 |
-
<button type="submit" class="btn btn-primary"> Upload </button>
|
| 443 |
-
</div>
|
| 444 |
-
</form>
|
| 445 |
-
</div>
|
| 446 |
-
</div>
|
| 447 |
-
</div>
|
| 448 |
-
<!-- Bootstrap Modal -->
|
| 449 |
-
<div class="modal fade" id="dropModal" tabindex="-1" aria-labelledby="dropModalLabel" aria-hidden="true">
|
| 450 |
-
<div class="modal-dialog">
|
| 451 |
-
<div class="modal-content">
|
| 452 |
-
<div class="modal-header">
|
| 453 |
-
<h5 class="modal-title" id="dropModalLabel">Configure Action</h5>
|
| 454 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 455 |
-
</div>
|
| 456 |
-
<div class="modal-body">
|
| 457 |
-
<p>Drop Position ID: <strong id="modalDropTarget"></strong></p>
|
| 458 |
-
|
| 459 |
-
<!-- Form will be dynamically inserted here -->
|
| 460 |
-
<div id="modalFormFields"></div>
|
| 461 |
-
</div>
|
| 462 |
-
<div class="modal-footer">
|
| 463 |
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
| 464 |
-
</div>
|
| 465 |
-
</div>
|
| 466 |
-
</div>
|
| 467 |
-
</div>
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
{% if instrument and use_llm %}
|
| 471 |
-
<script>
|
| 472 |
-
const buttonIds = {{ ['generate'] | tojson }};
|
| 473 |
-
</script>
|
| 474 |
-
<script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
|
| 475 |
-
{% endif %}
|
| 476 |
-
|
| 477 |
-
<script>
|
| 478 |
-
const updateListUrl = "{{ url_for('design.update_list') }}";
|
| 479 |
-
|
| 480 |
-
// Toggle visibility of line numbers
|
| 481 |
-
function toggleLineNumbers(save = true) {
|
| 482 |
-
const show = document.getElementById('toggleLineNumbers').checked;
|
| 483 |
-
document.querySelectorAll('.line-number').forEach(el => {
|
| 484 |
-
el.classList.toggle('d-none', !show);
|
| 485 |
-
});
|
| 486 |
-
|
| 487 |
-
if (save) {
|
| 488 |
-
localStorage.setItem('showLineNumbers', show ? 'true' : 'false');
|
| 489 |
-
}
|
| 490 |
-
}
|
| 491 |
-
|
| 492 |
-
function toggleCodeOverlay() {
|
| 493 |
-
const overlay = document.getElementById("pythonCodeOverlay");
|
| 494 |
-
const toggleBtn = document.getElementById("codeToggleBtn");
|
| 495 |
-
overlay.classList.toggle("show");
|
| 496 |
-
|
| 497 |
-
// Change arrow icon
|
| 498 |
-
const icon = toggleBtn.querySelector("i");
|
| 499 |
-
icon.classList.toggle("bi-chevron-left");
|
| 500 |
-
icon.classList.toggle("bi-chevron-right");
|
| 501 |
-
}
|
| 502 |
-
|
| 503 |
-
// Restore state on page load
|
| 504 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 505 |
-
const savedState = localStorage.getItem('showLineNumbers');
|
| 506 |
-
const checkbox = document.getElementById('toggleLineNumbers');
|
| 507 |
-
|
| 508 |
-
if (savedState === 'true') {
|
| 509 |
-
checkbox.checked = true;
|
| 510 |
-
}
|
| 511 |
-
|
| 512 |
-
toggleLineNumbers(false); // don't overwrite localStorage on load
|
| 513 |
-
|
| 514 |
-
checkbox.addEventListener('change', () => toggleLineNumbers());
|
| 515 |
-
});
|
| 516 |
-
</script>
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
<script src="{{ url_for('static', filename='js/sortable_design.js') }}"></script>
|
| 520 |
-
|
| 521 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/design/templates/design/experiment_run.html
DELETED
|
@@ -1,558 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Design execution{% endblock %}
|
| 3 |
-
|
| 4 |
-
{% block body %}
|
| 5 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.4.1/socket.io.js"></script>
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
{% if no_deck_warning and not dismiss %}
|
| 9 |
-
{# auto pop import when there is no deck#}
|
| 10 |
-
<script type="text/javascript">
|
| 11 |
-
function OpenBootstrapPopup() {
|
| 12 |
-
$("#importModal").modal('show');
|
| 13 |
-
}
|
| 14 |
-
window.onload = function () {
|
| 15 |
-
OpenBootstrapPopup();
|
| 16 |
-
};
|
| 17 |
-
</script>
|
| 18 |
-
{% endif %}
|
| 19 |
-
|
| 20 |
-
<div class="row">
|
| 21 |
-
{% if script['script'] or script['prep'] or script['cleanup'] %}
|
| 22 |
-
<div class="col-lg-6 col-sm-12" id="run-panel" style="{{ 'display: none;' if pause_status else '' }}">
|
| 23 |
-
<ul class="nav nav-tabs" id="myTabs" role="tablist">
|
| 24 |
-
<li class="nav-item" role="presentation">
|
| 25 |
-
<a class="nav-link {{ 'disabled' if config_list else '' }} {{ 'active' if not config_list else '' }}" id="tab1-tab" data-bs-toggle="tab" href="#tab1" role="tab" aria-controls="tab1" aria-selected="false">Repeat</a>
|
| 26 |
-
</li>
|
| 27 |
-
<li class="nav-item" role="presentation">
|
| 28 |
-
<a class="nav-link {{ 'disabled' if not config_list else '' }} {{ 'active' if config_list else '' }}" id="tab4-tab" data-bs-toggle="tab" href="#tab4" role="tab" aria-controls="tab4" aria-selected="false">Configuration</a>
|
| 29 |
-
</li>
|
| 30 |
-
<li class="nav-item" role="presentation">
|
| 31 |
-
<a class="nav-link {{ 'disabled' if not config_list or not return_list else '' }}" id="tab3-tab" data-bs-toggle="tab" href="#tab3" role="tab" aria-controls="tab3" aria-selected="false">Bayesian Optimization</a>
|
| 32 |
-
</li>
|
| 33 |
-
</ul>
|
| 34 |
-
<div class="tab-content" id="myTabsContent">
|
| 35 |
-
<div class="tab-pane fade {{ 'show active' if not config_list else '' }}" id="tab1" role="tabpanel" aria-labelledby="tab1-tab">
|
| 36 |
-
<p><h5>Control panel:</h5></p>
|
| 37 |
-
<form role="form" method='POST' name="run" action="{{url_for('design.experiment_run')}}">
|
| 38 |
-
<div class="input-group mb-3">
|
| 39 |
-
<label class="input-group-text" for="repeat">Repeat for </label>
|
| 40 |
-
<input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="1">
|
| 41 |
-
<label class="input-group-text" for="repeat"> times</label>
|
| 42 |
-
</div>
|
| 43 |
-
<div class="input-group mb-3">
|
| 44 |
-
<button class="form-control" type="submit" class="btn btn-dark">Run</button>
|
| 45 |
-
</div>
|
| 46 |
-
</form>
|
| 47 |
-
</div>
|
| 48 |
-
|
| 49 |
-
{#TODO#}
|
| 50 |
-
<div class="tab-pane fade {{ 'show active' if config_list else '' }}" id="tab4" role="tabpanel" aria-labelledby="tab4-tab">
|
| 51 |
-
<!-- File Management Section -->
|
| 52 |
-
<div class="card mb-4">
|
| 53 |
-
<div class="card-header d-flex justify-content-between align-items-center">
|
| 54 |
-
<h6 class="mb-0"><i class="bi bi-file-earmark-text"></i> Configuration File</h6>
|
| 55 |
-
<small class="text-muted">
|
| 56 |
-
<a href="{{ url_for('design.download', filetype='configure') }}">
|
| 57 |
-
<i class="bi bi-download"></i> Download Empty Template
|
| 58 |
-
</a>
|
| 59 |
-
</small>
|
| 60 |
-
</div>
|
| 61 |
-
<div class="card-body">
|
| 62 |
-
<div class="row g-3">
|
| 63 |
-
<!-- File Selection -->
|
| 64 |
-
<div class="col-md-6">
|
| 65 |
-
<form name="filenameForm" id="filenameForm" method="GET" action="{{ url_for('design.experiment_run') }}" enctype="multipart/form-data">
|
| 66 |
-
<div class="input-group">
|
| 67 |
-
<label class="input-group-text"><i class="bi bi-folder2-open"></i></label>
|
| 68 |
-
<select class="form-select" name="filename" id="filenameSelect" onchange="document.getElementById('filenameForm').submit();">
|
| 69 |
-
<option {{ 'selected' if not filename else '' }} value="">-- Select existing file --</option>
|
| 70 |
-
{% for config_file in config_file_list %}
|
| 71 |
-
<option {{ 'selected' if filename == config_file else '' }} value="{{ config_file }}">{{ config_file }}</option>
|
| 72 |
-
{% endfor %}
|
| 73 |
-
</select>
|
| 74 |
-
</div>
|
| 75 |
-
</form>
|
| 76 |
-
</div>
|
| 77 |
-
|
| 78 |
-
<!-- File Upload -->
|
| 79 |
-
<div class="col-md-6">
|
| 80 |
-
<form method="POST" id="loadFile" name="loadFile" action="{{ url_for('design.upload') }}" enctype="multipart/form-data">
|
| 81 |
-
<div class="input-group">
|
| 82 |
-
<input class="form-control" name="file" type="file" accept=".csv" required="required" onchange="document.getElementById('loadFile').submit();">
|
| 83 |
-
</div>
|
| 84 |
-
</form>
|
| 85 |
-
</div>
|
| 86 |
-
</div>
|
| 87 |
-
</div>
|
| 88 |
-
</div>
|
| 89 |
-
<!-- Configuration Table -->
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
<div class="card mb-4">
|
| 94 |
-
<div class="card-header position-relative">
|
| 95 |
-
<div class="position-absolute top-50 end-0 translate-middle-y me-3">
|
| 96 |
-
<span id="saveStatus" class="badge bg-success" style="display: none;">
|
| 97 |
-
<i class="bi bi-check-circle"></i> Auto-saved
|
| 98 |
-
</span>
|
| 99 |
-
<span id="modifiedStatus" class="badge bg-warning" style="display: none;">
|
| 100 |
-
<i class="bi bi-pencil"></i> Modified
|
| 101 |
-
</span>
|
| 102 |
-
</div>
|
| 103 |
-
</div>
|
| 104 |
-
<div class="card-body p-0">
|
| 105 |
-
<form method="POST" name="online-config" id="online-config" action="{{url_for('design.experiment_run')}}">
|
| 106 |
-
<div class="table-responsive">
|
| 107 |
-
<table id="dataInputTable" class="table table-striped table-hover mb-0">
|
| 108 |
-
<thead class="table-dark">
|
| 109 |
-
<tr>
|
| 110 |
-
<th style="width: 40px;">#</th>
|
| 111 |
-
{% for column in config_list %}
|
| 112 |
-
<th>{{ column }}</th>
|
| 113 |
-
{% endfor %}
|
| 114 |
-
<th></th>
|
| 115 |
-
</tr>
|
| 116 |
-
</thead>
|
| 117 |
-
<tbody id="tableBody">
|
| 118 |
-
</tbody>
|
| 119 |
-
</table>
|
| 120 |
-
</div>
|
| 121 |
-
<div class="card-footer">
|
| 122 |
-
<div class="d-flex justify-content-between align-items-center">
|
| 123 |
-
<div class="d-flex gap-2">
|
| 124 |
-
<button type="button" class="btn btn-success" onclick="addRow()">
|
| 125 |
-
<i class="bi bi-plus-circle"></i> Add Row
|
| 126 |
-
</button>
|
| 127 |
-
<button type="button" class="btn btn-warning" onclick="clearAllRows()">
|
| 128 |
-
<i class="bi bi-trash"></i> Clear All
|
| 129 |
-
</button>
|
| 130 |
-
<button type="button" class="btn btn-info" onclick="resetToFile()" id="resetToFileBtn" style="display: none;">
|
| 131 |
-
<i class="bi bi-arrow-clockwise"></i> Reset to File
|
| 132 |
-
</button>
|
| 133 |
-
</div>
|
| 134 |
-
<button type="submit" name="online-config" class="btn btn-primary btn-lg">
|
| 135 |
-
<i class="bi bi-play-circle"></i> Run
|
| 136 |
-
</button>
|
| 137 |
-
</div>
|
| 138 |
-
</div>
|
| 139 |
-
</form>
|
| 140 |
-
</div>
|
| 141 |
-
<!-- Config Preview (if loaded from file) -->
|
| 142 |
-
{% if config_preview %}
|
| 143 |
-
<div class="alert alert-info">
|
| 144 |
-
<small><i class="bi bi-info-circle"></i> {{ config_preview|length }} rows loaded from {{ filename }}</small>
|
| 145 |
-
</div>
|
| 146 |
-
{% endif %}
|
| 147 |
-
</div>
|
| 148 |
-
</div>
|
| 149 |
-
|
| 150 |
-
<div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
|
| 151 |
-
<form method="POST" name="bo" action="{{ url_for('design.experiment_run') }}">
|
| 152 |
-
<div class="container py-2">
|
| 153 |
-
<!-- Parameters -->
|
| 154 |
-
<h6 class="fw-bold mt-2 mb-1">Parameters</h6>
|
| 155 |
-
{% for config in config_list %}
|
| 156 |
-
<div class="row align-items-center mb-2">
|
| 157 |
-
<div class="col-3 col-form-label-sm">
|
| 158 |
-
{{ config }}:
|
| 159 |
-
</div>
|
| 160 |
-
<div class="col-6">
|
| 161 |
-
<select class="form-select form-select-sm" id="{{config}}_type" name="{{config}}_type">
|
| 162 |
-
<option selected value="range">range</option>
|
| 163 |
-
<option value="choice">choice</option>
|
| 164 |
-
<option value="fixed">fixed</option>
|
| 165 |
-
</select>
|
| 166 |
-
</div>
|
| 167 |
-
<div class="col-3">
|
| 168 |
-
<input type="text" class="form-control form-control-sm" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
|
| 169 |
-
</div>
|
| 170 |
-
</div>
|
| 171 |
-
{% endfor %}
|
| 172 |
-
<!-- Objective -->
|
| 173 |
-
<h6 class="fw-bold mt-3 mb-1">Objectives</h6>
|
| 174 |
-
{% for objective in return_list %}
|
| 175 |
-
<div class="row align-items-center mb-2">
|
| 176 |
-
<div class="col-3 col-form-label-sm">
|
| 177 |
-
{{ objective }}:
|
| 178 |
-
</div>
|
| 179 |
-
<div class="col-6">
|
| 180 |
-
<select class="form-select form-select-sm" id="{{objective}}_min" name="{{objective}}_min">
|
| 181 |
-
<option selected>minimize</option>
|
| 182 |
-
<option>maximize</option>
|
| 183 |
-
<option>none</option>
|
| 184 |
-
</select>
|
| 185 |
-
</div>
|
| 186 |
-
</div>
|
| 187 |
-
{% endfor %}
|
| 188 |
-
<h6 class="fw-bold mt-3 mb-1">Budget</h6>
|
| 189 |
-
<div class="input-group mb-3">
|
| 190 |
-
<label class="input-group-text" for="repeat">Max iteration </label>
|
| 191 |
-
<input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
|
| 192 |
-
</div>
|
| 193 |
-
{% if not no_deck_warning%}
|
| 194 |
-
<div class="input-group mb-3">
|
| 195 |
-
<button class="form-control" type="submit" name="bo">Run</button>
|
| 196 |
-
</div>
|
| 197 |
-
{% endif %}
|
| 198 |
-
</div>
|
| 199 |
-
</form>
|
| 200 |
-
</div>
|
| 201 |
-
</div>
|
| 202 |
-
</div>
|
| 203 |
-
{% else %}
|
| 204 |
-
<div class="col-lg-6 col-sm-12" id="placeholder-panel">
|
| 205 |
-
</div>
|
| 206 |
-
{% endif %}
|
| 207 |
-
|
| 208 |
-
<div class="col-lg-6 col-sm-12" id="code-panel" style="{{ '' if pause_status else 'display: none;'}}">
|
| 209 |
-
<p>
|
| 210 |
-
<h5>Progress:</h5>
|
| 211 |
-
{% if "prep" in line_collection.keys() %}
|
| 212 |
-
{% set stype = "prep" %}
|
| 213 |
-
<h6>Preparation:</h6>
|
| 214 |
-
{% for code in line_collection["prep"] %}
|
| 215 |
-
<pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
|
| 216 |
-
{% endfor %}
|
| 217 |
-
{% endif %}
|
| 218 |
-
{% if "script" in line_collection.keys() %}
|
| 219 |
-
{% set stype = "script" %}
|
| 220 |
-
<h6>Experiment:</h6>
|
| 221 |
-
{% for code in line_collection["script"] %}
|
| 222 |
-
<pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
|
| 223 |
-
{% endfor %}
|
| 224 |
-
{% endif %}
|
| 225 |
-
{% if "cleanup" in line_collection.keys() %}
|
| 226 |
-
{% set stype = "cleanup" %}
|
| 227 |
-
<h6>Cleanup:</h6>
|
| 228 |
-
{% for code in line_collection["cleanup"] %}
|
| 229 |
-
<pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
|
| 230 |
-
{% endfor %}
|
| 231 |
-
{% endif %}
|
| 232 |
-
</p>
|
| 233 |
-
</div>
|
| 234 |
-
<div class="col-lg-6 col-sm-12 logging-panel">
|
| 235 |
-
<p>
|
| 236 |
-
<div class="p d-flex justify-content-between align-items-center">
|
| 237 |
-
<h5>Progress:</h5>
|
| 238 |
-
<div class="d-flex gap-2 ms-auto">
|
| 239 |
-
<button id="pause-resume" class="btn btn-info text-white" data-bs-toggle="tooltip" title="Pause execution">
|
| 240 |
-
{% if pause_status %}
|
| 241 |
-
<i class="bi bi-play-circle"></i>
|
| 242 |
-
{% else %}
|
| 243 |
-
<i class="bi bi-pause-circle"></i>
|
| 244 |
-
{% endif %}
|
| 245 |
-
</button>
|
| 246 |
-
<button id="abort-current" class="btn btn-danger text-white" data-bs-toggle="tooltip" title="Stop execution after current step">
|
| 247 |
-
<i class="bi bi-stop-circle"></i>
|
| 248 |
-
</button>
|
| 249 |
-
<button id="abort-pending" class="btn btn-warning text-white" data-bs-toggle="tooltip" title="Stop execution after current iteration">
|
| 250 |
-
<i class="bi bi-hourglass-split"></i>
|
| 251 |
-
</button>
|
| 252 |
-
</div>
|
| 253 |
-
</div>
|
| 254 |
-
<div class="text-muted mt-2">
|
| 255 |
-
<small><strong>Note:</strong> The current step cannot be paused or stopped until it completes. </small>
|
| 256 |
-
</div>
|
| 257 |
-
|
| 258 |
-
<div class="progress" role="progressbar" aria-label="Animated striped example" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100">
|
| 259 |
-
<div id="progress-bar-inner" class="progress-bar progress-bar-striped progress-bar-animated"></div>
|
| 260 |
-
</div>
|
| 261 |
-
<p><h5>Log:</h5></p>
|
| 262 |
-
<div id="logging-panel"></div>
|
| 263 |
-
</div>
|
| 264 |
-
|
| 265 |
-
</div>
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
<!-- Error Modal -->
|
| 272 |
-
<div class="modal fade" id="error-modal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
|
| 273 |
-
<div class="modal-dialog">
|
| 274 |
-
<div class="modal-content">
|
| 275 |
-
<div class="modal-header">
|
| 276 |
-
<h5 class="modal-title" id="errorModalLabel">Error Detected</h5>
|
| 277 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 278 |
-
</div>
|
| 279 |
-
<div class="modal-body">
|
| 280 |
-
<p id="error-message">An error has occurred.</p>
|
| 281 |
-
<p>Do you want to continue execution or stop?</p>
|
| 282 |
-
</div>
|
| 283 |
-
<div class="modal-footer">
|
| 284 |
-
<button type="button" class="btn btn-primary" id="retry-btn" data-bs-dismiss="modal">Rerun Current Step</button>
|
| 285 |
-
<button type="button" class="btn btn-success" id="continue-btn" data-bs-dismiss="modal">Continue</button>
|
| 286 |
-
<button type="button" class="btn btn-danger" id="stop-btn" data-bs-dismiss="modal">Stop Execution</button>
|
| 287 |
-
</div>
|
| 288 |
-
</div>
|
| 289 |
-
</div>
|
| 290 |
-
</div>
|
| 291 |
-
|
| 292 |
-
<script src="{{ url_for('static', filename='js/socket_handler.js') }}"></script>
|
| 293 |
-
<script>
|
| 294 |
-
var rowCount = 0;
|
| 295 |
-
var configColumns = [
|
| 296 |
-
{% for column in config_list %}
|
| 297 |
-
'{{ column }}'{{ ',' if not loop.last else '' }}
|
| 298 |
-
{% endfor %}
|
| 299 |
-
];
|
| 300 |
-
var configTypes = {
|
| 301 |
-
{% for column, type in config_type_list.items() %}
|
| 302 |
-
'{{ column }}': '{{ type }}'{{ ',' if not loop.last else '' }}
|
| 303 |
-
{% endfor %}
|
| 304 |
-
};
|
| 305 |
-
|
| 306 |
-
// State management
|
| 307 |
-
var originalFileData = null;
|
| 308 |
-
var isModifiedFromFile = false;
|
| 309 |
-
var saveTimeout = null;
|
| 310 |
-
var lastSavedData = null;
|
| 311 |
-
|
| 312 |
-
function addRow(data = null, skipSave = false) {
|
| 313 |
-
rowCount++;
|
| 314 |
-
var tableBody = document.getElementById("tableBody");
|
| 315 |
-
var newRow = tableBody.insertRow(-1);
|
| 316 |
-
|
| 317 |
-
// Row number cell
|
| 318 |
-
var rowNumCell = newRow.insertCell(-1);
|
| 319 |
-
rowNumCell.innerHTML = '<span class="badge bg-secondary">' + rowCount + '</span>';
|
| 320 |
-
|
| 321 |
-
// Data cells
|
| 322 |
-
configColumns.forEach(function(column, index) {
|
| 323 |
-
var cell = newRow.insertCell(-1);
|
| 324 |
-
var value = data && data[column] ? data[column] : '';
|
| 325 |
-
var placeholder = configTypes[column] || 'value';
|
| 326 |
-
cell.innerHTML = '<input type="text" class="form-control form-control-sm" name="' +
|
| 327 |
-
column + '[' + rowCount + ']" value="' + value + '" placeholder="' + placeholder +
|
| 328 |
-
'" oninput="onInputChange()" onchange="onInputChange()">';
|
| 329 |
-
});
|
| 330 |
-
|
| 331 |
-
// Action cell
|
| 332 |
-
var actionCell = newRow.insertCell(-1);
|
| 333 |
-
actionCell.innerHTML = '<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRow(this)" title="Remove row">' +
|
| 334 |
-
'<i class="bi bi-trash"></i></button>';
|
| 335 |
-
|
| 336 |
-
if (!skipSave) {
|
| 337 |
-
markAsModified();
|
| 338 |
-
debouncedSave();
|
| 339 |
-
}
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
function removeRow(button) {
|
| 343 |
-
var row = button.closest('tr');
|
| 344 |
-
row.remove();
|
| 345 |
-
updateRowNumbers();
|
| 346 |
-
markAsModified();
|
| 347 |
-
debouncedSave();
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
function updateRowNumbers() {
|
| 351 |
-
var tableBody = document.getElementById("tableBody");
|
| 352 |
-
var rows = tableBody.getElementsByTagName('tr');
|
| 353 |
-
for (var i = 0; i < rows.length; i++) {
|
| 354 |
-
var badge = rows[i].querySelector('.badge');
|
| 355 |
-
if (badge) {
|
| 356 |
-
badge.textContent = i + 1;
|
| 357 |
-
}
|
| 358 |
-
}
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
function clearAllRows() {
|
| 362 |
-
if (confirm('Are you sure you want to clear all rows?')) {
|
| 363 |
-
var tableBody = document.getElementById("tableBody");
|
| 364 |
-
tableBody.innerHTML = '';
|
| 365 |
-
rowCount = 0;
|
| 366 |
-
markAsModified();
|
| 367 |
-
clearSavedData();
|
| 368 |
-
// Add 5 empty rows by default
|
| 369 |
-
for (let i = 0; i < 5; i++) {
|
| 370 |
-
addRow(null, true);
|
| 371 |
-
}
|
| 372 |
-
debouncedSave();
|
| 373 |
-
}
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
function resetToFile() {
|
| 377 |
-
if (originalFileData && confirm('Reset to original file data? This will lose all manual changes.')) {
|
| 378 |
-
loadDataFromSource(originalFileData, false);
|
| 379 |
-
isModifiedFromFile = false;
|
| 380 |
-
updateStatusIndicators();
|
| 381 |
-
debouncedSave();
|
| 382 |
-
}
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
function onInputChange() {
|
| 386 |
-
markAsModified();
|
| 387 |
-
debouncedSave();
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
function markAsModified() {
|
| 391 |
-
if (originalFileData) {
|
| 392 |
-
isModifiedFromFile = true;
|
| 393 |
-
updateStatusIndicators();
|
| 394 |
-
}
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
function updateStatusIndicators() {
|
| 398 |
-
var modifiedStatus = document.getElementById('modifiedStatus');
|
| 399 |
-
var resetBtn = document.getElementById('resetToFileBtn');
|
| 400 |
-
|
| 401 |
-
if (isModifiedFromFile && originalFileData) {
|
| 402 |
-
modifiedStatus.style.display = 'inline-block';
|
| 403 |
-
resetBtn.style.display = 'inline-block';
|
| 404 |
-
} else {
|
| 405 |
-
modifiedStatus.style.display = 'none';
|
| 406 |
-
resetBtn.style.display = 'none';
|
| 407 |
-
}
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
function showSaveStatus() {
|
| 411 |
-
var saveStatus = document.getElementById('saveStatus');
|
| 412 |
-
saveStatus.style.display = 'inline-block';
|
| 413 |
-
setTimeout(function() {
|
| 414 |
-
saveStatus.style.display = 'none';
|
| 415 |
-
}, 2000);
|
| 416 |
-
}
|
| 417 |
-
|
| 418 |
-
function debouncedSave() {
|
| 419 |
-
clearTimeout(saveTimeout);
|
| 420 |
-
saveTimeout = setTimeout(function() {
|
| 421 |
-
saveFormData();
|
| 422 |
-
showSaveStatus();
|
| 423 |
-
}, 1000); // Save 1 second after user stops typing
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
function saveFormData() {
|
| 427 |
-
var formData = getCurrentFormData();
|
| 428 |
-
try {
|
| 429 |
-
sessionStorage.setItem('configFormData', JSON.stringify(formData));
|
| 430 |
-
sessionStorage.setItem('configModified', isModifiedFromFile.toString());
|
| 431 |
-
lastSavedData = formData;
|
| 432 |
-
} catch (e) {
|
| 433 |
-
console.warn('Could not save form data to sessionStorage:', e);
|
| 434 |
-
}
|
| 435 |
-
}
|
| 436 |
-
|
| 437 |
-
function getCurrentFormData() {
|
| 438 |
-
var tableBody = document.getElementById("tableBody");
|
| 439 |
-
var rows = tableBody.getElementsByTagName('tr');
|
| 440 |
-
var data = [];
|
| 441 |
-
|
| 442 |
-
for (var i = 0; i < rows.length; i++) {
|
| 443 |
-
var inputs = rows[i].getElementsByTagName('input');
|
| 444 |
-
var rowData = {};
|
| 445 |
-
var hasData = false;
|
| 446 |
-
|
| 447 |
-
for (var j = 0; j < inputs.length; j++) {
|
| 448 |
-
var input = inputs[j];
|
| 449 |
-
var name = input.name;
|
| 450 |
-
if (name) {
|
| 451 |
-
var columnName = name.substring(0, name.indexOf('['));
|
| 452 |
-
rowData[columnName] = input.value;
|
| 453 |
-
if (input.value.trim() !== '') {
|
| 454 |
-
hasData = true;
|
| 455 |
-
}
|
| 456 |
-
}
|
| 457 |
-
}
|
| 458 |
-
|
| 459 |
-
if (hasData) {
|
| 460 |
-
data.push(rowData);
|
| 461 |
-
}
|
| 462 |
-
}
|
| 463 |
-
|
| 464 |
-
return data;
|
| 465 |
-
}
|
| 466 |
-
|
| 467 |
-
function loadSavedData() {
|
| 468 |
-
try {
|
| 469 |
-
var savedData = sessionStorage.getItem('configFormData');
|
| 470 |
-
var savedModified = sessionStorage.getItem('configModified');
|
| 471 |
-
|
| 472 |
-
if (savedData) {
|
| 473 |
-
var parsedData = JSON.parse(savedData);
|
| 474 |
-
isModifiedFromFile = savedModified === 'true';
|
| 475 |
-
return parsedData;
|
| 476 |
-
}
|
| 477 |
-
} catch (e) {
|
| 478 |
-
console.warn('Could not load saved form data:', e);
|
| 479 |
-
}
|
| 480 |
-
return null;
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
function clearSavedData() {
|
| 484 |
-
try {
|
| 485 |
-
sessionStorage.removeItem('configFormData');
|
| 486 |
-
sessionStorage.removeItem('configModified');
|
| 487 |
-
} catch (e) {
|
| 488 |
-
console.warn('Could not clear saved data:', e);
|
| 489 |
-
}
|
| 490 |
-
}
|
| 491 |
-
|
| 492 |
-
function loadDataFromSource(data, isFromFile = false) {
|
| 493 |
-
// Clear existing rows
|
| 494 |
-
var tableBody = document.getElementById("tableBody");
|
| 495 |
-
tableBody.innerHTML = '';
|
| 496 |
-
rowCount = 0;
|
| 497 |
-
|
| 498 |
-
// Add rows with data
|
| 499 |
-
data.forEach(function(rowData) {
|
| 500 |
-
addRow(rowData, true);
|
| 501 |
-
});
|
| 502 |
-
|
| 503 |
-
// Add a few empty rows for additional input
|
| 504 |
-
for (let i = 0; i < 3; i++) {
|
| 505 |
-
addRow(null, true);
|
| 506 |
-
}
|
| 507 |
-
|
| 508 |
-
if (isFromFile) {
|
| 509 |
-
originalFileData = JSON.parse(JSON.stringify(data)); // Deep copy
|
| 510 |
-
isModifiedFromFile = false;
|
| 511 |
-
clearSavedData(); // Clear saved data when loading from file
|
| 512 |
-
}
|
| 513 |
-
|
| 514 |
-
updateStatusIndicators();
|
| 515 |
-
}
|
| 516 |
-
|
| 517 |
-
function loadConfigData() {
|
| 518 |
-
// Check for saved form data first
|
| 519 |
-
var savedData = loadSavedData();
|
| 520 |
-
|
| 521 |
-
{% if config_preview %}
|
| 522 |
-
var fileData = {{ config_preview | tojson | safe }};
|
| 523 |
-
originalFileData = JSON.parse(JSON.stringify(fileData)); // Deep copy
|
| 524 |
-
|
| 525 |
-
if (savedData && savedData.length > 0) {
|
| 526 |
-
// Load saved data if available
|
| 527 |
-
loadDataFromSource(savedData, false);
|
| 528 |
-
console.log('Loaded saved form data');
|
| 529 |
-
} else {
|
| 530 |
-
// Load from file
|
| 531 |
-
loadDataFromSource(fileData, true);
|
| 532 |
-
console.log('Loaded file data');
|
| 533 |
-
}
|
| 534 |
-
{% else %}
|
| 535 |
-
if (savedData && savedData.length > 0) {
|
| 536 |
-
// Load saved data
|
| 537 |
-
loadDataFromSource(savedData, false);
|
| 538 |
-
console.log('Loaded saved form data');
|
| 539 |
-
} else {
|
| 540 |
-
// Add default empty rows
|
| 541 |
-
for (let i = 0; i < 5; i++) {
|
| 542 |
-
addRow(null, true);
|
| 543 |
-
}
|
| 544 |
-
}
|
| 545 |
-
{% endif %}
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
// Handle page unload
|
| 549 |
-
window.addEventListener('beforeunload', function() {
|
| 550 |
-
saveFormData();
|
| 551 |
-
});
|
| 552 |
-
|
| 553 |
-
// Initialize table when page loads
|
| 554 |
-
document.addEventListener("DOMContentLoaded", function() {
|
| 555 |
-
loadConfigData();
|
| 556 |
-
});
|
| 557 |
-
</script>
|
| 558 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/main/__init__.py
DELETED
|
File without changes
|
ivoryos/routes/main/main.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
from flask import Blueprint, render_template, current_app
|
| 2 |
-
from flask_login import login_required
|
| 3 |
-
from ivoryos.version import __version__ as ivoryos_version
|
| 4 |
-
|
| 5 |
-
main = Blueprint('main', __name__, template_folder='templates/main')
|
| 6 |
-
|
| 7 |
-
@main.route("/")
|
| 8 |
-
@login_required
|
| 9 |
-
def index():
|
| 10 |
-
"""
|
| 11 |
-
.. :quickref: Home page; ivoryos home page
|
| 12 |
-
|
| 13 |
-
Home page for all available routes
|
| 14 |
-
|
| 15 |
-
.. http:get:: /
|
| 16 |
-
|
| 17 |
-
"""
|
| 18 |
-
off_line = current_app.config["OFF_LINE"]
|
| 19 |
-
return render_template('home.html', off_line=off_line, version=ivoryos_version)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
@main.route("/help")
|
| 23 |
-
def help_info():
|
| 24 |
-
"""
|
| 25 |
-
.. :quickref: Help page; ivoryos info page
|
| 26 |
-
|
| 27 |
-
static information page
|
| 28 |
-
|
| 29 |
-
.. http:get:: /help
|
| 30 |
-
|
| 31 |
-
"""
|
| 32 |
-
sample_deck = """
|
| 33 |
-
from vapourtec.sf10 import SF10
|
| 34 |
-
|
| 35 |
-
# connect SF10 pump
|
| 36 |
-
sf10 = SF10(device_port="com7")
|
| 37 |
-
|
| 38 |
-
# start ivoryOS
|
| 39 |
-
from ivoryos.app import ivoryos
|
| 40 |
-
ivoryos(__name__)
|
| 41 |
-
"""
|
| 42 |
-
return render_template('help.html', sample_deck=sample_deck)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/main/templates/main/help.html
DELETED
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Documentation {% endblock %}
|
| 3 |
-
|
| 4 |
-
{% block body %}
|
| 5 |
-
<h2>Documentations:</h2>
|
| 6 |
-
<div class="accordion accordion-flush" id="helpPage" >
|
| 7 |
-
<div class="accordion-item">
|
| 8 |
-
<h2 class="accordion-header">
|
| 9 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#overall" aria-expanded="false" aria-controls="helpPageControl">
|
| 10 |
-
General information - What is ivoryOS?
|
| 11 |
-
</button>
|
| 12 |
-
</h2>
|
| 13 |
-
<div id="overall" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 14 |
-
<div class="accordion-body">
|
| 15 |
-
<p>
|
| 16 |
-
This web app aims to ease up the control of any Python-based self-driving labs (SDLs) by extracting and displaying functions and parameters for initialized modules dynamically.
|
| 17 |
-
The modules can be hardware API, high-level functions, or experiment workflow. This can potentially be used for general visual programming purposes.
|
| 18 |
-
</p>
|
| 19 |
-
{# <p>Controller: single instrument mode and multi-instrument interface.</p>#}
|
| 20 |
-
{# <p>Workflow editor: automation workflow editor#}
|
| 21 |
-
</div>
|
| 22 |
-
</div>
|
| 23 |
-
</div>
|
| 24 |
-
{# <p>In the Controller tab, you can connect any instrument or instruments from a complete automation deck. The GUI will parse and show available functions and prompt user to the input argument values if need.</p>#}
|
| 25 |
-
{# <p>In the Build Workflow tab, you can edit your own automation workflow from scratch or from the workflow library, where stores the ongoing projects and some basic example workflows.</p>#}
|
| 26 |
-
|
| 27 |
-
<div class="accordion-item">
|
| 28 |
-
<h2 class="accordion-header">
|
| 29 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#connect-device" aria-expanded="false" aria-controls="helpPageControl">
|
| 30 |
-
General information - start ivoryOS from script
|
| 31 |
-
</button>
|
| 32 |
-
</h2>
|
| 33 |
-
<div id="connect-device" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 34 |
-
<div class="accordion-body">
|
| 35 |
-
<pre ><code class="python" >{{ sample_deck }}</code></pre>
|
| 36 |
-
</div>
|
| 37 |
-
</div>
|
| 38 |
-
</div>
|
| 39 |
-
<div class="accordion-item">
|
| 40 |
-
<h2 class="accordion-header">
|
| 41 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#library" aria-expanded="false" aria-controls="helpPageControl">
|
| 42 |
-
Script Editor - Library
|
| 43 |
-
</button>
|
| 44 |
-
</h2>
|
| 45 |
-
<div id="library" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 46 |
-
<div class="accordion-body">
|
| 47 |
-
<p>In "Library" Tab, the interactive database displays all workflow information</p>
|
| 48 |
-
<p>On top of the library page, you can click filter to choose your desired deck out of other automation platforms. You can also search for experiment keyword on the right</p>
|
| 49 |
-
<p>Note that you can edit/delete your own workflow anytime, but in case you would like to adapt other author's workflow, you need to save as your own in a different file name.</p>
|
| 50 |
-
</div>
|
| 51 |
-
</div>
|
| 52 |
-
</div>
|
| 53 |
-
<div class="accordion-item">
|
| 54 |
-
<h2 class="accordion-header">
|
| 55 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#editor" aria-expanded="false" aria-controls="helpPageControl">
|
| 56 |
-
Script Editor - How to use script editor?
|
| 57 |
-
</button>
|
| 58 |
-
</h2>
|
| 59 |
-
<div id="editor" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 60 |
-
<div class="accordion-body">
|
| 61 |
-
<p>You can browse and load other's workflow in "Library" Tab or go to "Build Workflow" Tab to build from scratch.</p>
|
| 62 |
-
<p>On the workflow canvas, there are three coding blocks: Prep, Experiment and Clean up. As the names indicated, the Prep and Clean up are steps taken prior and after to the main experiment, they cannot be repeated. On the other hand, the Experiment section can be repeated by a designated times or <a href="#config">a .csv configuration file</a></p>
|
| 63 |
-
<p>On the left panel, you can choose the deck profile on the top, and browse deck action and builtin python functions. Click to input your argument and add to the canvas, then drag the action to change their order.</p>
|
| 64 |
-
<img src="{{url_for('static', filename='gui_annotation/Slide1.PNG')}}">
|
| 65 |
-
</div>
|
| 66 |
-
</div>
|
| 67 |
-
</div>
|
| 68 |
-
<div class="accordion-item">
|
| 69 |
-
<h2 class="accordion-header">
|
| 70 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#editor-advanced" aria-expanded="false" aria-controls="helpPageControl">
|
| 71 |
-
Script Editor - How to save data and configure reaction?
|
| 72 |
-
</button>
|
| 73 |
-
</h2>
|
| 74 |
-
<div id="editor-advanced" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 75 |
-
<div class="accordion-body">
|
| 76 |
-
|
| 77 |
-
<img src="{{url_for('static', filename='gui_annotation/Slide2.PNG')}}">
|
| 78 |
-
</div>
|
| 79 |
-
</div>
|
| 80 |
-
</div>
|
| 81 |
-
<div class="accordion-item">
|
| 82 |
-
<h2 class="accordion-header">
|
| 83 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#config" aria-expanded="false" aria-controls="helpPageControl">
|
| 84 |
-
Run my script - How to execute my workflow?
|
| 85 |
-
</button>
|
| 86 |
-
</h2>
|
| 87 |
-
<div id="config" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 88 |
-
<div class="accordion-body">
|
| 89 |
-
<p>For workflow with no parameters, simply input repeat times to run.</p>
|
| 90 |
-
<p>To configure your workflow, if there are less or equal than 5 parameters, one can use the web form or import a csv file.
|
| 91 |
-
For workflow with more than 5 parameters, web form is not available.</p>
|
| 92 |
-
<p>If there is any output, Bayesian Optimization will be available for adaptive experimentation. Note that the outputs need to be numeric values. </p>
|
| 93 |
-
{# <img src="{{url_for('static', filename='gui_annotation/Slide3.PNG')}}">#}
|
| 94 |
-
</div>
|
| 95 |
-
</div>
|
| 96 |
-
</div>
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
<div class="accordion-item">
|
| 101 |
-
<h2 class="accordion-header">
|
| 102 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#controller" aria-expanded="false" aria-controls="helpPageControl">
|
| 103 |
-
Control - How to use device interface?
|
| 104 |
-
</button>
|
| 105 |
-
</h2>
|
| 106 |
-
<div id="controller" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 107 |
-
<div class="accordion-body">
|
| 108 |
-
<p>The function cards are draggable for customized layout. The hidden icon is to remove functions from the interface. </p>
|
| 109 |
-
{# <p>(1) Import a python script where devices are connected using Deck Deice tab (e.g. applied to a complete automation deck with a deck.py file).</p>#}
|
| 110 |
-
{# <p>(2) Connect devices in the web app using New Deice tab. There are builtin instruments, but you can always import your own API.</p>#}
|
| 111 |
-
</div>
|
| 112 |
-
</div>
|
| 113 |
-
</div>
|
| 114 |
-
<div class="accordion-item">
|
| 115 |
-
<h2 class="accordion-header">
|
| 116 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#deck" aria-expanded="false" aria-controls="helpPageControl">
|
| 117 |
-
Control - Connect new devices
|
| 118 |
-
</button>
|
| 119 |
-
</h2>
|
| 120 |
-
<div id="deck" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 121 |
-
<div class="accordion-body">
|
| 122 |
-
<p>When temporarily connecting instruments, New device -> New connection -> Import API. Use absolute path of your Python API file.
|
| 123 |
-
Then enter required info (e.g. COM port) to connect a device.</p>
|
| 124 |
-
</div>
|
| 125 |
-
</div>
|
| 126 |
-
</div>
|
| 127 |
-
<div class="accordion-item">
|
| 128 |
-
<h2 class="accordion-header">
|
| 129 |
-
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#install" aria-expanded="false" aria-controls="helpPageControl">
|
| 130 |
-
Supports
|
| 131 |
-
</button>
|
| 132 |
-
</h2>
|
| 133 |
-
<div id="install" class="accordion-collapse collapse" data-bs-parent="#helpPage">
|
| 134 |
-
<div class="accordion-body">
|
| 135 |
-
This is project is a work in progress. In case of any bugs or suggestions, reach out to Ivory: ivoryzhang@chem.ubc.ca or create an issue on GitLab:
|
| 136 |
-
<a href="https://gitlab.com/heingroup/ivoryos">https://gitlab.com/heingroup/ivoryos</a>.
|
| 137 |
-
</div>
|
| 138 |
-
</div>
|
| 139 |
-
</div>
|
| 140 |
-
</div>
|
| 141 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/routes/main/templates/main/home.html
DELETED
|
@@ -1,103 +0,0 @@
|
|
| 1 |
-
{% extends 'base.html' %}
|
| 2 |
-
{% block title %}IvoryOS | Welcome{% endblock %}
|
| 3 |
-
|
| 4 |
-
{% block body %}
|
| 5 |
-
<div class="p-4">
|
| 6 |
-
<h1 class="mb-4" style="font-size: 3rem; font-weight: bold; color: #343a40;">
|
| 7 |
-
Welcome
|
| 8 |
-
</h1>
|
| 9 |
-
<p class="mb-5">Version: {{ version }}</p>
|
| 10 |
-
|
| 11 |
-
{% if enable_design %}
|
| 12 |
-
<!-- Workflow Design Section -->
|
| 13 |
-
<h4 class="mb-3">Workflow Design</h4>
|
| 14 |
-
<div class="row">
|
| 15 |
-
<div class="col-lg-6 mb-3 d-flex align-items-stretch">
|
| 16 |
-
<div class="card rounded shadow-sm flex-fill">
|
| 17 |
-
<div class="card-body">
|
| 18 |
-
<h5 class="card-title">
|
| 19 |
-
<i class="bi bi-folder2-open me-2"></i>Browse designs
|
| 20 |
-
</h5>
|
| 21 |
-
<p class="card-text">View all saved workflows from the database.</p>
|
| 22 |
-
<a href="{{ url_for('database.load_from_database') }}" class="stretched-link"></a>
|
| 23 |
-
</div>
|
| 24 |
-
</div>
|
| 25 |
-
</div>
|
| 26 |
-
<div class="col-lg-6 mb-3 d-flex align-items-stretch">
|
| 27 |
-
<div class="card rounded shadow-sm flex-fill">
|
| 28 |
-
<div class="card-body">
|
| 29 |
-
<h5 class="card-title">
|
| 30 |
-
<i class="bi bi-pencil-square me-2"></i>Edit designs
|
| 31 |
-
</h5>
|
| 32 |
-
<p class="card-text">Create or modify workflows using available functions.</p>
|
| 33 |
-
<a href="{{ url_for('design.experiment_builder') }}" class="stretched-link"></a>
|
| 34 |
-
</div>
|
| 35 |
-
</div>
|
| 36 |
-
</div>
|
| 37 |
-
</div>
|
| 38 |
-
{% endif %}
|
| 39 |
-
|
| 40 |
-
<!-- Workflow Control and Monitor Section -->
|
| 41 |
-
<h4 class="mt-5 mb-3">Workflow Control & Monitoring</h4>
|
| 42 |
-
<div class="row">
|
| 43 |
-
<!-- Always visible: Experiment data -->
|
| 44 |
-
<div class="col-lg-6 mb-3 d-flex align-items-stretch">
|
| 45 |
-
<div class="card rounded shadow-sm flex-fill">
|
| 46 |
-
<div class="card-body">
|
| 47 |
-
<h5 class="card-title">
|
| 48 |
-
<i class="bi bi-graph-up-arrow me-2"></i>Experiment data
|
| 49 |
-
</h5>
|
| 50 |
-
<p class="card-text">Browse workflow logs and output data.</p>
|
| 51 |
-
<a href="{{ url_for('database.list_workflows') }}" class="stretched-link"></a>
|
| 52 |
-
</div>
|
| 53 |
-
</div>
|
| 54 |
-
</div>
|
| 55 |
-
|
| 56 |
-
<!-- Conditionally visible: Run current workflow -->
|
| 57 |
-
{% if not off_line %}
|
| 58 |
-
<div class="col-lg-6 mb-3 d-flex align-items-stretch">
|
| 59 |
-
<div class="card rounded shadow-sm flex-fill">
|
| 60 |
-
<div class="card-body">
|
| 61 |
-
<h5 class="card-title">
|
| 62 |
-
<i class="bi bi-play-circle me-2"></i>Run current workflow
|
| 63 |
-
</h5>
|
| 64 |
-
<p class="card-text">Execute workflows with configurable parameters.</p>
|
| 65 |
-
<a href="{{ url_for('design.experiment_run') }}" class="stretched-link"></a>
|
| 66 |
-
</div>
|
| 67 |
-
</div>
|
| 68 |
-
</div>
|
| 69 |
-
{% endif %}
|
| 70 |
-
</div>
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
{% if not off_line %}
|
| 75 |
-
<!-- Direct Control Section -->
|
| 76 |
-
<h4 class="mt-5 mb-3">Direct Control</h4>
|
| 77 |
-
<div class="row">
|
| 78 |
-
<div class="col-lg-6 mb-3 d-flex align-items-stretch">
|
| 79 |
-
<div class="card rounded shadow-sm flex-fill">
|
| 80 |
-
<div class="card-body">
|
| 81 |
-
<h5 class="card-title">
|
| 82 |
-
<i class="bi bi-toggle-on me-2"></i>Direct control
|
| 83 |
-
</h5>
|
| 84 |
-
<p class="card-text">Manually control individual components.</p>
|
| 85 |
-
<a href="{{ url_for('control.deck_controllers') }}" class="stretched-link"></a>
|
| 86 |
-
</div>
|
| 87 |
-
</div>
|
| 88 |
-
</div>
|
| 89 |
-
<div class="col-lg-6 mb-3 d-flex align-items-stretch">
|
| 90 |
-
<div class="card rounded shadow-sm flex-fill">
|
| 91 |
-
<div class="card-body">
|
| 92 |
-
<h5 class="card-title">
|
| 93 |
-
<i class="bi bi-usb-plug me-2"></i>Connect a new device
|
| 94 |
-
</h5>
|
| 95 |
-
<p class="card-text">Add new hardware temporarily or for testing purposes.</p>
|
| 96 |
-
<a href="{{ url_for('control.controllers_home') }}" class="stretched-link"></a>
|
| 97 |
-
</div>
|
| 98 |
-
</div>
|
| 99 |
-
</div>
|
| 100 |
-
</div>
|
| 101 |
-
{% endif %}
|
| 102 |
-
</div>
|
| 103 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/static/favicon.ico
DELETED
|
Binary file (15.4 kB)
|
|
|
ivoryos/static/js/overlay.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
function addOverlayToButtons(buttonIds) {
|
| 2 |
-
buttonIds.forEach(function(buttonId) {
|
| 3 |
-
document.getElementById(buttonId).addEventListener('submit', function() {
|
| 4 |
-
// Display the overlay
|
| 5 |
-
document.getElementById('overlay').style.display = 'block';
|
| 6 |
-
document.getElementById('overlay-text').innerText = `Processing ${buttonId}...`;
|
| 7 |
-
});
|
| 8 |
-
});
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
// buttonIds should be set dynamically in your HTML template
|
| 12 |
-
addOverlayToButtons(buttonIds);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/static/js/socket_handler.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
| 1 |
-
document.addEventListener("DOMContentLoaded", function() {
|
| 2 |
-
var socket = io.connect('http://' + document.domain + ':' + location.port);
|
| 3 |
-
socket.on('connect', function() {
|
| 4 |
-
console.log('Connected');
|
| 5 |
-
});
|
| 6 |
-
socket.on('progress', function(data) {
|
| 7 |
-
var progress = data.progress;
|
| 8 |
-
console.log(progress);
|
| 9 |
-
// Update the progress bar's width and appearance
|
| 10 |
-
var progressBar = document.getElementById('progress-bar-inner');
|
| 11 |
-
progressBar.style.width = progress + '%';
|
| 12 |
-
progressBar.setAttribute('aria-valuenow', progress);
|
| 13 |
-
const runPanel = document.getElementById("run-panel");
|
| 14 |
-
const codePanel = document.getElementById("code-panel");
|
| 15 |
-
if (progress === 1) {
|
| 16 |
-
if (runPanel) runPanel.style.display = "none";
|
| 17 |
-
if (codePanel) {
|
| 18 |
-
codePanel.style.display = "block";
|
| 19 |
-
codePanel.scrollIntoView({ behavior: "smooth" });
|
| 20 |
-
}
|
| 21 |
-
progressBar.classList.remove('bg-success');
|
| 22 |
-
progressBar.classList.remove('bg-danger');
|
| 23 |
-
progressBar.classList.add('progress-bar-animated');
|
| 24 |
-
}
|
| 25 |
-
if (progress === 100) {
|
| 26 |
-
// Remove animation and set green color when 100% is reached
|
| 27 |
-
progressBar.classList.remove('progress-bar-animated');
|
| 28 |
-
progressBar.classList.add('bg-success'); // Bootstrap class for green color
|
| 29 |
-
setTimeout(() => {
|
| 30 |
-
if (runPanel) runPanel.style.display = "block";
|
| 31 |
-
if (codePanel) codePanel.style.display = "none";
|
| 32 |
-
}, 1000); // Small delay to let users see the completion
|
| 33 |
-
}
|
| 34 |
-
});
|
| 35 |
-
|
| 36 |
-
socket.on('error', function(errorData) {
|
| 37 |
-
console.error("Error received:", errorData);
|
| 38 |
-
var progressBar = document.getElementById('progress-bar-inner');
|
| 39 |
-
|
| 40 |
-
progressBar.classList.remove('bg-success');
|
| 41 |
-
progressBar.classList.add('bg-danger'); // Red color for error
|
| 42 |
-
// Show error modal
|
| 43 |
-
var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
|
| 44 |
-
document.getElementById('error-message').innerText = "An error occurred: " + errorData.message;
|
| 45 |
-
errorModal.show();
|
| 46 |
-
|
| 47 |
-
});
|
| 48 |
-
|
| 49 |
-
// Handle Pause/Resume Button
|
| 50 |
-
document.getElementById('pause-resume').addEventListener('click', function() {
|
| 51 |
-
socket.emit('pause');
|
| 52 |
-
console.log('Pause/Resume is toggled.');
|
| 53 |
-
var button = this;
|
| 54 |
-
var icon = button.querySelector("i");
|
| 55 |
-
|
| 56 |
-
// Toggle Pause and Resume
|
| 57 |
-
if (icon.classList.contains("bi-pause-circle")) {
|
| 58 |
-
icon.classList.remove("bi-pause-circle");
|
| 59 |
-
icon.classList.add("bi-play-circle");
|
| 60 |
-
button.innerHTML = '<i class="bi bi-play-circle"></i>';
|
| 61 |
-
button.setAttribute("title", "Resume execution");
|
| 62 |
-
} else {
|
| 63 |
-
icon.classList.remove("bi-play-circle");
|
| 64 |
-
icon.classList.add("bi-pause-circle");
|
| 65 |
-
button.innerHTML = '<i class="bi bi-pause-circle"></i>';
|
| 66 |
-
button.setAttribute("title", "Pause execution");
|
| 67 |
-
}
|
| 68 |
-
});
|
| 69 |
-
|
| 70 |
-
// Handle Modal Buttons
|
| 71 |
-
document.getElementById('continue-btn').addEventListener('click', function() {
|
| 72 |
-
socket.emit('pause'); // Resume execution
|
| 73 |
-
console.log("Execution resumed.");
|
| 74 |
-
});
|
| 75 |
-
|
| 76 |
-
document.getElementById('retry-btn').addEventListener('click', function() {
|
| 77 |
-
socket.emit('retry'); // Resume execution
|
| 78 |
-
console.log("Execution resumed, retrying.");
|
| 79 |
-
});
|
| 80 |
-
|
| 81 |
-
document.getElementById('stop-btn').addEventListener('click', function() {
|
| 82 |
-
socket.emit('pause'); // Resume execution
|
| 83 |
-
socket.emit('abort_current'); // Stop execution
|
| 84 |
-
console.log("Execution stopped.");
|
| 85 |
-
|
| 86 |
-
// Reset UI back to initial state
|
| 87 |
-
document.getElementById("code-panel").style.display = "none";
|
| 88 |
-
document.getElementById("run-panel").style.display = "block";
|
| 89 |
-
});
|
| 90 |
-
|
| 91 |
-
socket.on('log', function(data) {
|
| 92 |
-
var logMessage = data.message;
|
| 93 |
-
console.log(logMessage);
|
| 94 |
-
$('#logging-panel').append(logMessage + "<br>");
|
| 95 |
-
$('#logging-panel').scrollTop($('#logging-panel')[0].scrollHeight);
|
| 96 |
-
});
|
| 97 |
-
|
| 98 |
-
document.getElementById('abort-pending').addEventListener('click', function() {
|
| 99 |
-
var confirmation = confirm("Are you sure you want to stop after this iteration?");
|
| 100 |
-
if (confirmation) {
|
| 101 |
-
socket.emit('abort_pending');
|
| 102 |
-
console.log('Abort action sent to server.');
|
| 103 |
-
}
|
| 104 |
-
});
|
| 105 |
-
document.getElementById('abort-current').addEventListener('click', function() {
|
| 106 |
-
var confirmation = confirm("Are you sure you want to stop after this step?");
|
| 107 |
-
if (confirmation) {
|
| 108 |
-
socket.emit('abort_current');
|
| 109 |
-
console.log('Stop action sent to server.');
|
| 110 |
-
}
|
| 111 |
-
});
|
| 112 |
-
|
| 113 |
-
socket.on('execution', function(data) {
|
| 114 |
-
// Remove highlighting from all lines
|
| 115 |
-
document.querySelectorAll('pre code').forEach(el => el.style.backgroundColor = '');
|
| 116 |
-
|
| 117 |
-
// Get the currently executing line and highlight it
|
| 118 |
-
let executingLine = document.getElementById(data.section);
|
| 119 |
-
if (executingLine) {
|
| 120 |
-
executingLine.style.backgroundColor = '#cce5ff'; // Highlight
|
| 121 |
-
executingLine.style.transition = 'background-color 0.3s ease-in-out';
|
| 122 |
-
|
| 123 |
-
}
|
| 124 |
-
});
|
| 125 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/static/js/sortable_card.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
$(function() {
|
| 2 |
-
$("#sortable-grid").sortable({
|
| 3 |
-
items: ".card",
|
| 4 |
-
cursor: "move",
|
| 5 |
-
opacity: 0.7,
|
| 6 |
-
revert: true,
|
| 7 |
-
placeholder: "ui-state-highlight",
|
| 8 |
-
start: function(event, ui) {
|
| 9 |
-
ui.placeholder.height(ui.item.height());
|
| 10 |
-
},
|
| 11 |
-
update: function() {
|
| 12 |
-
const newOrder = $("#sortable-grid").sortable("toArray");
|
| 13 |
-
$.ajax({
|
| 14 |
-
url: saveOrderUrl, // saveOrderUrl should be set dynamically in your HTML template
|
| 15 |
-
method: 'POST',
|
| 16 |
-
contentType: 'application/json',
|
| 17 |
-
data: JSON.stringify({ order: newOrder }),
|
| 18 |
-
success: function() {
|
| 19 |
-
console.log('Order saved');
|
| 20 |
-
}
|
| 21 |
-
});
|
| 22 |
-
}
|
| 23 |
-
}).disableSelection();
|
| 24 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/static/js/sortable_design.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
$(document).ready(function () {
|
| 2 |
-
let dropTargetId = ""; // Store the ID of the drop target
|
| 3 |
-
|
| 4 |
-
$("#list ul").sortable({
|
| 5 |
-
cancel: ".unsortable",
|
| 6 |
-
opacity: 0.8,
|
| 7 |
-
cursor: "move",
|
| 8 |
-
placeholder: "drop-placeholder",
|
| 9 |
-
update: function () {
|
| 10 |
-
var item_order = [];
|
| 11 |
-
$("ul.reorder li").each(function () {
|
| 12 |
-
item_order.push($(this).attr("id"));
|
| 13 |
-
});
|
| 14 |
-
var order_string = "order=" + item_order.join(",");
|
| 15 |
-
|
| 16 |
-
$.ajax({
|
| 17 |
-
method: "POST",
|
| 18 |
-
url: updateListUrl,
|
| 19 |
-
data: order_string,
|
| 20 |
-
cache: false,
|
| 21 |
-
success: function (data) {
|
| 22 |
-
$("#response").html(data);
|
| 23 |
-
$("#response").slideDown("slow");
|
| 24 |
-
window.location.href = window.location.href;
|
| 25 |
-
}
|
| 26 |
-
});
|
| 27 |
-
}
|
| 28 |
-
});
|
| 29 |
-
|
| 30 |
-
// Make Entire Accordion Item Draggable
|
| 31 |
-
$(".accordion-item").on("dragstart", function (event) {
|
| 32 |
-
let formHtml = $(this).find(".accordion-body").html(); // Get the correct form
|
| 33 |
-
event.originalEvent.dataTransfer.setData("form", formHtml || ""); // Store form HTML
|
| 34 |
-
event.originalEvent.dataTransfer.setData("action", $(this).find(".draggable-action").data("action"));
|
| 35 |
-
event.originalEvent.dataTransfer.setData("id", $(this).find(".draggable-action").attr("id"));
|
| 36 |
-
|
| 37 |
-
$(this).addClass("dragging");
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
$("#list ul, .canvas").on("dragover", function (event) {
|
| 42 |
-
event.preventDefault();
|
| 43 |
-
let $target = $(event.target).closest("li");
|
| 44 |
-
|
| 45 |
-
// If we're over a valid <li> element in the list
|
| 46 |
-
if ($target.length) {
|
| 47 |
-
dropTargetId = $target.attr("id") || ""; // Store the drop target ID
|
| 48 |
-
|
| 49 |
-
$(".drop-placeholder").remove(); // Remove existing placeholders
|
| 50 |
-
$("<li class='drop-placeholder'></li>").insertBefore($target); // Insert before the target element
|
| 51 |
-
} else if (!$("#list ul").children().length && $(this).hasClass("canvas")) {
|
| 52 |
-
$(".drop-placeholder").remove(); // Remove any placeholder
|
| 53 |
-
// $("#list ul").append("<li class='drop-placeholder'></li>"); // Append placeholder to canvas
|
| 54 |
-
} else {
|
| 55 |
-
dropTargetId = ""; // Append placeholder to canvas
|
| 56 |
-
}
|
| 57 |
-
});
|
| 58 |
-
|
| 59 |
-
$("#list ul, .canvas").on("dragleave", function () {
|
| 60 |
-
$(".drop-placeholder").remove(); // Remove placeholder on leave
|
| 61 |
-
});
|
| 62 |
-
|
| 63 |
-
$("#list ul, .canvas").on("drop", function (event) {
|
| 64 |
-
event.preventDefault();
|
| 65 |
-
|
| 66 |
-
var actionName = event.originalEvent.dataTransfer.getData("action");
|
| 67 |
-
var actionId = event.originalEvent.dataTransfer.getData("id");
|
| 68 |
-
var formHtml = event.originalEvent.dataTransfer.getData("form"); // Retrieve form HTML
|
| 69 |
-
let listLength = $("ul.reorder li").length;
|
| 70 |
-
dropTargetId = dropTargetId || listLength + 1; // Assign a "last" ID or unique identifier
|
| 71 |
-
$(".drop-placeholder").remove();
|
| 72 |
-
// Trigger the modal with the appropriate action
|
| 73 |
-
triggerModal(formHtml, actionName, actionId, dropTargetId);
|
| 74 |
-
|
| 75 |
-
});
|
| 76 |
-
|
| 77 |
-
// Function to trigger the modal (same for both buttons and accordion items)
|
| 78 |
-
function triggerModal(formHtml, actionName, actionId, dropTargetId) {
|
| 79 |
-
if (formHtml && formHtml.trim() !== "") {
|
| 80 |
-
var $form = $("<div>").html(formHtml); // Convert HTML string to jQuery object
|
| 81 |
-
|
| 82 |
-
// Create a hidden input for the drop target ID
|
| 83 |
-
var $hiddenInput = $("<input>")
|
| 84 |
-
.attr("type", "hidden")
|
| 85 |
-
.attr("name", "drop_target_id")
|
| 86 |
-
.attr("id", "dropTargetInput")
|
| 87 |
-
.val(dropTargetId);
|
| 88 |
-
|
| 89 |
-
// Insert before the submit button
|
| 90 |
-
$form.find("button[type='submit']").before($hiddenInput);
|
| 91 |
-
|
| 92 |
-
$("#modalFormFields").empty().append($form.children());
|
| 93 |
-
$("#dropModal").modal("show"); // Show modal
|
| 94 |
-
|
| 95 |
-
// Store and display drop target ID in the modal
|
| 96 |
-
$("#modalDropTarget").text(dropTargetId || "N/A");
|
| 97 |
-
|
| 98 |
-
$("#modalFormFields").data("action-id", actionId);
|
| 99 |
-
$("#modalFormFields").data("action-name", actionName);
|
| 100 |
-
$("#modalFormFields").data("drop-target-id", dropTargetId);
|
| 101 |
-
} else {
|
| 102 |
-
console.error("Form HTML is undefined or empty!");
|
| 103 |
-
}
|
| 104 |
-
}
|
| 105 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/static/logo.webp
DELETED
|
Binary file (15 kB)
|
|
|
ivoryos/static/style.css
DELETED
|
@@ -1,211 +0,0 @@
|
|
| 1 |
-
.login {
|
| 2 |
-
width: 500px;
|
| 3 |
-
padding: 8% 0 0;
|
| 4 |
-
margin: auto;
|
| 5 |
-
align-content: center;
|
| 6 |
-
}
|
| 7 |
-
.card{
|
| 8 |
-
display: flex;
|
| 9 |
-
justify-content: flex-end;
|
| 10 |
-
cursor: move;
|
| 11 |
-
border: none;
|
| 12 |
-
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 13 |
-
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
.card a {
|
| 17 |
-
text-decoration: none;
|
| 18 |
-
}
|
| 19 |
-
.card-title {
|
| 20 |
-
color: #007bff; /* Bootstrap primary color */
|
| 21 |
-
}
|
| 22 |
-
.grid-container {
|
| 23 |
-
display: grid;
|
| 24 |
-
grid-template-columns: repeat(auto-fit, minmax(350px, 400px));
|
| 25 |
-
gap: 25px;
|
| 26 |
-
padding: 10px;
|
| 27 |
-
}
|
| 28 |
-
.navbar-nav {
|
| 29 |
-
font-size: larger;
|
| 30 |
-
}
|
| 31 |
-
.navbar-nav li{
|
| 32 |
-
margin-right:10px;
|
| 33 |
-
margin-left:10px;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
.canvas {
|
| 37 |
-
height: 70vh;
|
| 38 |
-
overflow: hidden;
|
| 39 |
-
overflow-y: scroll;
|
| 40 |
-
background: #e8e8cd;
|
| 41 |
-
background-size: 20px 20px;
|
| 42 |
-
background-image: radial-gradient(circle, cadetblue 1px, rgba(0, 0, 0, 0) 1px);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
.canvas content{
|
| 46 |
-
margin-top: 10px;
|
| 47 |
-
}
|
| 48 |
-
body {
|
| 49 |
-
padding-top: 100px;
|
| 50 |
-
background-color: #f8f9fa; /* Light grey background */
|
| 51 |
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 52 |
-
}
|
| 53 |
-
/*.run-panel-row-height {*/
|
| 54 |
-
/* height: 80vh;*/
|
| 55 |
-
/*}*/
|
| 56 |
-
pre {
|
| 57 |
-
tab-size: 4;
|
| 58 |
-
}
|
| 59 |
-
.scroll-column{
|
| 60 |
-
height: 90vh;
|
| 61 |
-
overflow: hidden;
|
| 62 |
-
overflow-y: scroll;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
label {
|
| 66 |
-
display:block
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
.scroll {
|
| 70 |
-
overflow: auto;
|
| 71 |
-
-webkit-overflow-scrolling: touch; /* enables momentum-scrolling on iOS */
|
| 72 |
-
position: absolute;
|
| 73 |
-
top: 0;
|
| 74 |
-
left: 0;
|
| 75 |
-
right: 0;
|
| 76 |
-
bottom: 0;
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
/*Remove the scrollbar from Chrome, Safari, Edge and IE*/
|
| 81 |
-
::-webkit-scrollbar {
|
| 82 |
-
background: transparent;
|
| 83 |
-
}
|
| 84 |
-
|
| 85 |
-
* {
|
| 86 |
-
-ms-overflow-style: none !important;
|
| 87 |
-
}
|
| 88 |
-
.script-table{
|
| 89 |
-
background: white;
|
| 90 |
-
width: fit-content;
|
| 91 |
-
}
|
| 92 |
-
.script-table td {
|
| 93 |
-
border-bottom: none;
|
| 94 |
-
border-top: cadetblue;
|
| 95 |
-
}
|
| 96 |
-
.script-table th{
|
| 97 |
-
border-top: cadetblue;
|
| 98 |
-
border-bottom: none;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
.bottom-button {
|
| 102 |
-
position: absolute;
|
| 103 |
-
bottom: 20px;
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
hr.vertical {
|
| 107 |
-
width: 5px;
|
| 108 |
-
height: 100%;
|
| 109 |
-
display: inline-block;
|
| 110 |
-
/* or height in PX */
|
| 111 |
-
}
|
| 112 |
-
.list-group a {
|
| 113 |
-
text-align: left;
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
.tray input[type="checkbox"]:checked + label{
|
| 117 |
-
background: radial-gradient(circle, white 40%, midnightblue, dodgerblue 43%);
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
.btn-vial{
|
| 121 |
-
height: 50px;
|
| 122 |
-
width: 50px;
|
| 123 |
-
border-radius: 50%;
|
| 124 |
-
background: lightgray;
|
| 125 |
-
margin-right: 10px;
|
| 126 |
-
margin-top: 10px;
|
| 127 |
-
/*text-align: center;*/
|
| 128 |
-
|
| 129 |
-
}
|
| 130 |
-
|
| 131 |
-
.tray {
|
| 132 |
-
width: 70px;
|
| 133 |
-
height: 70px;
|
| 134 |
-
display: inline-block;
|
| 135 |
-
background: darkgrey;
|
| 136 |
-
/*text-align:center;*/
|
| 137 |
-
vertical-align: middle;
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
.disabled-link {
|
| 141 |
-
pointer-events: none;
|
| 142 |
-
color: currentColor;
|
| 143 |
-
cursor: not-allowed;
|
| 144 |
-
opacity: 0.5;
|
| 145 |
-
text-decoration: none;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
.controller-card a {
|
| 150 |
-
text-decoration: none;
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
#logging-panel {
|
| 154 |
-
flex-grow: 1;
|
| 155 |
-
height: 50vh;
|
| 156 |
-
overflow-y: auto;
|
| 157 |
-
background-color: #f5f5f5;
|
| 158 |
-
padding: 10px;
|
| 159 |
-
}
|
| 160 |
-
|
| 161 |
-
.dropdown:hover .dropdown-menu {
|
| 162 |
-
display: block;
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
#reorder {
|
| 166 |
-
overflow-y: scroll;
|
| 167 |
-
-webkit-overflow-scrolling: touch;
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
.accordion-item.design-control .accordion-button.collapsed {
|
| 171 |
-
background-color:#c1f2f1 !important;
|
| 172 |
-
}
|
| 173 |
-
.accordion-item.design-control .accordion-button {
|
| 174 |
-
background-color:#b3dad9 !important;
|
| 175 |
-
}
|
| 176 |
-
.accordion-item.design-control .accordion-button:hover {
|
| 177 |
-
background-color:#b3dad9 !important;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
.accordion-item.text-to-code .accordion-button.collapsed {
|
| 181 |
-
background-color: #cdc1f2 !important;
|
| 182 |
-
}
|
| 183 |
-
.accordion-item.text-to-code .accordion-button {
|
| 184 |
-
background-color: #cdc1f2 !important;
|
| 185 |
-
}
|
| 186 |
-
.accordion-item.text-to-code .accordion-button:hover {
|
| 187 |
-
background-color: #b8afdc !important;
|
| 188 |
-
}
|
| 189 |
-
.overlay {
|
| 190 |
-
position: fixed;
|
| 191 |
-
top: 0;
|
| 192 |
-
left: 0;
|
| 193 |
-
width: 100%;
|
| 194 |
-
height: 100%;
|
| 195 |
-
background-color: rgba(0, 0, 0, 0.5);
|
| 196 |
-
display: none;
|
| 197 |
-
z-index: 1000;
|
| 198 |
-
text-align: center;
|
| 199 |
-
color: white;
|
| 200 |
-
font-size: 24px;
|
| 201 |
-
padding-top: 20%;
|
| 202 |
-
}
|
| 203 |
-
.drop-placeholder {
|
| 204 |
-
height: 2px !important; /* Keep it very thin */
|
| 205 |
-
min-height: 2px !important;
|
| 206 |
-
margin: 0 !important;
|
| 207 |
-
padding: 0 !important;
|
| 208 |
-
background: rgba(0, 0, 0, 0.2); /* Slight visibility */
|
| 209 |
-
border-radius: 2px;
|
| 210 |
-
list-style: none; /* Remove any default list styling */
|
| 211 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/templates/base.html
DELETED
|
@@ -1,157 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
-
<title>{% block title %}{% endblock %}</title>
|
| 7 |
-
{#bootstrap#}
|
| 8 |
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css" integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
|
| 9 |
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
| 10 |
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
| 11 |
-
{#static#}
|
| 12 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
| 13 |
-
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
|
| 14 |
-
{#for python code displaying#}
|
| 15 |
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
|
| 16 |
-
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js" integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" crossorigin="anonymous"></script>
|
| 17 |
-
<script>hljs.highlightAll();</script>
|
| 18 |
-
{#drag design#}
|
| 19 |
-
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
|
| 20 |
-
{#drag card#}
|
| 21 |
-
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
|
| 22 |
-
</head>
|
| 23 |
-
<body>
|
| 24 |
-
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
| 25 |
-
<div class= "container">
|
| 26 |
-
{# {{ module_config }}#}
|
| 27 |
-
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
| 28 |
-
<img src="{{url_for('static', filename='logo.webp')}}" alt="Logo" height="60" class="d-inline-block align-text-bottom">
|
| 29 |
-
</a>
|
| 30 |
-
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
| 31 |
-
<span class="navbar-toggler-icon"></span>
|
| 32 |
-
</button>
|
| 33 |
-
|
| 34 |
-
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
| 35 |
-
<ul class="navbar-nav mr-auto">
|
| 36 |
-
<li class="nav-item">
|
| 37 |
-
<a class="nav-link" href="{{ url_for('main.index') }}" aria-current="page">Home</a>
|
| 38 |
-
</li>
|
| 39 |
-
{% if enable_design %}
|
| 40 |
-
<li class="nav-item">
|
| 41 |
-
<a class="nav-link" href="{{ url_for('database.load_from_database') }}" aria-current="page">Library</a>
|
| 42 |
-
</li>
|
| 43 |
-
<li class="nav-item">
|
| 44 |
-
<a class="nav-link" href="{{ url_for('design.experiment_builder') }}">Design</a>
|
| 45 |
-
</li>
|
| 46 |
-
<li class="nav-item">
|
| 47 |
-
<a class="nav-link" href="{{ url_for('design.experiment_run') }}">Compile/Run</a>
|
| 48 |
-
</li>
|
| 49 |
-
<li class="nav-item">
|
| 50 |
-
<a class="nav-link" href="{{ url_for('database.list_workflows') }}">Data</a>
|
| 51 |
-
</li>
|
| 52 |
-
{% endif %}
|
| 53 |
-
|
| 54 |
-
<li class="nav-item">
|
| 55 |
-
<a class="nav-link" href="{{ url_for('control.deck_controllers') }}">Devices</a></li>
|
| 56 |
-
</li>
|
| 57 |
-
{# <li class="nav-item">#}
|
| 58 |
-
{# <a class="nav-link" href="{{ url_for('control.controllers_home') }}">Temp Devices</a></li>#}
|
| 59 |
-
{# </li>#}
|
| 60 |
-
{# <li class="nav-item">#}
|
| 61 |
-
{# <a class="nav-link" href="{{ url_for('main.help_info') }}">About</a>#}
|
| 62 |
-
{# </li>#}
|
| 63 |
-
{% if plugins %}
|
| 64 |
-
{% for plugin in plugins %}
|
| 65 |
-
<li class="nav-item">
|
| 66 |
-
<a class="nav-link" href="{{ url_for(plugin+'.main') }}">{{ plugin.capitalize() }}</a></li>
|
| 67 |
-
</li>
|
| 68 |
-
{% endfor %}
|
| 69 |
-
{% endif %}
|
| 70 |
-
</ul>
|
| 71 |
-
<ul class="navbar-nav ms-auto">
|
| 72 |
-
|
| 73 |
-
{% if session["user"] %}
|
| 74 |
-
<div class="dropdown">
|
| 75 |
-
<li class="nav-item " aria-expanded="false"><i class="bi bi-person-circle"></i> {{ session["user"] }}</li>
|
| 76 |
-
<ul class="dropdown-menu">
|
| 77 |
-
<li><a class="dropdown-item" href="{{ url_for("auth.logout") }}" role="button" aria-expanded="false">Logout</a></li>
|
| 78 |
-
</ul>
|
| 79 |
-
|
| 80 |
-
</div>
|
| 81 |
-
{% else %}
|
| 82 |
-
<li class="nav-item">
|
| 83 |
-
<a class="nav-link" href="{{ url_for("auth.login") }}">Login</a>
|
| 84 |
-
</li>
|
| 85 |
-
{% endif %}
|
| 86 |
-
{# <li class="nav-item">#}
|
| 87 |
-
{# <a class="nav-link"href="{{ url_for("signup") }}">Signup</a>#}
|
| 88 |
-
{# </li>#}
|
| 89 |
-
</ul>
|
| 90 |
-
</div>
|
| 91 |
-
</div>
|
| 92 |
-
</nav>
|
| 93 |
-
|
| 94 |
-
<div class= "container">
|
| 95 |
-
<div class="flash">
|
| 96 |
-
{% with messages = get_flashed_messages() %}
|
| 97 |
-
{% if messages %}
|
| 98 |
-
<div class="alert alert-warning">
|
| 99 |
-
Message:
|
| 100 |
-
{% for message in messages %}
|
| 101 |
-
<div >
|
| 102 |
-
{{ message|safe }}
|
| 103 |
-
</div>
|
| 104 |
-
{% endfor %}
|
| 105 |
-
</div>
|
| 106 |
-
{% endif %}
|
| 107 |
-
{% endwith %}
|
| 108 |
-
</div>
|
| 109 |
-
{% block body %}{% endblock %}
|
| 110 |
-
</div>
|
| 111 |
-
|
| 112 |
-
<div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="importModal" aria-hidden="true" >
|
| 113 |
-
<div class="modal-dialog">
|
| 114 |
-
<div class="modal-content">
|
| 115 |
-
<div class="modal-header">
|
| 116 |
-
<h1 class="modal-title fs-5" id="importModal">Import deck by file path</h1>
|
| 117 |
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
| 118 |
-
</div>
|
| 119 |
-
<form method="POST" action="{{ url_for('control.import_deck') }}" enctype="multipart/form-data">
|
| 120 |
-
<div class="modal-body">
|
| 121 |
-
<h5>from connection history</h5>
|
| 122 |
-
<div class="form-group">
|
| 123 |
-
<select class="form-select" name="filepath">
|
| 124 |
-
<option disabled selected value> -- select an option -- </option>
|
| 125 |
-
{% for connection in history %}
|
| 126 |
-
<option style="overflow-wrap: break-word;" name="filepath" id="filepath" value="{{connection}}">{{connection}}</option>
|
| 127 |
-
{% endfor %}
|
| 128 |
-
{# <option>clear history</option>#}
|
| 129 |
-
</select>
|
| 130 |
-
</div>
|
| 131 |
-
<h5>input manually</h5>
|
| 132 |
-
<div class="input-group mb-3">
|
| 133 |
-
<label class="input-group-text" for="filepath">File Path:</label>
|
| 134 |
-
<input type="text" class="form-control" name="filepath" id="filepath">
|
| 135 |
-
</div>
|
| 136 |
-
<div class="input-group mb-3">
|
| 137 |
-
<div class="form-check">
|
| 138 |
-
<input type="checkbox" class="form-check-input" id="update" name="update" value="update">
|
| 139 |
-
<label class="form-check-label" for="update">Update editor config</label>
|
| 140 |
-
</div>
|
| 141 |
-
</div>
|
| 142 |
-
|
| 143 |
-
<div class="modal-footer">
|
| 144 |
-
<div class="form-check">
|
| 145 |
-
<input type="checkbox" class="form-check-input" id="dismiss" name="dismiss" value="dismiss">
|
| 146 |
-
<label class="form-check-label" for="dismiss">Don't remind me</label>
|
| 147 |
-
</div>
|
| 148 |
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
|
| 149 |
-
<button type="submit" class="btn btn-primary"> Save </button>
|
| 150 |
-
</div>
|
| 151 |
-
</div>
|
| 152 |
-
</form>
|
| 153 |
-
</div>
|
| 154 |
-
</div>
|
| 155 |
-
</div>
|
| 156 |
-
</body>
|
| 157 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/__init__.py
DELETED
|
File without changes
|
ivoryos/utils/bo_campaign.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
from ivoryos.utils.utils import install_and_import
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
def ax_init_form(data, arg_types):
|
| 5 |
-
"""
|
| 6 |
-
create Ax campaign from the web form input
|
| 7 |
-
:param data:
|
| 8 |
-
"""
|
| 9 |
-
install_and_import("ax", "ax-platform")
|
| 10 |
-
parameter, objectives = ax_wrapper(data, arg_types)
|
| 11 |
-
from ax.service.ax_client import AxClient
|
| 12 |
-
ax_client = AxClient()
|
| 13 |
-
ax_client.create_experiment(parameter, objectives=objectives)
|
| 14 |
-
return ax_client
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def ax_wrapper(data: dict, arg_types: list):
|
| 18 |
-
"""
|
| 19 |
-
Ax platform wrapper function for creating optimization campaign parameters and objective from the web form input
|
| 20 |
-
:param data: e.g.,
|
| 21 |
-
{
|
| 22 |
-
"param_1_type": "range", "param_1_value": [1,2],
|
| 23 |
-
"param_2_type": "range", "param_2_value": [1,2],
|
| 24 |
-
"obj_1_min": True,
|
| 25 |
-
"obj_2_min": True
|
| 26 |
-
}
|
| 27 |
-
:return: the optimization campaign parameters
|
| 28 |
-
parameter=[
|
| 29 |
-
{"name": "param_1", "type": "range", "bounds": [1,2]},
|
| 30 |
-
{"name": "param_1", "type": "range", "bounds": [1,2]}
|
| 31 |
-
]
|
| 32 |
-
objectives=[
|
| 33 |
-
{"name": "obj_1", "min": True, "threshold": None},
|
| 34 |
-
{"name": "obj_2", "min": True, "threshold": None},
|
| 35 |
-
]
|
| 36 |
-
"""
|
| 37 |
-
from ax.service.utils.instantiation import ObjectiveProperties
|
| 38 |
-
parameter = []
|
| 39 |
-
objectives = {}
|
| 40 |
-
# Iterate through the webui_data dictionary
|
| 41 |
-
for key, value in data.items():
|
| 42 |
-
# Check if the key corresponds to a parameter type
|
| 43 |
-
if "_type" in key:
|
| 44 |
-
param_name = key.split("_type")[0]
|
| 45 |
-
param_type = value
|
| 46 |
-
param_value = data[f"{param_name}_value"].split(",")
|
| 47 |
-
try:
|
| 48 |
-
values = [float(v) for v in param_value]
|
| 49 |
-
except Exception:
|
| 50 |
-
values = param_value
|
| 51 |
-
if param_type == "range":
|
| 52 |
-
param = {"name": param_name, "type": param_type, "bounds": values}
|
| 53 |
-
if param_type == "choice":
|
| 54 |
-
param = {"name": param_name, "type": param_type, "values": values}
|
| 55 |
-
if param_type == "fixed":
|
| 56 |
-
param = {"name": param_name, "type": param_type, "value": values[0]}
|
| 57 |
-
_type = arg_types[param_name] if arg_types[param_name] in ["str", "bool", "int"] else "float"
|
| 58 |
-
param.update({"value_type": _type})
|
| 59 |
-
parameter.append(param)
|
| 60 |
-
elif key.endswith("_min"):
|
| 61 |
-
if not value == 'none':
|
| 62 |
-
obj_name = key.split("_min")[0]
|
| 63 |
-
is_min = True if value == "minimize" else False
|
| 64 |
-
|
| 65 |
-
threshold = None if f"{obj_name}_threshold" not in data else data[f"{obj_name}_threshold"]
|
| 66 |
-
properties = ObjectiveProperties(minimize=is_min)
|
| 67 |
-
objectives[obj_name] = properties
|
| 68 |
-
|
| 69 |
-
return parameter, objectives
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def ax_init_opc(bo_args):
|
| 73 |
-
install_and_import("ax", "ax-platform")
|
| 74 |
-
from ax.service.ax_client import AxClient
|
| 75 |
-
from ax.service.utils.instantiation import ObjectiveProperties
|
| 76 |
-
|
| 77 |
-
ax_client = AxClient()
|
| 78 |
-
objectives = bo_args.get("objectives")
|
| 79 |
-
objectives_formatted = {}
|
| 80 |
-
for obj in objectives:
|
| 81 |
-
obj_name = obj.get("name")
|
| 82 |
-
minimize = obj.get("minimize")
|
| 83 |
-
objectives_formatted[obj_name] = ObjectiveProperties(minimize=minimize)
|
| 84 |
-
bo_args["objectives"] = objectives_formatted
|
| 85 |
-
ax_client.create_experiment(**bo_args)
|
| 86 |
-
|
| 87 |
-
return ax_client
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/client_proxy.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
# import argparse
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
# import requests
|
| 5 |
-
|
| 6 |
-
# session = requests.Session()
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
# Function to create class and methods dynamically
|
| 10 |
-
def create_function(url, class_name, functions):
|
| 11 |
-
class_template = f'class {class_name.capitalize()}:\n url = "{url}ivoryos/api/control/deck.{class_name}"\n'
|
| 12 |
-
|
| 13 |
-
for function_name, details in functions.items():
|
| 14 |
-
signature = details['signature']
|
| 15 |
-
docstring = details.get('docstring', '')
|
| 16 |
-
|
| 17 |
-
# Creating the function definition
|
| 18 |
-
method = f' def {function_name}{signature}:\n'
|
| 19 |
-
if docstring:
|
| 20 |
-
method += f' """{docstring}"""\n'
|
| 21 |
-
|
| 22 |
-
# Generating the session.post code for sending data
|
| 23 |
-
method += ' return session.post(self.url, data={'
|
| 24 |
-
method += f'"hidden_name": "{function_name}"'
|
| 25 |
-
|
| 26 |
-
# Extracting the parameters from the signature string for the data payload
|
| 27 |
-
param_str = signature[6:-1] # Remove the "(self" and final ")"
|
| 28 |
-
params = [param.strip() for param in param_str.split(',')] if param_str else []
|
| 29 |
-
|
| 30 |
-
for param in params:
|
| 31 |
-
param_name = param.split(':')[0].strip() # Split on ':' and get parameter name
|
| 32 |
-
method += f', "{param_name}": {param_name}'
|
| 33 |
-
|
| 34 |
-
method += '}).json()\n'
|
| 35 |
-
class_template += method + '\n'
|
| 36 |
-
|
| 37 |
-
return class_template
|
| 38 |
-
|
| 39 |
-
# Function to export the generated classes to a Python script
|
| 40 |
-
def export_to_python(class_definitions, path):
|
| 41 |
-
with open(os.path.join(path, "generated_proxy.py"), 'w') as f:
|
| 42 |
-
# Writing the imports at the top of the script
|
| 43 |
-
f.write('import requests\n\n')
|
| 44 |
-
f.write('session = requests.Session()\n\n')
|
| 45 |
-
|
| 46 |
-
# Writing each class definition to the file
|
| 47 |
-
for class_name, class_def in class_definitions.items():
|
| 48 |
-
f.write(class_def)
|
| 49 |
-
f.write('\n')
|
| 50 |
-
|
| 51 |
-
# Creating instances of the dynamically generated classes
|
| 52 |
-
for class_name in class_definitions.keys():
|
| 53 |
-
instance_name = class_name.lower() # Using lowercase for instance names
|
| 54 |
-
f.write(f'{instance_name} = {class_name.capitalize()}()\n')
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/db_models.py
DELETED
|
@@ -1,700 +0,0 @@
|
|
| 1 |
-
import ast
|
| 2 |
-
import builtins
|
| 3 |
-
import json
|
| 4 |
-
import keyword
|
| 5 |
-
import re
|
| 6 |
-
import uuid
|
| 7 |
-
from datetime import datetime
|
| 8 |
-
from typing import Dict
|
| 9 |
-
|
| 10 |
-
from flask_login import UserMixin
|
| 11 |
-
from flask_sqlalchemy import SQLAlchemy
|
| 12 |
-
from sqlalchemy_utils import JSONType
|
| 13 |
-
|
| 14 |
-
db = SQLAlchemy()
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
class User(db.Model, UserMixin):
|
| 18 |
-
__tablename__ = 'user'
|
| 19 |
-
# id = db.Column(db.Integer)
|
| 20 |
-
username = db.Column(db.String(50), primary_key=True, unique=True, nullable=False)
|
| 21 |
-
# email = db.Column(db.String)
|
| 22 |
-
hashPassword = db.Column(db.String(255))
|
| 23 |
-
|
| 24 |
-
# password = db.Column()
|
| 25 |
-
def __init__(self, username, password):
|
| 26 |
-
# self.id = id
|
| 27 |
-
self.username = username
|
| 28 |
-
# self.email = email
|
| 29 |
-
self.hashPassword = password
|
| 30 |
-
|
| 31 |
-
def get_id(self):
|
| 32 |
-
return self.username
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
class Script(db.Model):
|
| 36 |
-
__tablename__ = 'script'
|
| 37 |
-
# id = db.Column(db.Integer, primary_key=True)
|
| 38 |
-
name = db.Column(db.String(50), primary_key=True, unique=True)
|
| 39 |
-
deck = db.Column(db.String(50), nullable=True)
|
| 40 |
-
status = db.Column(db.String(50), nullable=True)
|
| 41 |
-
script_dict = db.Column(JSONType, nullable=True)
|
| 42 |
-
time_created = db.Column(db.String(50), nullable=True)
|
| 43 |
-
last_modified = db.Column(db.String(50), nullable=True)
|
| 44 |
-
id_order = db.Column(JSONType, nullable=True)
|
| 45 |
-
editing_type = db.Column(db.String(50), nullable=True)
|
| 46 |
-
author = db.Column(db.String(50), nullable=False)
|
| 47 |
-
# registered = db.Column(db.Boolean, nullable=True, default=False)
|
| 48 |
-
|
| 49 |
-
def __init__(self, name=None, deck=None, status=None, script_dict: dict = None, id_order: dict = None,
|
| 50 |
-
time_created=None, last_modified=None, editing_type=None, author: str = None,
|
| 51 |
-
# registered:bool=False,
|
| 52 |
-
python_script: str = None
|
| 53 |
-
):
|
| 54 |
-
if script_dict is None:
|
| 55 |
-
script_dict = {"prep": [], "script": [], "cleanup": []}
|
| 56 |
-
elif type(script_dict) is not dict:
|
| 57 |
-
script_dict = json.loads(script_dict)
|
| 58 |
-
if id_order is None:
|
| 59 |
-
id_order = {"prep": [], "script": [], "cleanup": []}
|
| 60 |
-
elif type(id_order) is not dict:
|
| 61 |
-
id_order = json.loads(id_order)
|
| 62 |
-
if status is None:
|
| 63 |
-
status = 'editing'
|
| 64 |
-
if time_created is None:
|
| 65 |
-
time_created = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 66 |
-
if last_modified is None:
|
| 67 |
-
last_modified = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 68 |
-
if editing_type is None:
|
| 69 |
-
editing_type = "script"
|
| 70 |
-
|
| 71 |
-
self.name = name
|
| 72 |
-
self.deck = deck
|
| 73 |
-
self.status = status
|
| 74 |
-
self.script_dict = script_dict
|
| 75 |
-
self.time_created = time_created
|
| 76 |
-
self.last_modified = last_modified
|
| 77 |
-
self.id_order = id_order
|
| 78 |
-
self.editing_type = editing_type
|
| 79 |
-
self.author = author
|
| 80 |
-
self.python_script = python_script
|
| 81 |
-
# self.r = registered
|
| 82 |
-
|
| 83 |
-
def as_dict(self):
|
| 84 |
-
dict = self.__dict__
|
| 85 |
-
dict.pop('_sa_instance_state', None)
|
| 86 |
-
return dict
|
| 87 |
-
|
| 88 |
-
def get(self):
|
| 89 |
-
workflows = db.session.query(Script).all()
|
| 90 |
-
# result = script_schema.dump(workflows)
|
| 91 |
-
return workflows
|
| 92 |
-
|
| 93 |
-
def find_by_uuid(self, uuid):
|
| 94 |
-
for stype in self.script_dict:
|
| 95 |
-
for action in self.script_dict[stype]:
|
| 96 |
-
|
| 97 |
-
if action['uuid'] == int(uuid):
|
| 98 |
-
return action
|
| 99 |
-
|
| 100 |
-
def _convert_type(self, args, arg_types):
|
| 101 |
-
if arg_types in ["list", "tuple", "set"]:
|
| 102 |
-
try:
|
| 103 |
-
args = ast.literal_eval(args)
|
| 104 |
-
return args
|
| 105 |
-
except Exception:
|
| 106 |
-
pass
|
| 107 |
-
if type(arg_types) is not list:
|
| 108 |
-
arg_types = [arg_types]
|
| 109 |
-
for arg_type in arg_types:
|
| 110 |
-
try:
|
| 111 |
-
# print(arg_type)
|
| 112 |
-
args = eval(f"{arg_type}('{args}')")
|
| 113 |
-
return
|
| 114 |
-
except Exception:
|
| 115 |
-
|
| 116 |
-
pass
|
| 117 |
-
raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
|
| 118 |
-
|
| 119 |
-
def update_by_uuid(self, uuid, args, output):
|
| 120 |
-
action = self.find_by_uuid(uuid)
|
| 121 |
-
if not action:
|
| 122 |
-
return
|
| 123 |
-
arg_types = action['arg_types']
|
| 124 |
-
if type(action['args']) is dict:
|
| 125 |
-
# pass
|
| 126 |
-
self.eval_list(args, arg_types)
|
| 127 |
-
else:
|
| 128 |
-
pass
|
| 129 |
-
action['args'] = args
|
| 130 |
-
action['return'] = output
|
| 131 |
-
|
| 132 |
-
@staticmethod
|
| 133 |
-
def eval_list(args, arg_types):
|
| 134 |
-
for arg in args:
|
| 135 |
-
arg_type = arg_types[arg]
|
| 136 |
-
if arg_type in ["list", "tuple", "set"]:
|
| 137 |
-
|
| 138 |
-
if type(arg) is str and not args[arg].startswith("#"):
|
| 139 |
-
# arg_types = arg_types[arg]
|
| 140 |
-
# if arg_types in ["list", "tuple", "set"]:
|
| 141 |
-
convert_type = getattr(builtins, arg_type) # Handle unknown types s
|
| 142 |
-
try:
|
| 143 |
-
output = ast.literal_eval(args[arg])
|
| 144 |
-
if type(output) not in [list, tuple, set]:
|
| 145 |
-
output = [output]
|
| 146 |
-
args[arg] = convert_type(output)
|
| 147 |
-
# return args
|
| 148 |
-
except ValueError:
|
| 149 |
-
_list = ''.join(args[arg]).split(',')
|
| 150 |
-
# convert_type = getattr(builtins, arg_types) # Handle unknown types s
|
| 151 |
-
args[arg] = convert_type([s.strip() for s in _list])
|
| 152 |
-
|
| 153 |
-
@property
|
| 154 |
-
def stypes(self):
|
| 155 |
-
return list(self.script_dict.keys())
|
| 156 |
-
|
| 157 |
-
@property
|
| 158 |
-
def currently_editing_script(self):
|
| 159 |
-
return self.script_dict[self.editing_type]
|
| 160 |
-
|
| 161 |
-
@currently_editing_script.setter
|
| 162 |
-
def currently_editing_script(self, script):
|
| 163 |
-
self.script_dict[self.editing_type] = script
|
| 164 |
-
|
| 165 |
-
@property
|
| 166 |
-
def currently_editing_order(self):
|
| 167 |
-
return self.id_order[self.editing_type]
|
| 168 |
-
|
| 169 |
-
@currently_editing_order.setter
|
| 170 |
-
def currently_editing_order(self, script):
|
| 171 |
-
self.id_order[self.editing_type] = script
|
| 172 |
-
|
| 173 |
-
def update_time_stamp(self):
|
| 174 |
-
self.last_modified = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 175 |
-
|
| 176 |
-
def get_script(self, stype: str):
|
| 177 |
-
return self.script_dict[stype]
|
| 178 |
-
|
| 179 |
-
def isEmpty(self) -> bool:
|
| 180 |
-
if not (self.script_dict['script'] or self.script_dict['prep'] or self.script_dict['cleanup']):
|
| 181 |
-
return True
|
| 182 |
-
return False
|
| 183 |
-
|
| 184 |
-
def _sort(self, script_type):
|
| 185 |
-
if len(self.id_order[script_type]) > 0:
|
| 186 |
-
for action in self.script_dict[script_type]:
|
| 187 |
-
for i in range(len(self.id_order[script_type])):
|
| 188 |
-
if action['id'] == int(self.id_order[script_type][i]):
|
| 189 |
-
# print(i+1)
|
| 190 |
-
action['id'] = i + 1
|
| 191 |
-
break
|
| 192 |
-
self.id_order[script_type].sort()
|
| 193 |
-
if not int(self.id_order[script_type][-1]) == len(self.script_dict[script_type]):
|
| 194 |
-
new_order = list(range(1, len(self.script_dict[script_type]) + 1))
|
| 195 |
-
self.id_order[script_type] = [str(i) for i in new_order]
|
| 196 |
-
self.script_dict[script_type].sort(key=lambda x: x['id'])
|
| 197 |
-
|
| 198 |
-
def sort_actions(self, script_type=None):
|
| 199 |
-
if script_type:
|
| 200 |
-
self._sort(script_type)
|
| 201 |
-
else:
|
| 202 |
-
for i in self.stypes:
|
| 203 |
-
self._sort(i)
|
| 204 |
-
|
| 205 |
-
def add_action(self, action: dict, insert_position=None):
|
| 206 |
-
current_len = len(self.currently_editing_script)
|
| 207 |
-
action_to_add = action.copy()
|
| 208 |
-
action_to_add['id'] = current_len + 1
|
| 209 |
-
action_to_add['uuid'] = uuid.uuid4().fields[-1]
|
| 210 |
-
self.currently_editing_script.append(action_to_add)
|
| 211 |
-
self._insert_action(insert_position, current_len)
|
| 212 |
-
self.update_time_stamp()
|
| 213 |
-
|
| 214 |
-
def add_variable(self, statement, variable, type, insert_position=None):
|
| 215 |
-
variable = self.validate_function_name(variable)
|
| 216 |
-
convert_type = getattr(builtins, type)
|
| 217 |
-
statement = convert_type(statement)
|
| 218 |
-
current_len = len(self.currently_editing_script)
|
| 219 |
-
uid = uuid.uuid4().fields[-1]
|
| 220 |
-
action = {"id": current_len + 1, "instrument": 'variable', "action": variable,
|
| 221 |
-
"args": {"statement": 'None' if statement == '' else statement}, "return": '', "uuid": uid,
|
| 222 |
-
"arg_types": {"statement": type}}
|
| 223 |
-
self.currently_editing_script.append(action)
|
| 224 |
-
self._insert_action(insert_position, current_len)
|
| 225 |
-
self.update_time_stamp()
|
| 226 |
-
|
| 227 |
-
def _insert_action(self, insert_position, current_len, action_len:int=1):
|
| 228 |
-
|
| 229 |
-
if insert_position is None:
|
| 230 |
-
self.currently_editing_order.extend([str(current_len + i + 1) for i in range(action_len)])
|
| 231 |
-
else:
|
| 232 |
-
index = int(insert_position) - 1
|
| 233 |
-
self.currently_editing_order[index:index] = [str(current_len + i + 1) for i in range(action_len)]
|
| 234 |
-
self.sort_actions()
|
| 235 |
-
|
| 236 |
-
def get_added_variables(self):
|
| 237 |
-
added_variables: Dict[str, str] = {action["action"]: action["arg_types"]["statement"] for action in
|
| 238 |
-
self.currently_editing_script if action["instrument"] == "variable"}
|
| 239 |
-
|
| 240 |
-
return added_variables
|
| 241 |
-
|
| 242 |
-
def get_output_variables(self):
|
| 243 |
-
output_variables: Dict[str, str] = {action["return"]: "function_output" for action in
|
| 244 |
-
self.currently_editing_script if action["return"]}
|
| 245 |
-
|
| 246 |
-
return output_variables
|
| 247 |
-
|
| 248 |
-
def get_variables(self):
|
| 249 |
-
output_variables: Dict[str, str] = self.get_output_variables()
|
| 250 |
-
added_variables = self.get_added_variables()
|
| 251 |
-
output_variables.update(added_variables)
|
| 252 |
-
|
| 253 |
-
return output_variables
|
| 254 |
-
|
| 255 |
-
def validate_variables(self, kwargs):
|
| 256 |
-
"""
|
| 257 |
-
Validates the kwargs passed to the Script
|
| 258 |
-
"""
|
| 259 |
-
output_variables: Dict[str, str] = self.get_variables()
|
| 260 |
-
# print(output_variables)
|
| 261 |
-
for key, value in kwargs.items():
|
| 262 |
-
if type(value) is str and value in output_variables:
|
| 263 |
-
var_type = output_variables[value]
|
| 264 |
-
kwargs[key] = {value: var_type}
|
| 265 |
-
if isinstance(value, str) and value.startswith("#"):
|
| 266 |
-
kwargs[key] = f"#{self.validate_function_name(value[1:])}"
|
| 267 |
-
return kwargs
|
| 268 |
-
|
| 269 |
-
def add_logic_action(self, logic_type: str, statement, insert_position=None):
|
| 270 |
-
current_len = len(self.currently_editing_script)
|
| 271 |
-
uid = uuid.uuid4().fields[-1]
|
| 272 |
-
logic_dict = {
|
| 273 |
-
"if":
|
| 274 |
-
[
|
| 275 |
-
{"id": current_len + 1, "instrument": 'if', "action": 'if',
|
| 276 |
-
"args": {"statement": 'True' if statement == '' else statement},
|
| 277 |
-
"return": '', "uuid": uid, "arg_types": {"statement": ''}},
|
| 278 |
-
{"id": current_len + 2, "instrument": 'if', "action": 'else', "args": {}, "return": '',
|
| 279 |
-
"uuid": uid},
|
| 280 |
-
{"id": current_len + 3, "instrument": 'if', "action": 'endif', "args": {}, "return": '',
|
| 281 |
-
"uuid": uid},
|
| 282 |
-
],
|
| 283 |
-
"while":
|
| 284 |
-
[
|
| 285 |
-
{"id": current_len + 1, "instrument": 'while', "action": 'while',
|
| 286 |
-
"args": {"statement": 'False' if statement == '' else statement}, "return": '', "uuid": uid,
|
| 287 |
-
"arg_types": {"statement": ''}},
|
| 288 |
-
{"id": current_len + 2, "instrument": 'while', "action": 'endwhile', "args": {}, "return": '',
|
| 289 |
-
"uuid": uid},
|
| 290 |
-
],
|
| 291 |
-
|
| 292 |
-
"wait":
|
| 293 |
-
[
|
| 294 |
-
{"id": current_len + 1, "instrument": 'wait', "action": "wait",
|
| 295 |
-
"args": {"statement": 1 if statement == '' else statement},
|
| 296 |
-
"return": '', "uuid": uid, "arg_types": {"statement": "float"}},
|
| 297 |
-
],
|
| 298 |
-
"repeat":
|
| 299 |
-
[
|
| 300 |
-
{"id": current_len + 1, "instrument": 'repeat', "action": "repeat",
|
| 301 |
-
"args": {"statement": 1 if statement == '' else statement}, "return": '', "uuid": uid,
|
| 302 |
-
"arg_types": {"statement": "int"}},
|
| 303 |
-
{"id": current_len + 2, "instrument": 'repeat', "action": 'endrepeat',
|
| 304 |
-
"args": {}, "return": '', "uuid": uid},
|
| 305 |
-
],
|
| 306 |
-
}
|
| 307 |
-
action_list = logic_dict[logic_type]
|
| 308 |
-
self.currently_editing_script.extend(action_list)
|
| 309 |
-
self._insert_action(insert_position, current_len, len(action_list))
|
| 310 |
-
self.update_time_stamp()
|
| 311 |
-
|
| 312 |
-
def delete_action(self, id: int):
|
| 313 |
-
"""
|
| 314 |
-
Delete the action by id (step number)
|
| 315 |
-
"""
|
| 316 |
-
uid = next((action['uuid'] for action in self.currently_editing_script if action['id'] == int(id)), None)
|
| 317 |
-
id_to_be_removed = [action['id'] for action in self.currently_editing_script if action['uuid'] == uid]
|
| 318 |
-
order = self.currently_editing_order
|
| 319 |
-
script = self.currently_editing_script
|
| 320 |
-
self.currently_editing_order = [i for i in order if int(i) not in id_to_be_removed]
|
| 321 |
-
self.currently_editing_script = [action for action in script if action['id'] not in id_to_be_removed]
|
| 322 |
-
self.sort_actions()
|
| 323 |
-
self.update_time_stamp()
|
| 324 |
-
|
| 325 |
-
def duplicate_action(self, id: int):
|
| 326 |
-
"""
|
| 327 |
-
duplicate action by id (step number), available only for non logic actions
|
| 328 |
-
"""
|
| 329 |
-
action_to_duplicate = next((action for action in self.currently_editing_script if action['id'] == int(id)),
|
| 330 |
-
None)
|
| 331 |
-
insert_id = action_to_duplicate.get("id")
|
| 332 |
-
self.add_action(action_to_duplicate)
|
| 333 |
-
# print(self.currently_editing_script)
|
| 334 |
-
if action_to_duplicate is not None:
|
| 335 |
-
# Update IDs for all subsequent actions
|
| 336 |
-
for action in self.currently_editing_script:
|
| 337 |
-
if action['id'] > insert_id:
|
| 338 |
-
action['id'] += 1
|
| 339 |
-
self.currently_editing_script[-1]['id'] = insert_id + 1
|
| 340 |
-
# Sort actions if necessary and update the time stamp
|
| 341 |
-
self.sort_actions()
|
| 342 |
-
self.update_time_stamp()
|
| 343 |
-
else:
|
| 344 |
-
raise ValueError("Action not found: Unable to duplicate the action with ID", id)
|
| 345 |
-
|
| 346 |
-
def config(self, stype):
|
| 347 |
-
"""
|
| 348 |
-
take the global script_dict
|
| 349 |
-
:return: list of variable that require input
|
| 350 |
-
"""
|
| 351 |
-
configure = []
|
| 352 |
-
config_type_dict = {}
|
| 353 |
-
for action in self.script_dict[stype]:
|
| 354 |
-
args = action['args']
|
| 355 |
-
if args is not None:
|
| 356 |
-
if type(args) is not dict:
|
| 357 |
-
if type(args) is str and args.startswith("#") and not args[1:] in configure:
|
| 358 |
-
configure.append(args[1:])
|
| 359 |
-
config_type_dict[args[1:]] = action['arg_types']
|
| 360 |
-
|
| 361 |
-
else:
|
| 362 |
-
for arg in args:
|
| 363 |
-
if type(args[arg]) is str \
|
| 364 |
-
and args[arg].startswith("#") \
|
| 365 |
-
and not args[arg][1:] in configure:
|
| 366 |
-
configure.append(args[arg][1:])
|
| 367 |
-
if arg in action['arg_types']:
|
| 368 |
-
if action['arg_types'][arg] == '':
|
| 369 |
-
config_type_dict[args[arg][1:]] = "any"
|
| 370 |
-
else:
|
| 371 |
-
config_type_dict[args[arg][1:]] = action['arg_types'][arg]
|
| 372 |
-
else:
|
| 373 |
-
config_type_dict[args[arg][1:]] = "any"
|
| 374 |
-
# todo
|
| 375 |
-
return configure, config_type_dict
|
| 376 |
-
|
| 377 |
-
def config_return(self):
|
| 378 |
-
"""
|
| 379 |
-
take the global script_dict
|
| 380 |
-
:return: list of variable that require input
|
| 381 |
-
"""
|
| 382 |
-
|
| 383 |
-
return_list = set([action['return'] for action in self.script_dict['script'] if not action['return'] == ''])
|
| 384 |
-
output_str = "return {"
|
| 385 |
-
for i in return_list:
|
| 386 |
-
output_str += "'" + i + "':" + i + ","
|
| 387 |
-
output_str += "}"
|
| 388 |
-
return output_str, return_list
|
| 389 |
-
|
| 390 |
-
def finalize(self):
|
| 391 |
-
"""finalize script, disable editing"""
|
| 392 |
-
self.status = "finalized"
|
| 393 |
-
self.update_time_stamp()
|
| 394 |
-
|
| 395 |
-
def save_as(self, name):
|
| 396 |
-
"""resave script, enable editing"""
|
| 397 |
-
self.name = name
|
| 398 |
-
self.status = "editing"
|
| 399 |
-
self.update_time_stamp()
|
| 400 |
-
|
| 401 |
-
def indent(self, unit=0):
|
| 402 |
-
"""helper: create _ unit of indent in code string"""
|
| 403 |
-
string = "\n"
|
| 404 |
-
for _ in range(unit):
|
| 405 |
-
string += "\t"
|
| 406 |
-
return string
|
| 407 |
-
|
| 408 |
-
def convert_to_lines(self, exec_str_collection: dict):
|
| 409 |
-
"""
|
| 410 |
-
Parse a dictionary of script functions and extract function body lines.
|
| 411 |
-
|
| 412 |
-
:param exec_str_collection: Dictionary containing script types and corresponding function strings.
|
| 413 |
-
:return: A dict containing script types as keys and lists of function body lines as values.
|
| 414 |
-
"""
|
| 415 |
-
line_collection = {}
|
| 416 |
-
for stype, func_str in exec_str_collection.items():
|
| 417 |
-
if func_str:
|
| 418 |
-
module = ast.parse(func_str)
|
| 419 |
-
func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
|
| 420 |
-
|
| 421 |
-
# Extract function body as source lines
|
| 422 |
-
line_collection[stype] = [ast.unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
|
| 423 |
-
# print(line_collection[stype])
|
| 424 |
-
return line_collection
|
| 425 |
-
|
| 426 |
-
def compile(self, script_path=None):
|
| 427 |
-
"""
|
| 428 |
-
Compile the current script to a Python file.
|
| 429 |
-
:return: String to write to a Python file.
|
| 430 |
-
"""
|
| 431 |
-
self.sort_actions()
|
| 432 |
-
run_name = self.name if self.name else "untitled"
|
| 433 |
-
run_name = self.validate_function_name(run_name)
|
| 434 |
-
exec_str_collection = {}
|
| 435 |
-
|
| 436 |
-
for i in self.stypes:
|
| 437 |
-
if self.script_dict[i]:
|
| 438 |
-
func_str = self._generate_function_header(run_name, i) + self._generate_function_body(i)
|
| 439 |
-
exec_str_collection[i] = func_str
|
| 440 |
-
if script_path:
|
| 441 |
-
self._write_to_file(script_path, run_name, exec_str_collection)
|
| 442 |
-
|
| 443 |
-
return exec_str_collection
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
@staticmethod
|
| 448 |
-
def validate_function_name(name):
|
| 449 |
-
"""Replace invalid characters with underscores"""
|
| 450 |
-
name = re.sub(r'\W|^(?=\d)', '_', name)
|
| 451 |
-
# Check if it's a Python keyword and adjust if necessary
|
| 452 |
-
if keyword.iskeyword(name):
|
| 453 |
-
name += '_'
|
| 454 |
-
return name
|
| 455 |
-
|
| 456 |
-
def _generate_function_header(self, run_name, stype):
|
| 457 |
-
"""
|
| 458 |
-
Generate the function header.
|
| 459 |
-
"""
|
| 460 |
-
configure, config_type = self.config(stype)
|
| 461 |
-
|
| 462 |
-
configure = [param + f":{param_type}" if not param_type == "any" else "" for param, param_type in
|
| 463 |
-
config_type.items()]
|
| 464 |
-
|
| 465 |
-
script_type = f"_{stype}" if stype != "script" else ""
|
| 466 |
-
function_header = f"def {run_name}{script_type}("
|
| 467 |
-
|
| 468 |
-
if stype == "script":
|
| 469 |
-
function_header += ", ".join(configure)
|
| 470 |
-
|
| 471 |
-
function_header += "):"
|
| 472 |
-
# function_header += self.indent(1) + f"global {run_name}_{stype}"
|
| 473 |
-
return function_header
|
| 474 |
-
|
| 475 |
-
def _generate_function_body(self, stype):
|
| 476 |
-
"""
|
| 477 |
-
Generate the function body for each type in stypes.
|
| 478 |
-
"""
|
| 479 |
-
body = ''
|
| 480 |
-
indent_unit = 1
|
| 481 |
-
|
| 482 |
-
for index, action in enumerate(self.script_dict[stype]):
|
| 483 |
-
text, indent_unit = self._process_action(indent_unit, action, index, stype)
|
| 484 |
-
body += text
|
| 485 |
-
return_str, return_list = self.config_return()
|
| 486 |
-
if return_list and stype == "script":
|
| 487 |
-
body += self.indent(indent_unit) + return_str
|
| 488 |
-
return body
|
| 489 |
-
|
| 490 |
-
def _process_action(self, indent_unit, action, index, stype):
|
| 491 |
-
"""
|
| 492 |
-
Process each action within the script dictionary.
|
| 493 |
-
"""
|
| 494 |
-
instrument = action['instrument']
|
| 495 |
-
statement = action['args'].get('statement')
|
| 496 |
-
args = self._process_args(action['args'])
|
| 497 |
-
|
| 498 |
-
save_data = action['return']
|
| 499 |
-
action_name = action['action']
|
| 500 |
-
next_action = self._get_next_action(stype, index)
|
| 501 |
-
# print(args)
|
| 502 |
-
if instrument == 'if':
|
| 503 |
-
return self._process_if(indent_unit, action_name, statement, next_action)
|
| 504 |
-
elif instrument == 'while':
|
| 505 |
-
return self._process_while(indent_unit, action_name, statement, next_action)
|
| 506 |
-
elif instrument == 'variable':
|
| 507 |
-
return self.indent(indent_unit) + f"{action_name} = {statement}", indent_unit
|
| 508 |
-
elif instrument == 'wait':
|
| 509 |
-
return f"{self.indent(indent_unit)}time.sleep({statement})", indent_unit
|
| 510 |
-
elif instrument == 'repeat':
|
| 511 |
-
return self._process_repeat(indent_unit, action_name, statement, next_action)
|
| 512 |
-
#todo
|
| 513 |
-
# elif instrument == 'registered_workflows':
|
| 514 |
-
# return inspect.getsource(my_function)
|
| 515 |
-
else:
|
| 516 |
-
return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data)
|
| 517 |
-
|
| 518 |
-
def _process_args(self, args):
|
| 519 |
-
"""
|
| 520 |
-
Process arguments, handling any specific formatting needs.
|
| 521 |
-
"""
|
| 522 |
-
if isinstance(args, str) and args.startswith("#"):
|
| 523 |
-
return args[1:]
|
| 524 |
-
return args
|
| 525 |
-
|
| 526 |
-
def _process_if(self, indent_unit, action, args, next_action):
|
| 527 |
-
"""
|
| 528 |
-
Process 'if' and 'else' actions.
|
| 529 |
-
"""
|
| 530 |
-
exec_string = ""
|
| 531 |
-
if action == 'if':
|
| 532 |
-
exec_string += self.indent(indent_unit) + f"if {args}:"
|
| 533 |
-
indent_unit += 1
|
| 534 |
-
if next_action and next_action['instrument'] == 'if' and next_action['action'] == 'else':
|
| 535 |
-
exec_string += self.indent(indent_unit) + "pass"
|
| 536 |
-
# else:
|
| 537 |
-
|
| 538 |
-
elif action == 'else':
|
| 539 |
-
indent_unit -= 1
|
| 540 |
-
exec_string += self.indent(indent_unit) + "else:"
|
| 541 |
-
indent_unit += 1
|
| 542 |
-
if next_action and next_action['instrument'] == 'if' and next_action['action'] == 'endif':
|
| 543 |
-
exec_string += self.indent(indent_unit) + "pass"
|
| 544 |
-
else:
|
| 545 |
-
indent_unit -= 1
|
| 546 |
-
return exec_string, indent_unit
|
| 547 |
-
|
| 548 |
-
def _process_while(self, indent_unit, action, args, next_action):
|
| 549 |
-
"""
|
| 550 |
-
Process 'while' and 'endwhile' actions.
|
| 551 |
-
"""
|
| 552 |
-
exec_string = ""
|
| 553 |
-
if action == 'while':
|
| 554 |
-
exec_string += self.indent(indent_unit) + f"while {args}:"
|
| 555 |
-
indent_unit += 1
|
| 556 |
-
if next_action and next_action['instrument'] == 'while':
|
| 557 |
-
exec_string += self.indent(indent_unit) + "pass"
|
| 558 |
-
elif action == 'endwhile':
|
| 559 |
-
indent_unit -= 1
|
| 560 |
-
return exec_string, indent_unit
|
| 561 |
-
|
| 562 |
-
def _process_repeat(self, indent_unit, action, args, next_action):
|
| 563 |
-
"""
|
| 564 |
-
Process 'while' and 'endwhile' actions.
|
| 565 |
-
"""
|
| 566 |
-
exec_string = ""
|
| 567 |
-
if action == 'repeat':
|
| 568 |
-
exec_string += self.indent(indent_unit) + f"for _ in range({args}):"
|
| 569 |
-
indent_unit += 1
|
| 570 |
-
if next_action and next_action['instrument'] == 'repeat':
|
| 571 |
-
exec_string += self.indent(indent_unit) + "pass"
|
| 572 |
-
elif action == 'endrepeat':
|
| 573 |
-
indent_unit -= 1
|
| 574 |
-
return exec_string, indent_unit
|
| 575 |
-
|
| 576 |
-
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data):
|
| 577 |
-
"""
|
| 578 |
-
Process actions related to instruments.
|
| 579 |
-
"""
|
| 580 |
-
|
| 581 |
-
if isinstance(args, dict):
|
| 582 |
-
args_str = self._process_dict_args(args)
|
| 583 |
-
single_line = f"{instrument}.{action}(**{args_str})"
|
| 584 |
-
elif isinstance(args, str):
|
| 585 |
-
single_line = f"{instrument}.{action} = {args}"
|
| 586 |
-
else:
|
| 587 |
-
single_line = f"{instrument}.{action}()"
|
| 588 |
-
|
| 589 |
-
if save_data:
|
| 590 |
-
save_data += " = "
|
| 591 |
-
|
| 592 |
-
return self.indent(indent_unit) + save_data + single_line, indent_unit
|
| 593 |
-
|
| 594 |
-
def _process_dict_args(self, args):
|
| 595 |
-
"""
|
| 596 |
-
Process dictionary arguments, handling special cases like variables.
|
| 597 |
-
"""
|
| 598 |
-
args_str = args.__str__()
|
| 599 |
-
for arg in args:
|
| 600 |
-
if isinstance(args[arg], str) and args[arg].startswith("#"):
|
| 601 |
-
args_str = args_str.replace(f"'#{args[arg][1:]}'", args[arg][1:])
|
| 602 |
-
elif isinstance(args[arg], dict):
|
| 603 |
-
# print(args[arg])
|
| 604 |
-
variables = self.get_variables()
|
| 605 |
-
value = next(iter(args[arg]))
|
| 606 |
-
if value not in variables:
|
| 607 |
-
raise ValueError(f"Variable ({value}) is not defined.")
|
| 608 |
-
args_str = args_str.replace(f"{args[arg]}", next(iter(args[arg])))
|
| 609 |
-
# elif self._is_variable(arg):
|
| 610 |
-
# print("is variable")
|
| 611 |
-
# args_str = args_str.replace(f"'{args[arg]}'", args[arg])
|
| 612 |
-
return args_str
|
| 613 |
-
|
| 614 |
-
def _get_next_action(self, stype, index):
|
| 615 |
-
"""
|
| 616 |
-
Get the next action in the sequence if it exists.
|
| 617 |
-
"""
|
| 618 |
-
if index < (len(self.script_dict[stype]) - 1):
|
| 619 |
-
return self.script_dict[stype][index + 1]
|
| 620 |
-
return None
|
| 621 |
-
|
| 622 |
-
def _is_variable(self, arg):
|
| 623 |
-
"""
|
| 624 |
-
Check if the argument is of type 'variable'.
|
| 625 |
-
"""
|
| 626 |
-
return arg in self.script_dict and self.script_dict[arg].get("arg_types") == "variable"
|
| 627 |
-
|
| 628 |
-
def _write_to_file(self, script_path, run_name, exec_string):
|
| 629 |
-
"""
|
| 630 |
-
Write the compiled script to a file.
|
| 631 |
-
"""
|
| 632 |
-
with open(script_path + run_name + ".py", "w") as s:
|
| 633 |
-
if self.deck:
|
| 634 |
-
s.write(f"import {self.deck} as deck")
|
| 635 |
-
else:
|
| 636 |
-
s.write("deck = None")
|
| 637 |
-
s.write("\nimport time")
|
| 638 |
-
for i in exec_string.values():
|
| 639 |
-
s.write(f"\n\n\n{i}")
|
| 640 |
-
|
| 641 |
-
class WorkflowRun(db.Model):
|
| 642 |
-
__tablename__ = 'workflow_runs'
|
| 643 |
-
|
| 644 |
-
id = db.Column(db.Integer, primary_key=True)
|
| 645 |
-
name = db.Column(db.String(128), nullable=False)
|
| 646 |
-
platform = db.Column(db.String(128), nullable=False)
|
| 647 |
-
start_time = db.Column(db.DateTime, default=datetime.now())
|
| 648 |
-
end_time = db.Column(db.DateTime)
|
| 649 |
-
data_path = db.Column(db.String(256))
|
| 650 |
-
steps = db.relationship(
|
| 651 |
-
'WorkflowStep',
|
| 652 |
-
backref='workflow_runs',
|
| 653 |
-
cascade='all, delete-orphan',
|
| 654 |
-
passive_deletes=True
|
| 655 |
-
)
|
| 656 |
-
def as_dict(self):
|
| 657 |
-
dict = self.__dict__
|
| 658 |
-
dict.pop('_sa_instance_state', None)
|
| 659 |
-
return dict
|
| 660 |
-
|
| 661 |
-
class WorkflowStep(db.Model):
|
| 662 |
-
__tablename__ = 'workflow_steps'
|
| 663 |
-
|
| 664 |
-
id = db.Column(db.Integer, primary_key=True)
|
| 665 |
-
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow_runs.id', ondelete='CASCADE'), nullable=False)
|
| 666 |
-
|
| 667 |
-
phase = db.Column(db.String(64), nullable=False) # 'prep', 'main', 'cleanup'
|
| 668 |
-
repeat_index = db.Column(db.Integer, default=0) # Only applies to 'main' phase
|
| 669 |
-
step_index = db.Column(db.Integer, default=0)
|
| 670 |
-
method_name = db.Column(db.String(128), nullable=False)
|
| 671 |
-
start_time = db.Column(db.DateTime)
|
| 672 |
-
end_time = db.Column(db.DateTime)
|
| 673 |
-
run_error = db.Column(db.Boolean, default=False)
|
| 674 |
-
|
| 675 |
-
def as_dict(self):
|
| 676 |
-
dict = self.__dict__.copy()
|
| 677 |
-
dict.pop('_sa_instance_state', None)
|
| 678 |
-
return dict
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
class SingleStep(db.Model):
|
| 682 |
-
__tablename__ = 'single_steps'
|
| 683 |
-
|
| 684 |
-
id = db.Column(db.Integer, primary_key=True)
|
| 685 |
-
method_name = db.Column(db.String(128), nullable=False)
|
| 686 |
-
kwargs = db.Column(JSONType, nullable=False)
|
| 687 |
-
start_time = db.Column(db.DateTime)
|
| 688 |
-
end_time = db.Column(db.DateTime)
|
| 689 |
-
run_error = db.Column(db.String(128))
|
| 690 |
-
output = db.Column(JSONType)
|
| 691 |
-
|
| 692 |
-
def as_dict(self):
|
| 693 |
-
dict = self.__dict__.copy()
|
| 694 |
-
dict.pop('_sa_instance_state', None)
|
| 695 |
-
return dict
|
| 696 |
-
|
| 697 |
-
if __name__ == "__main__":
|
| 698 |
-
a = Script()
|
| 699 |
-
|
| 700 |
-
print("")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/form.py
DELETED
|
@@ -1,560 +0,0 @@
|
|
| 1 |
-
from enum import Enum
|
| 2 |
-
from typing import get_origin, get_args, Union, Any
|
| 3 |
-
|
| 4 |
-
from wtforms.fields.choices import SelectField
|
| 5 |
-
from wtforms.fields.core import Field
|
| 6 |
-
from wtforms.validators import InputRequired, ValidationError, Optional
|
| 7 |
-
from wtforms.widgets.core import TextInput
|
| 8 |
-
|
| 9 |
-
from flask_wtf import FlaskForm
|
| 10 |
-
from wtforms import StringField, FloatField, HiddenField, BooleanField, IntegerField
|
| 11 |
-
import inspect
|
| 12 |
-
|
| 13 |
-
from ivoryos.utils.db_models import Script
|
| 14 |
-
from ivoryos.utils.global_config import GlobalConfig
|
| 15 |
-
|
| 16 |
-
global_config = GlobalConfig()
|
| 17 |
-
|
| 18 |
-
def find_variable(data, script):
|
| 19 |
-
"""
|
| 20 |
-
find user defined variables and return values in the script:Script
|
| 21 |
-
:param data: string of input variable name
|
| 22 |
-
:param script:Script object
|
| 23 |
-
"""
|
| 24 |
-
variables: dict[str, str] = script.get_variables()
|
| 25 |
-
for variable_name, variable_type in variables.items():
|
| 26 |
-
if variable_name == data:
|
| 27 |
-
return data, variable_type # variable_type int float str or "function_output"
|
| 28 |
-
return None, None
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class VariableOrStringField(Field):
|
| 32 |
-
widget = TextInput()
|
| 33 |
-
|
| 34 |
-
def __init__(self, label='', validators=None, script=None, **kwargs):
|
| 35 |
-
super(VariableOrStringField, self).__init__(label, validators, **kwargs)
|
| 36 |
-
self.script = script
|
| 37 |
-
|
| 38 |
-
def process_formdata(self, valuelist):
|
| 39 |
-
if valuelist:
|
| 40 |
-
if not self.script.editing_type == "script" and valuelist[0].startswith("#"):
|
| 41 |
-
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
| 42 |
-
self.data = valuelist[0]
|
| 43 |
-
|
| 44 |
-
def _value(self):
|
| 45 |
-
if self.script:
|
| 46 |
-
variable, variable_type = find_variable(self.data, self.script)
|
| 47 |
-
if variable:
|
| 48 |
-
return variable
|
| 49 |
-
|
| 50 |
-
return str(self.data) if self.data is not None else ""
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
class VariableOrFloatField(Field):
|
| 54 |
-
widget = TextInput()
|
| 55 |
-
|
| 56 |
-
def __init__(self, label='', validators=None, script=None, **kwargs):
|
| 57 |
-
super(VariableOrFloatField, self).__init__(label, validators, **kwargs)
|
| 58 |
-
self.script = script
|
| 59 |
-
|
| 60 |
-
def _value(self):
|
| 61 |
-
if self.script:
|
| 62 |
-
variable, variable_type = find_variable(self.data, self.script)
|
| 63 |
-
if variable:
|
| 64 |
-
return variable
|
| 65 |
-
|
| 66 |
-
if self.raw_data:
|
| 67 |
-
return self.raw_data[0]
|
| 68 |
-
if self.data is not None:
|
| 69 |
-
return str(self.data)
|
| 70 |
-
return ""
|
| 71 |
-
|
| 72 |
-
def process_formdata(self, valuelist):
|
| 73 |
-
if not valuelist:
|
| 74 |
-
return
|
| 75 |
-
elif valuelist[0].startswith("#"):
|
| 76 |
-
if not self.script.editing_type == "script":
|
| 77 |
-
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
| 78 |
-
self.data = valuelist[0]
|
| 79 |
-
return
|
| 80 |
-
try:
|
| 81 |
-
if self.script:
|
| 82 |
-
try:
|
| 83 |
-
variable, variable_type = find_variable(valuelist[0], self.script)
|
| 84 |
-
if variable:
|
| 85 |
-
if not variable_type == "function_output":
|
| 86 |
-
if variable_type not in ["float", "int"]:
|
| 87 |
-
raise ValueError("Variable is not a valid float")
|
| 88 |
-
self.data = variable
|
| 89 |
-
return
|
| 90 |
-
except ValueError:
|
| 91 |
-
pass
|
| 92 |
-
self.data = float(valuelist[0])
|
| 93 |
-
except ValueError as exc:
|
| 94 |
-
self.data = None
|
| 95 |
-
raise ValueError(self.gettext("Not a valid float value.")) from exc
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
# unset_value = UnsetValue()
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
class VariableOrIntField(Field):
|
| 102 |
-
widget = TextInput()
|
| 103 |
-
|
| 104 |
-
def __init__(self, label='', validators=None, script=None, **kwargs):
|
| 105 |
-
super(VariableOrIntField, self).__init__(label, validators, **kwargs)
|
| 106 |
-
self.script = script
|
| 107 |
-
|
| 108 |
-
def _value(self):
|
| 109 |
-
if self.script:
|
| 110 |
-
variable, variable_type = find_variable(self.data, self.script)
|
| 111 |
-
if variable:
|
| 112 |
-
return variable
|
| 113 |
-
|
| 114 |
-
if self.raw_data:
|
| 115 |
-
return self.raw_data[0]
|
| 116 |
-
if self.data is not None:
|
| 117 |
-
return str(self.data)
|
| 118 |
-
return ""
|
| 119 |
-
|
| 120 |
-
def process_formdata(self, valuelist):
|
| 121 |
-
if not valuelist:
|
| 122 |
-
return
|
| 123 |
-
if self.script:
|
| 124 |
-
variable, variable_type = find_variable(valuelist[0], self.script)
|
| 125 |
-
if variable:
|
| 126 |
-
try:
|
| 127 |
-
if not variable_type == "function_output":
|
| 128 |
-
if not variable_type == "int":
|
| 129 |
-
raise ValueError("Not a valid integer value")
|
| 130 |
-
self.data = str(variable)
|
| 131 |
-
return
|
| 132 |
-
except ValueError:
|
| 133 |
-
pass
|
| 134 |
-
if valuelist[0].startswith("#"):
|
| 135 |
-
if not self.script.editing_type == "script":
|
| 136 |
-
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
| 137 |
-
self.data = valuelist[0]
|
| 138 |
-
return
|
| 139 |
-
try:
|
| 140 |
-
self.data = int(valuelist[0])
|
| 141 |
-
except ValueError as exc:
|
| 142 |
-
self.data = None
|
| 143 |
-
raise ValueError(self.gettext("Not a valid integer value.")) from exc
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
class VariableOrBoolField(BooleanField):
|
| 147 |
-
widget = TextInput()
|
| 148 |
-
false_values = (False, "false", "", "False", "f", "F")
|
| 149 |
-
|
| 150 |
-
def __init__(self, label='', validators=None, script=None, **kwargs):
|
| 151 |
-
super(VariableOrBoolField, self).__init__(label, validators, **kwargs)
|
| 152 |
-
self.script = script
|
| 153 |
-
|
| 154 |
-
def process_data(self, value):
|
| 155 |
-
|
| 156 |
-
if self.script:
|
| 157 |
-
variable, variable_type = find_variable(value, self.script)
|
| 158 |
-
if variable:
|
| 159 |
-
if not variable_type == "function_output":
|
| 160 |
-
raise ValueError("Not accepting boolean variables")
|
| 161 |
-
return variable
|
| 162 |
-
|
| 163 |
-
self.data = bool(value)
|
| 164 |
-
|
| 165 |
-
def process_formdata(self, valuelist):
|
| 166 |
-
# todo
|
| 167 |
-
# print(valuelist)
|
| 168 |
-
if not valuelist or not type(valuelist) is list:
|
| 169 |
-
self.data = False
|
| 170 |
-
else:
|
| 171 |
-
value = valuelist[0] if type(valuelist) is list else valuelist
|
| 172 |
-
if value.startswith("#"):
|
| 173 |
-
if not self.script.editing_type == "script":
|
| 174 |
-
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
| 175 |
-
self.data = valuelist[0]
|
| 176 |
-
elif value in self.false_values:
|
| 177 |
-
self.data = False
|
| 178 |
-
else:
|
| 179 |
-
self.data = True
|
| 180 |
-
|
| 181 |
-
def _value(self):
|
| 182 |
-
|
| 183 |
-
if self.script:
|
| 184 |
-
variable, variable_type = find_variable(self.raw_data, self.script)
|
| 185 |
-
if variable:
|
| 186 |
-
return variable
|
| 187 |
-
|
| 188 |
-
if self.raw_data:
|
| 189 |
-
return str(self.raw_data[0])
|
| 190 |
-
return "y"
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
class FlexibleEnumField(StringField):
|
| 194 |
-
def __init__(self, label=None, validators=None, choices=None, script=None, **kwargs):
|
| 195 |
-
super().__init__(label, validators, **kwargs)
|
| 196 |
-
self.script = script
|
| 197 |
-
self.enum_class = choices
|
| 198 |
-
self.choices = [e.name for e in self.enum_class]
|
| 199 |
-
# self.value_list = [e.name for e in self.enum_class]
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def process_formdata(self, valuelist):
|
| 203 |
-
if valuelist:
|
| 204 |
-
key = valuelist[0]
|
| 205 |
-
if key in self.choices:
|
| 206 |
-
# Convert the string key to Enum instance
|
| 207 |
-
self.data = self.enum_class[key].value
|
| 208 |
-
elif self.data.startswith("#"):
|
| 209 |
-
if not self.script.editing_type == "script":
|
| 210 |
-
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
| 211 |
-
self.data = self.data
|
| 212 |
-
else:
|
| 213 |
-
raise ValidationError(
|
| 214 |
-
f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
def format_name(name):
|
| 218 |
-
"""Converts 'example_name' to 'Example Name'."""
|
| 219 |
-
name = name.split(".")[-1]
|
| 220 |
-
text = ' '.join(word for word in name.split('_'))
|
| 221 |
-
return text.capitalize()
|
| 222 |
-
|
| 223 |
-
def parse_annotation(annotation):
|
| 224 |
-
"""
|
| 225 |
-
Given a type annotation, return:
|
| 226 |
-
- a list of all valid types (excluding NoneType)
|
| 227 |
-
- a boolean indicating if the value can be None (optional)
|
| 228 |
-
"""
|
| 229 |
-
origin = get_origin(annotation)
|
| 230 |
-
args = get_args(annotation)
|
| 231 |
-
|
| 232 |
-
if annotation is Any:
|
| 233 |
-
return [str], True # fallback: accept any string, optional
|
| 234 |
-
|
| 235 |
-
if origin is Union:
|
| 236 |
-
types = list(set(args))
|
| 237 |
-
is_optional = type(None) in types
|
| 238 |
-
non_none_types = [t for t in types if t is not type(None)]
|
| 239 |
-
return non_none_types, is_optional
|
| 240 |
-
|
| 241 |
-
# Not a Union, just a regular type
|
| 242 |
-
return [annotation], False
|
| 243 |
-
|
| 244 |
-
def create_form_for_method(method, autofill, script=None, design=True):
|
| 245 |
-
"""
|
| 246 |
-
Create forms for each method or signature
|
| 247 |
-
:param method: dict(docstring, signature)
|
| 248 |
-
:param autofill:bool if autofill is enabled
|
| 249 |
-
:param script:Script object
|
| 250 |
-
:param design: if design is enabled
|
| 251 |
-
"""
|
| 252 |
-
|
| 253 |
-
class DynamicForm(FlaskForm):
|
| 254 |
-
pass
|
| 255 |
-
|
| 256 |
-
annotation_mapping = {
|
| 257 |
-
int: (VariableOrIntField if design else IntegerField, 'Enter integer value'),
|
| 258 |
-
float: (VariableOrFloatField if design else FloatField, 'Enter numeric value'),
|
| 259 |
-
str: (VariableOrStringField if design else StringField, 'Enter text'),
|
| 260 |
-
bool: (VariableOrBoolField if design else BooleanField, 'Empty for false')
|
| 261 |
-
}
|
| 262 |
-
sig = method if type(method) is inspect.Signature else inspect.signature(method)
|
| 263 |
-
|
| 264 |
-
for param in sig.parameters.values():
|
| 265 |
-
if param.name == 'self':
|
| 266 |
-
continue
|
| 267 |
-
formatted_param_name = format_name(param.name)
|
| 268 |
-
|
| 269 |
-
default_value = None
|
| 270 |
-
if autofill:
|
| 271 |
-
default_value = f'#{param.name}'
|
| 272 |
-
else:
|
| 273 |
-
if param.default is not param.empty:
|
| 274 |
-
if isinstance(param.default, Enum):
|
| 275 |
-
default_value = param.default.name
|
| 276 |
-
else:
|
| 277 |
-
default_value = param.default
|
| 278 |
-
|
| 279 |
-
field_kwargs = {
|
| 280 |
-
"label": formatted_param_name,
|
| 281 |
-
"default": default_value,
|
| 282 |
-
"validators": [InputRequired()] if param.default is param.empty else [Optional()],
|
| 283 |
-
**({"script": script} if (autofill or design) else {})
|
| 284 |
-
}
|
| 285 |
-
if isinstance(param.annotation, type) and issubclass(param.annotation, Enum):
|
| 286 |
-
# enum_class = [(e.name, e.value) for e in param.annotation]
|
| 287 |
-
field_class = FlexibleEnumField
|
| 288 |
-
placeholder_text = f"Choose or type a value for {param.annotation.__name__} (start with # for custom)"
|
| 289 |
-
extra_kwargs = {"choices": param.annotation}
|
| 290 |
-
else:
|
| 291 |
-
# print(param.annotation)
|
| 292 |
-
annotation, optional = parse_annotation(param.annotation)
|
| 293 |
-
annotation = annotation[0]
|
| 294 |
-
field_class, placeholder_text = annotation_mapping.get(
|
| 295 |
-
annotation,
|
| 296 |
-
(VariableOrStringField if design else StringField, f'Enter {param.annotation} value')
|
| 297 |
-
)
|
| 298 |
-
extra_kwargs = {}
|
| 299 |
-
if optional:
|
| 300 |
-
field_kwargs["filters"] = [lambda x: x if x != '' else None]
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
render_kwargs = {"placeholder": placeholder_text}
|
| 304 |
-
|
| 305 |
-
# Create the field with additional rendering kwargs for placeholder text
|
| 306 |
-
field = field_class(**field_kwargs, render_kw=render_kwargs, **extra_kwargs)
|
| 307 |
-
setattr(DynamicForm, param.name, field)
|
| 308 |
-
|
| 309 |
-
# setattr(DynamicForm, f'add', fname)
|
| 310 |
-
return DynamicForm
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
def create_add_form(attr, attr_name, autofill: bool, script=None, design: bool = True):
|
| 314 |
-
"""
|
| 315 |
-
Create forms for each method or signature
|
| 316 |
-
:param attr: dict(docstring, signature)
|
| 317 |
-
:param attr_name: method name
|
| 318 |
-
:param autofill:bool if autofill is enabled
|
| 319 |
-
:param script:Script object
|
| 320 |
-
:param design: if design is enabled. Design allows string input for parameter names ("#param") for all fields
|
| 321 |
-
"""
|
| 322 |
-
signature = attr.get('signature', {})
|
| 323 |
-
docstring = attr.get('docstring', "")
|
| 324 |
-
# print(signature, docstring)
|
| 325 |
-
dynamic_form = create_form_for_method(signature, autofill, script, design)
|
| 326 |
-
if design:
|
| 327 |
-
return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
|
| 328 |
-
setattr(dynamic_form, 'return', return_value)
|
| 329 |
-
hidden_method_name = HiddenField(name=f'hidden_name', description=docstring, render_kw={"value": f'{attr_name}'})
|
| 330 |
-
setattr(dynamic_form, 'hidden_name', hidden_method_name)
|
| 331 |
-
return dynamic_form
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
def create_form_from_module(sdl_module, autofill: bool = False, script=None, design: bool = False):
|
| 335 |
-
"""
|
| 336 |
-
Create forms for each method, used for control routes
|
| 337 |
-
:param sdl_module: method module
|
| 338 |
-
:param autofill:bool if autofill is enabled
|
| 339 |
-
:param script:Script object
|
| 340 |
-
:param design: if design is enabled
|
| 341 |
-
"""
|
| 342 |
-
method_forms = {}
|
| 343 |
-
for attr_name in dir(sdl_module):
|
| 344 |
-
method = getattr(sdl_module, attr_name)
|
| 345 |
-
if inspect.ismethod(method) and not attr_name.startswith('_'):
|
| 346 |
-
signature = inspect.signature(method)
|
| 347 |
-
docstring = inspect.getdoc(method)
|
| 348 |
-
attr = dict(signature=signature, docstring=docstring)
|
| 349 |
-
form_class = create_add_form(attr, attr_name, autofill, script, design)
|
| 350 |
-
method_forms[attr_name] = form_class()
|
| 351 |
-
return method_forms
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
def create_form_from_pseudo(pseudo: dict, autofill: bool, script=None, design=True):
|
| 355 |
-
"""
|
| 356 |
-
Create forms for pseudo method, used for design routes
|
| 357 |
-
:param pseudo:{'dose_liquid': {
|
| 358 |
-
"docstring": "some docstring",
|
| 359 |
-
"signature": Signature(amount_in_ml: float, rate_ml_per_minute: float) }
|
| 360 |
-
}
|
| 361 |
-
:param autofill:bool if autofill is enabled
|
| 362 |
-
:param script:Script object
|
| 363 |
-
:param design: if design is enabled
|
| 364 |
-
"""
|
| 365 |
-
method_forms = {}
|
| 366 |
-
for attr_name, signature in pseudo.items():
|
| 367 |
-
# signature = info.get('signature', {})
|
| 368 |
-
form_class = create_add_form(signature, attr_name, autofill, script, design)
|
| 369 |
-
method_forms[attr_name] = form_class()
|
| 370 |
-
return method_forms
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
def create_form_from_action(action: dict, script=None, design=True):
|
| 374 |
-
'''
|
| 375 |
-
Create forms for single action, used for design routes
|
| 376 |
-
:param action: {'action': 'dose_solid', 'arg_types': {'amount_in_mg': 'float', 'bring_in': 'bool'},
|
| 377 |
-
'args': {'amount_in_mg': 5.0, 'bring_in': False}, 'id': 9,
|
| 378 |
-
'instrument': 'deck.sdl', 'return': '', 'uuid': 266929188668995}
|
| 379 |
-
:param script:Script object
|
| 380 |
-
:param design: if design is enabled
|
| 381 |
-
|
| 382 |
-
'''
|
| 383 |
-
|
| 384 |
-
arg_types = action.get("arg_types", {})
|
| 385 |
-
args = action.get("args", {})
|
| 386 |
-
save_as = action.get("return")
|
| 387 |
-
|
| 388 |
-
class DynamicForm(FlaskForm):
|
| 389 |
-
pass
|
| 390 |
-
|
| 391 |
-
annotation_mapping = {
|
| 392 |
-
"int": (VariableOrIntField if design else IntegerField, 'Enter integer value'),
|
| 393 |
-
"float": (VariableOrFloatField if design else FloatField, 'Enter numeric value'),
|
| 394 |
-
"str": (VariableOrStringField if design else StringField, 'Enter text'),
|
| 395 |
-
"bool": (VariableOrBoolField if design else BooleanField, 'Empty for false')
|
| 396 |
-
}
|
| 397 |
-
|
| 398 |
-
for name, param_type in arg_types.items():
|
| 399 |
-
formatted_param_name = format_name(name)
|
| 400 |
-
value = args.get(name, "")
|
| 401 |
-
if type(value) is dict:
|
| 402 |
-
value = next(iter(value))
|
| 403 |
-
field_kwargs = {
|
| 404 |
-
"label": formatted_param_name,
|
| 405 |
-
"default": f'{value}',
|
| 406 |
-
"validators": [InputRequired()],
|
| 407 |
-
**({"script": script})
|
| 408 |
-
}
|
| 409 |
-
param_type = param_type if type(param_type) is str else f"{param_type}"
|
| 410 |
-
field_class, placeholder_text = annotation_mapping.get(
|
| 411 |
-
param_type,
|
| 412 |
-
(VariableOrStringField if design else StringField, f'Enter {param_type} value')
|
| 413 |
-
)
|
| 414 |
-
render_kwargs = {"placeholder": placeholder_text}
|
| 415 |
-
|
| 416 |
-
# Create the field with additional rendering kwargs for placeholder text
|
| 417 |
-
field = field_class(**field_kwargs, render_kw=render_kwargs)
|
| 418 |
-
setattr(DynamicForm, name, field)
|
| 419 |
-
|
| 420 |
-
if design:
|
| 421 |
-
return_value = StringField(label='Save value as', default=f"{save_as}", render_kw={"placeholder": "Optional"})
|
| 422 |
-
setattr(DynamicForm, 'return', return_value)
|
| 423 |
-
return DynamicForm()
|
| 424 |
-
|
| 425 |
-
def create_all_builtin_forms(script):
|
| 426 |
-
all_builtin_forms = {}
|
| 427 |
-
for logic_name in ['if', 'while', 'variable', 'wait', 'repeat']:
|
| 428 |
-
# signature = info.get('signature', {})
|
| 429 |
-
form_class = create_builtin_form(logic_name, script)
|
| 430 |
-
all_builtin_forms[logic_name] = form_class()
|
| 431 |
-
return all_builtin_forms
|
| 432 |
-
|
| 433 |
-
def create_builtin_form(logic_type, script):
|
| 434 |
-
"""
|
| 435 |
-
Create a builtin form {if, while, variable, repeat, wait}
|
| 436 |
-
"""
|
| 437 |
-
class BuiltinFunctionForm(FlaskForm):
|
| 438 |
-
pass
|
| 439 |
-
|
| 440 |
-
placeholder_text = {
|
| 441 |
-
'wait': 'Enter second',
|
| 442 |
-
'repeat': 'Enter an integer'
|
| 443 |
-
}.get(logic_type, 'Enter statement')
|
| 444 |
-
description_text = {
|
| 445 |
-
'variable': 'Your variable can be numbers, boolean (True or False) or text ("text")',
|
| 446 |
-
}.get(logic_type, '')
|
| 447 |
-
field_class = {
|
| 448 |
-
'wait': VariableOrFloatField,
|
| 449 |
-
'repeat': VariableOrIntField
|
| 450 |
-
}.get(logic_type, VariableOrStringField) # Default to StringField as a fallback
|
| 451 |
-
field_kwargs = {
|
| 452 |
-
"label": f'statement',
|
| 453 |
-
"validators": [InputRequired()] if logic_type in ['wait', "variable"] else [],
|
| 454 |
-
"description": description_text,
|
| 455 |
-
"script": script
|
| 456 |
-
}
|
| 457 |
-
render_kwargs = {"placeholder": placeholder_text}
|
| 458 |
-
field = field_class(**field_kwargs, render_kw=render_kwargs)
|
| 459 |
-
setattr(BuiltinFunctionForm, "statement", field)
|
| 460 |
-
if logic_type == 'variable':
|
| 461 |
-
variable_field = StringField(label=f'variable', validators=[InputRequired()],
|
| 462 |
-
description="Your variable name cannot include space",
|
| 463 |
-
render_kw=render_kwargs)
|
| 464 |
-
type_field = SelectField(
|
| 465 |
-
'Select Input Type',
|
| 466 |
-
choices=[('int', 'Integer'), ('float', 'Float'), ('str', 'String'), ('bool', 'Boolean')],
|
| 467 |
-
default='str' # Optional default value
|
| 468 |
-
)
|
| 469 |
-
setattr(BuiltinFunctionForm, "variable", variable_field)
|
| 470 |
-
setattr(BuiltinFunctionForm, "type", type_field)
|
| 471 |
-
hidden_field = HiddenField(name=f'builtin_name', render_kw={"value": f'{logic_type}'})
|
| 472 |
-
setattr(BuiltinFunctionForm, "builtin_name", hidden_field)
|
| 473 |
-
return BuiltinFunctionForm
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
def get_method_from_workflow(function_string):
|
| 477 |
-
"""Creates a function from a string and assigns it a new name."""
|
| 478 |
-
|
| 479 |
-
namespace = {}
|
| 480 |
-
exec(function_string, globals(), namespace) # Execute the string in a safe namespace
|
| 481 |
-
func_name = next(iter(namespace))
|
| 482 |
-
# Get the function name dynamically
|
| 483 |
-
return namespace[func_name]
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
def create_workflow_forms(script, autofill: bool = False, design: bool = False):
|
| 487 |
-
workflow_forms = {}
|
| 488 |
-
functions = {}
|
| 489 |
-
class RegisteredWorkflows:
|
| 490 |
-
pass
|
| 491 |
-
|
| 492 |
-
deck_name = script.deck
|
| 493 |
-
workflows = Script.query.filter(Script.deck==deck_name, Script.name != script.name).all()
|
| 494 |
-
for workflow in workflows:
|
| 495 |
-
compiled_strs = workflow.compile().get('script', "")
|
| 496 |
-
method = get_method_from_workflow(compiled_strs)
|
| 497 |
-
functions[workflow.name] = dict(signature=inspect.signature(method), docstring=inspect.getdoc(method))
|
| 498 |
-
setattr(RegisteredWorkflows, workflow.name, method)
|
| 499 |
-
|
| 500 |
-
form_class = create_form_for_method(method, autofill, script, design)
|
| 501 |
-
|
| 502 |
-
hidden_method_name = HiddenField(name=f'hidden_name', description="",
|
| 503 |
-
render_kw={"value": f'{workflow.name}'})
|
| 504 |
-
if design:
|
| 505 |
-
return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
|
| 506 |
-
setattr(form_class, 'return', return_value)
|
| 507 |
-
setattr(form_class, 'workflow_name', hidden_method_name)
|
| 508 |
-
workflow_forms[workflow.name] = form_class()
|
| 509 |
-
global_config.registered_workflows = RegisteredWorkflows
|
| 510 |
-
return workflow_forms, functions
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
def create_action_button(script, stype=None):
|
| 514 |
-
"""
|
| 515 |
-
Creates action buttons for design route (design canvas)
|
| 516 |
-
:param script: Script object
|
| 517 |
-
:param stype: script type (script, prep, cleanup)
|
| 518 |
-
"""
|
| 519 |
-
stype = stype or script.editing_type
|
| 520 |
-
variables = script.get_variables()
|
| 521 |
-
return [_action_button(i, variables) for i in script.get_script(stype)]
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
def _action_button(action: dict, variables: dict):
|
| 525 |
-
"""
|
| 526 |
-
Creates action button for one action
|
| 527 |
-
:param action: Action dict
|
| 528 |
-
:param variables: created variable dict
|
| 529 |
-
"""
|
| 530 |
-
style = {
|
| 531 |
-
"repeat": "background-color: lightsteelblue",
|
| 532 |
-
"if": "background-color: salmon",
|
| 533 |
-
"while": "background-color: salmon",
|
| 534 |
-
}.get(action['instrument'], "")
|
| 535 |
-
|
| 536 |
-
if action['instrument'] in ['if', 'while', 'repeat']:
|
| 537 |
-
text = f"{action['action']} {action['args'].get('statement', '')}"
|
| 538 |
-
elif action['instrument'] == 'variable':
|
| 539 |
-
text = f"{action['action']} = {action['args'].get('statement')}"
|
| 540 |
-
else:
|
| 541 |
-
# regular action button
|
| 542 |
-
prefix = f"{action['return']} = " if action['return'] else ""
|
| 543 |
-
action_text = f"{action['instrument'].split('.')[-1] if action['instrument'].startswith('deck') else action['instrument']}.{action['action']}"
|
| 544 |
-
arg_string = ""
|
| 545 |
-
if action['args']:
|
| 546 |
-
if type(action['args']) is dict:
|
| 547 |
-
arg_list = []
|
| 548 |
-
for k, v in action['args'].items():
|
| 549 |
-
if isinstance(v, dict):
|
| 550 |
-
value = next(iter(v)) # Extract the first key if it's a dict
|
| 551 |
-
# show warning color for variable calling when there is no definition
|
| 552 |
-
style = "background-color: khaki" if value not in variables.keys() else ""
|
| 553 |
-
else:
|
| 554 |
-
value = v # Keep the original value if not a dict
|
| 555 |
-
arg_list.append(f"{k} = {value}") # Format the key-value pair
|
| 556 |
-
arg_string = "(" + ", ".join(arg_list) + ")"
|
| 557 |
-
else:
|
| 558 |
-
arg_string = f"= {action['args']}"
|
| 559 |
-
text = f"{prefix}{action_text} {arg_string}"
|
| 560 |
-
return dict(label=text, style=style, uuid=action["uuid"], id=action["id"], instrument=action['instrument'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/global_config.py
DELETED
|
@@ -1,87 +0,0 @@
|
|
| 1 |
-
import threading
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
class GlobalConfig:
|
| 5 |
-
_instance = None
|
| 6 |
-
|
| 7 |
-
def __new__(cls, *args, **kwargs):
|
| 8 |
-
if cls._instance is None:
|
| 9 |
-
cls._instance = super(GlobalConfig, cls).__new__(cls, *args, **kwargs)
|
| 10 |
-
cls._instance._deck = None
|
| 11 |
-
cls._instance._registered_workflows = None
|
| 12 |
-
cls._instance._agent = None
|
| 13 |
-
cls._instance._defined_variables = {}
|
| 14 |
-
cls._instance._api_variables = set()
|
| 15 |
-
cls._instance._deck_snapshot = {}
|
| 16 |
-
cls._instance._runner_lock = threading.Lock()
|
| 17 |
-
cls._instance._runner_status = None
|
| 18 |
-
return cls._instance
|
| 19 |
-
|
| 20 |
-
@property
|
| 21 |
-
def deck(self):
|
| 22 |
-
return self._deck
|
| 23 |
-
|
| 24 |
-
@deck.setter
|
| 25 |
-
def deck(self, value):
|
| 26 |
-
if self._deck is None:
|
| 27 |
-
self._deck = value
|
| 28 |
-
|
| 29 |
-
@property
|
| 30 |
-
def registered_workflows(self):
|
| 31 |
-
return self._registered_workflows
|
| 32 |
-
|
| 33 |
-
@registered_workflows.setter
|
| 34 |
-
def registered_workflows(self, value):
|
| 35 |
-
if self._registered_workflows is None:
|
| 36 |
-
self._registered_workflows = value
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
@property
|
| 40 |
-
def deck_snapshot(self):
|
| 41 |
-
return self._deck_snapshot
|
| 42 |
-
|
| 43 |
-
@deck_snapshot.setter
|
| 44 |
-
def deck_snapshot(self, value):
|
| 45 |
-
self._deck_snapshot = value
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
@property
|
| 49 |
-
def agent(self):
|
| 50 |
-
return self._agent
|
| 51 |
-
|
| 52 |
-
@agent.setter
|
| 53 |
-
def agent(self, value):
|
| 54 |
-
if self._agent is None:
|
| 55 |
-
self._agent = value
|
| 56 |
-
|
| 57 |
-
@property
|
| 58 |
-
def defined_variables(self):
|
| 59 |
-
return self._defined_variables
|
| 60 |
-
|
| 61 |
-
@defined_variables.setter
|
| 62 |
-
def defined_variables(self, value):
|
| 63 |
-
self._defined_variables = value
|
| 64 |
-
|
| 65 |
-
@property
|
| 66 |
-
def api_variables(self):
|
| 67 |
-
return self._api_variables
|
| 68 |
-
|
| 69 |
-
@api_variables.setter
|
| 70 |
-
def api_variables(self, value):
|
| 71 |
-
self._api_variables = value
|
| 72 |
-
|
| 73 |
-
@property
|
| 74 |
-
def runner_lock(self):
|
| 75 |
-
return self._runner_lock
|
| 76 |
-
|
| 77 |
-
@runner_lock.setter
|
| 78 |
-
def runner_lock(self, value):
|
| 79 |
-
self._runner_lock = value
|
| 80 |
-
|
| 81 |
-
@property
|
| 82 |
-
def runner_status(self):
|
| 83 |
-
return self._runner_status
|
| 84 |
-
|
| 85 |
-
@runner_status.setter
|
| 86 |
-
def runner_status(self, value):
|
| 87 |
-
self._runner_status = value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/input_types.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
## Supported Input Types
|
| 2 |
-
### **Commonly Used Types**
|
| 3 |
-
| Type | Example Input | Example Conversion |
|
| 4 |
-
|---------|------------------------|--------------------------------------|
|
| 5 |
-
| `int` | `"42"` | `42` |
|
| 6 |
-
| `float` | `"3.14"` | `3.14` |
|
| 7 |
-
| `str` | `"hello"` | `"hello"` |
|
| 8 |
-
| `bool` | `"True"` / `"False"` | `True` / `False` |
|
| 9 |
-
| `list ` | `"1,2,3"` | `[1, 2, 3]` |
|
| 10 |
-
| `tuple` | `"1,hello,3.5"` | `(1, "hello", 3.5)` |
|
| 11 |
-
| `set` | `"1,2,3"` | `{1, 2, 3}` |
|
| 12 |
-
| `Any` | `"anything"` | `"anything"` (no conversion applied) |
|
| 13 |
-
|
| 14 |
-
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/llm_agent.py
DELETED
|
@@ -1,183 +0,0 @@
|
|
| 1 |
-
import inspect
|
| 2 |
-
import json
|
| 3 |
-
import os
|
| 4 |
-
import re
|
| 5 |
-
|
| 6 |
-
from openai import OpenAI
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
# from dotenv import load_dotenv
|
| 10 |
-
# load_dotenv()
|
| 11 |
-
|
| 12 |
-
# host = "137.82.65.246"
|
| 13 |
-
# model = "llama3"
|
| 14 |
-
|
| 15 |
-
# structured output,
|
| 16 |
-
# class Action(BaseModel):
|
| 17 |
-
# action: str
|
| 18 |
-
# args: dict
|
| 19 |
-
# arg_types: dict
|
| 20 |
-
#
|
| 21 |
-
#
|
| 22 |
-
# class ActionPlan(BaseModel):
|
| 23 |
-
# actions: list[Action]
|
| 24 |
-
# # final_answer: str
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
class LlmAgent:
|
| 28 |
-
def __init__(self, model="llama3", output_path=os.curdir, host=None):
|
| 29 |
-
self.host = host
|
| 30 |
-
self.base_url = f"http://{self.host}:11434/v1/" if host is not None else ""
|
| 31 |
-
self.model = model
|
| 32 |
-
self.output_path = os.path.join(output_path, "llm_output") if output_path is not None else None
|
| 33 |
-
self.client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) if host is None else OpenAI(api_key="ollama",
|
| 34 |
-
base_url=self.base_url)
|
| 35 |
-
if self.output_path is not None:
|
| 36 |
-
os.makedirs(self.output_path, exist_ok=True)
|
| 37 |
-
|
| 38 |
-
@staticmethod
|
| 39 |
-
def extract_annotations_docstrings(module_sigs):
|
| 40 |
-
class_str = ""
|
| 41 |
-
|
| 42 |
-
for name, value in module_sigs.items():
|
| 43 |
-
signature = value.get("signature")
|
| 44 |
-
docstring = value.get("docstring")
|
| 45 |
-
class_str += f'\tdef {name}{signature}:\n'
|
| 46 |
-
class_str += f'\t\t"""\n\t\t{docstring}\n\t\t"""' + '\n' if docstring else ''
|
| 47 |
-
class_str = class_str.replace('self, ', '')
|
| 48 |
-
class_str = class_str.replace('self', '')
|
| 49 |
-
name_list = list(module_sigs.keys())
|
| 50 |
-
# print(class_str)
|
| 51 |
-
# with open(os.path.join(self.output_path, "docstring_manual.txt"), "w") as f:
|
| 52 |
-
# f.write(class_str)
|
| 53 |
-
return class_str, name_list
|
| 54 |
-
|
| 55 |
-
@staticmethod
|
| 56 |
-
def parse_code_from_msg(msg):
|
| 57 |
-
msg = msg.strip()
|
| 58 |
-
# print(msg)
|
| 59 |
-
# code_blocks = re.findall(r'```(?:json\s)?(.*?)```', msg, re.DOTALL)
|
| 60 |
-
code_blocks = re.findall(r'\[\s*\{.*?\}\s*\]', msg, re.DOTALL)
|
| 61 |
-
|
| 62 |
-
json_blocks = []
|
| 63 |
-
for block in code_blocks:
|
| 64 |
-
if not block.startswith('['):
|
| 65 |
-
start_index = block.find('[')
|
| 66 |
-
block = block[start_index:]
|
| 67 |
-
block = re.sub(r'//.*', '', block)
|
| 68 |
-
block = block.replace('True', 'true').replace('False', 'false')
|
| 69 |
-
try:
|
| 70 |
-
# Try to parse the block as JSON
|
| 71 |
-
json_data = json.loads(block.strip())
|
| 72 |
-
if isinstance(json_data, list):
|
| 73 |
-
json_blocks = json_data
|
| 74 |
-
except json.JSONDecodeError:
|
| 75 |
-
continue
|
| 76 |
-
return json_blocks
|
| 77 |
-
|
| 78 |
-
def _generate(self, robot_sigs, prompt):
|
| 79 |
-
# deck_info, name_list = self.extract_annotations_docstrings(type(robot))
|
| 80 |
-
deck_info, name_list = self.extract_annotations_docstrings(robot_sigs)
|
| 81 |
-
full_prompt = '''I have some python functions, for example when calling them I want to write them using JSON,
|
| 82 |
-
it is necessary to include all args
|
| 83 |
-
for example
|
| 84 |
-
def dose_solid(amount_in_mg:float, bring_in:bool=True): def analyze():
|
| 85 |
-
dose_solid(3)
|
| 86 |
-
analyze()
|
| 87 |
-
I would want to write to
|
| 88 |
-
[
|
| 89 |
-
{
|
| 90 |
-
"action": "dose_solid",
|
| 91 |
-
"arg_types": {
|
| 92 |
-
"amount_in_mg": "float",
|
| 93 |
-
"bring_in": "bool"
|
| 94 |
-
},
|
| 95 |
-
"args": {
|
| 96 |
-
"amount_in_mg": 3,
|
| 97 |
-
"bring_in": true
|
| 98 |
-
}
|
| 99 |
-
},
|
| 100 |
-
{
|
| 101 |
-
"action": "analyze",
|
| 102 |
-
"arg_types": {},
|
| 103 |
-
"args": {}
|
| 104 |
-
}
|
| 105 |
-
]
|
| 106 |
-
''' + f'''
|
| 107 |
-
Now these are my callable functions,
|
| 108 |
-
{deck_info}
|
| 109 |
-
and I want you to find the most appropriate function if I want to do these tasks
|
| 110 |
-
"""{prompt}"""
|
| 111 |
-
,and write a list of dictionary in json accordingly. Please only use these action names {name_list},
|
| 112 |
-
can you also help find the default value you can't find the info from my request.
|
| 113 |
-
'''
|
| 114 |
-
if self.output_path is not None:
|
| 115 |
-
with open(os.path.join(self.output_path, "prompt.txt"), "w") as f:
|
| 116 |
-
f.write(full_prompt)
|
| 117 |
-
messages = [{"role": "user",
|
| 118 |
-
"content": full_prompt}, ]
|
| 119 |
-
# if self.host == "openai":
|
| 120 |
-
output = self.client.chat.completions.create(
|
| 121 |
-
messages=messages,
|
| 122 |
-
model=self.model,
|
| 123 |
-
# response_format={"type": "json_object"},
|
| 124 |
-
)
|
| 125 |
-
msg = output.choices[0].message.content
|
| 126 |
-
# msg = output.choices[0].message.parsed
|
| 127 |
-
|
| 128 |
-
code = self.parse_code_from_msg(msg)
|
| 129 |
-
code = [action for action in code if action.get('action', '') in name_list]
|
| 130 |
-
# print('\033[91m', code, '\033[0m')
|
| 131 |
-
return code
|
| 132 |
-
|
| 133 |
-
def generate_code(self, robot_signature, prompt, attempt_allowance: int = 3):
|
| 134 |
-
attempt = 0
|
| 135 |
-
|
| 136 |
-
while attempt < attempt_allowance:
|
| 137 |
-
_code = self._generate(robot_signature, prompt)
|
| 138 |
-
attempt += 1
|
| 139 |
-
if _code:
|
| 140 |
-
break
|
| 141 |
-
|
| 142 |
-
return self.fill_blanks(_code, robot_signature)
|
| 143 |
-
# return code
|
| 144 |
-
|
| 145 |
-
@staticmethod
|
| 146 |
-
def fill_blanks(actions, robot_signature):
|
| 147 |
-
for action in actions:
|
| 148 |
-
action_name = action['action']
|
| 149 |
-
action_signature = robot_signature.get(action_name).get('signature', {})
|
| 150 |
-
args = action.get("args", {})
|
| 151 |
-
arg_types = action.get("arg_types", {})
|
| 152 |
-
for param in action_signature.parameters.values():
|
| 153 |
-
if param.name == 'self':
|
| 154 |
-
continue
|
| 155 |
-
if param.name not in args:
|
| 156 |
-
args[param.name] = param.default if param.default is not param.empty else ''
|
| 157 |
-
arg_types[param.name] = param.annotation.__name__
|
| 158 |
-
action['args'] = args
|
| 159 |
-
action['arg_types'] = arg_types
|
| 160 |
-
return actions
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
if __name__ == "__main__":
|
| 164 |
-
from pprint import pprint
|
| 165 |
-
from example.abstract_sdl_example.abstract_sdl import deck
|
| 166 |
-
|
| 167 |
-
from utils import parse_functions
|
| 168 |
-
|
| 169 |
-
deck_sig = parse_functions(deck, doc_string=True)
|
| 170 |
-
# llm_agent = LlmAgent(host="openai", model="gpt-3.5-turbo")
|
| 171 |
-
llm_agent = LlmAgent(host="localhost", model="llama3.1")
|
| 172 |
-
# robot = IrohDeck()
|
| 173 |
-
# extract_annotations_docstrings(DummySDLDeck)
|
| 174 |
-
prompt = '''I want to start with dosing 10 mg of current sample, and add 1 mL of toluene
|
| 175 |
-
and equilibrate for 10 minute at 40 degrees, then sample 20 ul of sample to analyze with hplc, and save result'''
|
| 176 |
-
code = llm_agent.generate_code(deck_sig, prompt)
|
| 177 |
-
pprint(code)
|
| 178 |
-
|
| 179 |
-
"""
|
| 180 |
-
I want to dose 10mg, 6mg, 4mg, 3mg, 2mg, 1mg to 6 vials
|
| 181 |
-
I want to add 10 mg to vial a3, and 10 ml of liquid, then shake them for 3 minutes
|
| 182 |
-
|
| 183 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/script_runner.py
DELETED
|
@@ -1,336 +0,0 @@
|
|
| 1 |
-
import ast
|
| 2 |
-
import os
|
| 3 |
-
import csv
|
| 4 |
-
import threading
|
| 5 |
-
import time
|
| 6 |
-
from datetime import datetime
|
| 7 |
-
|
| 8 |
-
from ivoryos.utils import utils, bo_campaign
|
| 9 |
-
from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db, SingleStep
|
| 10 |
-
from ivoryos.utils.global_config import GlobalConfig
|
| 11 |
-
|
| 12 |
-
global_config = GlobalConfig()
|
| 13 |
-
global deck
|
| 14 |
-
deck = None
|
| 15 |
-
# global deck, registered_workflows
|
| 16 |
-
# deck, registered_workflows = None, None
|
| 17 |
-
|
| 18 |
-
class ScriptRunner:
|
| 19 |
-
def __init__(self, globals_dict=None):
|
| 20 |
-
self.retry = False
|
| 21 |
-
if globals_dict is None:
|
| 22 |
-
globals_dict = globals()
|
| 23 |
-
self.globals_dict = globals_dict
|
| 24 |
-
self.pause_event = threading.Event() # A threading event to manage pause/resume
|
| 25 |
-
self.pause_event.set()
|
| 26 |
-
self.stop_pending_event = threading.Event()
|
| 27 |
-
self.stop_current_event = threading.Event()
|
| 28 |
-
self.is_running = False
|
| 29 |
-
self.lock = global_config.runner_lock
|
| 30 |
-
self.paused = False
|
| 31 |
-
self.current_app = None
|
| 32 |
-
|
| 33 |
-
def toggle_pause(self):
|
| 34 |
-
"""Toggles between pausing and resuming the script"""
|
| 35 |
-
self.paused = not self.paused
|
| 36 |
-
if self.pause_event.is_set():
|
| 37 |
-
self.pause_event.clear() # Pause the script
|
| 38 |
-
return "Paused"
|
| 39 |
-
else:
|
| 40 |
-
self.pause_event.set() # Resume the script
|
| 41 |
-
return "Resumed"
|
| 42 |
-
|
| 43 |
-
def pause_status(self):
|
| 44 |
-
"""Toggles between pausing and resuming the script"""
|
| 45 |
-
return self.paused
|
| 46 |
-
|
| 47 |
-
def reset_stop_event(self):
|
| 48 |
-
"""Resets the stop event"""
|
| 49 |
-
self.stop_pending_event.clear()
|
| 50 |
-
self.stop_current_event.clear()
|
| 51 |
-
self.pause_event.set()
|
| 52 |
-
|
| 53 |
-
def abort_pending(self):
|
| 54 |
-
"""Abort the pending iteration after the current is finished"""
|
| 55 |
-
self.stop_pending_event.set()
|
| 56 |
-
# print("Stop pending tasks")
|
| 57 |
-
|
| 58 |
-
def stop_execution(self):
|
| 59 |
-
"""Force stop everything, including ongoing tasks."""
|
| 60 |
-
self.stop_current_event.set()
|
| 61 |
-
self.abort_pending()
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
|
| 65 |
-
output_path="", compiled=False, current_app=None):
|
| 66 |
-
global deck
|
| 67 |
-
if deck is None:
|
| 68 |
-
deck = global_config.deck
|
| 69 |
-
|
| 70 |
-
if self.current_app is None:
|
| 71 |
-
self.current_app = current_app
|
| 72 |
-
# time.sleep(1) # Optional: may help ensure deck readiness
|
| 73 |
-
|
| 74 |
-
# Try to acquire lock without blocking
|
| 75 |
-
if not self.lock.acquire(blocking=False):
|
| 76 |
-
if logger:
|
| 77 |
-
logger.info("System is busy. Please wait for it to finish or stop it before starting a new one.")
|
| 78 |
-
return None
|
| 79 |
-
|
| 80 |
-
self.reset_stop_event()
|
| 81 |
-
|
| 82 |
-
thread = threading.Thread(
|
| 83 |
-
target=self._run_with_stop_check,
|
| 84 |
-
args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app, compiled)
|
| 85 |
-
)
|
| 86 |
-
thread.start()
|
| 87 |
-
return thread
|
| 88 |
-
|
| 89 |
-
def exec_steps(self, script, section_name, logger, socketio, run_id, i_progress, **kwargs):
|
| 90 |
-
"""
|
| 91 |
-
Executes a function defined in a string line by line
|
| 92 |
-
:param func_str: The function as a string
|
| 93 |
-
:param kwargs: Arguments to pass to the function
|
| 94 |
-
:return: The final result of the function execution
|
| 95 |
-
"""
|
| 96 |
-
_func_str = script.python_script or script.compile()
|
| 97 |
-
step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
| 98 |
-
global deck
|
| 99 |
-
# global deck, registered_workflows
|
| 100 |
-
if deck is None:
|
| 101 |
-
deck = global_config.deck
|
| 102 |
-
# if registered_workflows is None:
|
| 103 |
-
# registered_workflows = global_config.registered_workflows
|
| 104 |
-
|
| 105 |
-
# for i, line in enumerate(step_list):
|
| 106 |
-
# if line.startswith("registered_workflows"):
|
| 107 |
-
#
|
| 108 |
-
# func_str = script.compile()
|
| 109 |
-
# Parse function body from string
|
| 110 |
-
temp_connections = global_config.defined_variables
|
| 111 |
-
# Prepare execution environment
|
| 112 |
-
exec_globals = {"deck": deck, "time":time} # Add required global objects
|
| 113 |
-
# exec_globals = {"deck": deck, "time": time, "registered_workflows":registered_workflows} # Add required global objects
|
| 114 |
-
exec_globals.update(temp_connections)
|
| 115 |
-
exec_locals = {} # Local execution scope
|
| 116 |
-
|
| 117 |
-
# Define function arguments manually in exec_locals
|
| 118 |
-
exec_locals.update(kwargs)
|
| 119 |
-
index = 0
|
| 120 |
-
|
| 121 |
-
# Execute each line dynamically
|
| 122 |
-
while index < len(step_list):
|
| 123 |
-
if self.stop_current_event.is_set():
|
| 124 |
-
logger.info(f'Stopping execution during {section_name}')
|
| 125 |
-
step = WorkflowStep(
|
| 126 |
-
workflow_id=run_id,
|
| 127 |
-
phase=section_name,
|
| 128 |
-
repeat_index=i_progress,
|
| 129 |
-
step_index=index,
|
| 130 |
-
method_name="stop",
|
| 131 |
-
start_time=datetime.now(),
|
| 132 |
-
end_time=datetime.now(),
|
| 133 |
-
run_error=False,
|
| 134 |
-
)
|
| 135 |
-
db.session.add(step)
|
| 136 |
-
break
|
| 137 |
-
line = step_list[index]
|
| 138 |
-
method_name = line.strip().split("(")[0] if "(" in line else line.strip()
|
| 139 |
-
start_time = datetime.now()
|
| 140 |
-
step = WorkflowStep(
|
| 141 |
-
workflow_id=run_id,
|
| 142 |
-
phase=section_name,
|
| 143 |
-
repeat_index=i_progress,
|
| 144 |
-
step_index=index,
|
| 145 |
-
method_name=method_name,
|
| 146 |
-
start_time=start_time,
|
| 147 |
-
)
|
| 148 |
-
db.session.add(step)
|
| 149 |
-
db.session.commit()
|
| 150 |
-
logger.info(f"Executing: {line}")
|
| 151 |
-
socketio.emit('execution', {'section': f"{section_name}-{index}"})
|
| 152 |
-
# self._emit_progress(socketio, 100)
|
| 153 |
-
# if line.startswith("registered_workflows"):
|
| 154 |
-
# line = line.replace("registered_workflows.", "")
|
| 155 |
-
try:
|
| 156 |
-
if line.startswith("time.sleep("): # add safe sleep for time.sleep lines
|
| 157 |
-
duration_str = line[len("time.sleep("):-1]
|
| 158 |
-
duration = float(duration_str)
|
| 159 |
-
self.safe_sleep(duration)
|
| 160 |
-
else:
|
| 161 |
-
exec(line, exec_globals, exec_locals)
|
| 162 |
-
step.run_error = False
|
| 163 |
-
except Exception as e:
|
| 164 |
-
logger.error(f"Error during script execution: {e}")
|
| 165 |
-
socketio.emit('error', {'message': str(e)})
|
| 166 |
-
|
| 167 |
-
step.run_error = True
|
| 168 |
-
self.toggle_pause()
|
| 169 |
-
step.end_time = datetime.now()
|
| 170 |
-
# db.session.add(step)
|
| 171 |
-
db.session.commit()
|
| 172 |
-
|
| 173 |
-
self.pause_event.wait()
|
| 174 |
-
|
| 175 |
-
# todo update script during the run
|
| 176 |
-
# _func_str = script.compile()
|
| 177 |
-
# step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
| 178 |
-
if not step.run_error:
|
| 179 |
-
index += 1
|
| 180 |
-
elif not self.retry:
|
| 181 |
-
index += 1
|
| 182 |
-
return exec_locals # Return the 'results' variable
|
| 183 |
-
|
| 184 |
-
def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
|
| 185 |
-
output_path, current_app, compiled):
|
| 186 |
-
time.sleep(1)
|
| 187 |
-
# _func_str = script.compile()
|
| 188 |
-
# step_list_dict: dict = script.convert_to_lines(_func_str)
|
| 189 |
-
self._emit_progress(socketio, 1)
|
| 190 |
-
|
| 191 |
-
# Run "prep" section once
|
| 192 |
-
script_dict = script.script_dict
|
| 193 |
-
with current_app.app_context():
|
| 194 |
-
|
| 195 |
-
run = WorkflowRun(name=script.name or "untitled", platform=script.deck or "deck",start_time=datetime.now())
|
| 196 |
-
db.session.add(run)
|
| 197 |
-
db.session.commit()
|
| 198 |
-
run_id = run.id # Save the ID
|
| 199 |
-
global_config.runner_status = {"id":run_id, "type": "workflow"}
|
| 200 |
-
self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id)
|
| 201 |
-
output_list = []
|
| 202 |
-
_, arg_type = script.config("script")
|
| 203 |
-
_, return_list = script.config_return()
|
| 204 |
-
|
| 205 |
-
# Run "script" section multiple times
|
| 206 |
-
if repeat_count:
|
| 207 |
-
self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
|
| 208 |
-
run_name, return_list, compiled, logger, socketio, run_id=run_id)
|
| 209 |
-
elif config:
|
| 210 |
-
self._run_config_section(config, arg_type, output_list, script, run_name, logger,
|
| 211 |
-
socketio, run_id=run_id, compiled=compiled)
|
| 212 |
-
|
| 213 |
-
# Run "cleanup" section once
|
| 214 |
-
self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
|
| 215 |
-
# Reset the running flag when done
|
| 216 |
-
self.lock.release()
|
| 217 |
-
# Save results if necessary
|
| 218 |
-
filename = None
|
| 219 |
-
if not script.python_script and output_list:
|
| 220 |
-
filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
|
| 221 |
-
self._emit_progress(socketio, 100)
|
| 222 |
-
with current_app.app_context():
|
| 223 |
-
run = db.session.get(WorkflowRun, run_id) # SQLAlchemy 1.4+ recommended method
|
| 224 |
-
run.end_time = datetime.now()
|
| 225 |
-
run.data_path = filename
|
| 226 |
-
db.session.commit()
|
| 227 |
-
|
| 228 |
-
def _run_actions(self, script, section_name="", logger=None, socketio=None, run_id=None):
|
| 229 |
-
_func_str = script.python_script or script.compile()
|
| 230 |
-
step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
| 231 |
-
logger.info(f'Executing {section_name} steps') if step_list else logger.info(f'No {section_name} steps')
|
| 232 |
-
if self.stop_pending_event.is_set():
|
| 233 |
-
logger.info(f"Stopping execution during {section_name} section.")
|
| 234 |
-
return
|
| 235 |
-
if step_list:
|
| 236 |
-
self.exec_steps(script, section_name, logger, socketio, run_id=run_id, i_progress=0)
|
| 237 |
-
|
| 238 |
-
def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id, compiled=True):
|
| 239 |
-
if not compiled:
|
| 240 |
-
for i in config:
|
| 241 |
-
try:
|
| 242 |
-
i = utils.convert_config_type(i, arg_type)
|
| 243 |
-
compiled = True
|
| 244 |
-
except Exception as e:
|
| 245 |
-
logger.info(e)
|
| 246 |
-
compiled = False
|
| 247 |
-
break
|
| 248 |
-
if compiled:
|
| 249 |
-
for i, kwargs in enumerate(config):
|
| 250 |
-
kwargs = dict(kwargs)
|
| 251 |
-
if self.stop_pending_event.is_set():
|
| 252 |
-
logger.info(f'Stopping execution during {run_name}: {i + 1}/{len(config)}')
|
| 253 |
-
break
|
| 254 |
-
logger.info(f'Executing {i + 1} of {len(config)} with kwargs = {kwargs}')
|
| 255 |
-
progress = (i + 1) * 100 / len(config)
|
| 256 |
-
self._emit_progress(socketio, progress)
|
| 257 |
-
# fname = f"{run_name}_script"
|
| 258 |
-
# function = self.globals_dict[fname]
|
| 259 |
-
output = self.exec_steps(script, "script", logger, socketio, run_id, i, **kwargs)
|
| 260 |
-
if output:
|
| 261 |
-
# kwargs.update(output)
|
| 262 |
-
output_list.append(output)
|
| 263 |
-
|
| 264 |
-
def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
|
| 265 |
-
logger, socketio, run_id):
|
| 266 |
-
if bo_args:
|
| 267 |
-
logger.info('Initializing optimizer...')
|
| 268 |
-
if compiled:
|
| 269 |
-
ax_client = bo_campaign.ax_init_opc(bo_args)
|
| 270 |
-
else:
|
| 271 |
-
ax_client = bo_campaign.ax_init_form(bo_args, arg_types)
|
| 272 |
-
for i_progress in range(int(repeat_count)):
|
| 273 |
-
if self.stop_pending_event.is_set():
|
| 274 |
-
logger.info(f'Stopping execution during {run_name}: {i_progress + 1}/{int(repeat_count)}')
|
| 275 |
-
break
|
| 276 |
-
logger.info(f'Executing {run_name} experiment: {i_progress + 1}/{int(repeat_count)}')
|
| 277 |
-
progress = (i_progress + 1) * 100 / int(repeat_count) - 0.1
|
| 278 |
-
self._emit_progress(socketio, progress)
|
| 279 |
-
if bo_args:
|
| 280 |
-
try:
|
| 281 |
-
parameters, trial_index = ax_client.get_next_trial()
|
| 282 |
-
logger.info(f'Output value: {parameters}')
|
| 283 |
-
# fname = f"{run_name}_script"
|
| 284 |
-
# function = self.globals_dict[fname]
|
| 285 |
-
output = self.exec_steps(script, "script", logger, socketio, run_id, i_progress, **parameters)
|
| 286 |
-
|
| 287 |
-
_output = {key: value for key, value in output.items() if key in return_list}
|
| 288 |
-
ax_client.complete_trial(trial_index=trial_index, raw_data=_output)
|
| 289 |
-
output.update(parameters)
|
| 290 |
-
except Exception as e:
|
| 291 |
-
logger.info(f'Optimization error: {e}')
|
| 292 |
-
break
|
| 293 |
-
else:
|
| 294 |
-
# fname = f"{run_name}_script"
|
| 295 |
-
# function = self.globals_dict[fname]
|
| 296 |
-
output = self.exec_steps(script, "script", logger, socketio, run_id, i_progress)
|
| 297 |
-
|
| 298 |
-
if output:
|
| 299 |
-
output_list.append(output)
|
| 300 |
-
logger.info(f'Output value: {output}')
|
| 301 |
-
return output_list
|
| 302 |
-
|
| 303 |
-
@staticmethod
|
| 304 |
-
def _save_results(run_name, arg_type, return_list, output_list, logger, output_path):
|
| 305 |
-
args = list(arg_type.keys()) if arg_type else []
|
| 306 |
-
args.extend(return_list)
|
| 307 |
-
filename = run_name + "_" + datetime.now().strftime("%Y-%m-%d %H-%M") + ".csv"
|
| 308 |
-
file_path = os.path.join(output_path, filename)
|
| 309 |
-
with open(file_path, "w", newline='') as file:
|
| 310 |
-
writer = csv.DictWriter(file, fieldnames=args)
|
| 311 |
-
writer.writeheader()
|
| 312 |
-
writer.writerows(output_list)
|
| 313 |
-
logger.info(f'Results saved to {file_path}')
|
| 314 |
-
return filename
|
| 315 |
-
|
| 316 |
-
@staticmethod
|
| 317 |
-
def _emit_progress(socketio, progress):
|
| 318 |
-
socketio.emit('progress', {'progress': progress})
|
| 319 |
-
|
| 320 |
-
def safe_sleep(self, duration: float):
|
| 321 |
-
interval = 1 # check every 1 second
|
| 322 |
-
end_time = time.time() + duration
|
| 323 |
-
while time.time() < end_time:
|
| 324 |
-
if self.stop_current_event.is_set():
|
| 325 |
-
return # Exit early if stop is requested
|
| 326 |
-
time.sleep(min(interval, end_time - time.time()))
|
| 327 |
-
|
| 328 |
-
def get_status(self):
|
| 329 |
-
"""Returns current status of the script runner."""
|
| 330 |
-
with self.current_app.app_context():
|
| 331 |
-
return {
|
| 332 |
-
"is_running": self.lock.locked(),
|
| 333 |
-
"paused": self.paused,
|
| 334 |
-
"stop_pending": self.stop_pending_event.is_set(),
|
| 335 |
-
"stop_current": self.stop_current_event.is_set(),
|
| 336 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/task_runner.py
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
import threading
|
| 2 |
-
import time
|
| 3 |
-
from datetime import datetime
|
| 4 |
-
|
| 5 |
-
from ivoryos.utils.db_models import db, SingleStep
|
| 6 |
-
from ivoryos.utils.global_config import GlobalConfig
|
| 7 |
-
|
| 8 |
-
global_config = GlobalConfig()
|
| 9 |
-
global deck
|
| 10 |
-
deck = None
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class TaskRunner:
|
| 14 |
-
def __init__(self, globals_dict=None):
|
| 15 |
-
self.retry = False
|
| 16 |
-
if globals_dict is None:
|
| 17 |
-
globals_dict = globals()
|
| 18 |
-
self.globals_dict = globals_dict
|
| 19 |
-
self.lock = global_config.runner_lock
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
|
| 23 |
-
global deck
|
| 24 |
-
if deck is None:
|
| 25 |
-
deck = global_config.deck
|
| 26 |
-
|
| 27 |
-
# Try to acquire lock without blocking
|
| 28 |
-
if not self.lock.acquire(blocking=False):
|
| 29 |
-
current_status = global_config.runner_status
|
| 30 |
-
current_status["status"] = "busy"
|
| 31 |
-
return current_status
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
if wait:
|
| 35 |
-
output = self._run_single_step(component, method, kwargs, current_app)
|
| 36 |
-
else:
|
| 37 |
-
print("running with thread")
|
| 38 |
-
thread = threading.Thread(
|
| 39 |
-
target=self._run_single_step, args=(component, method, kwargs, current_app)
|
| 40 |
-
)
|
| 41 |
-
thread.start()
|
| 42 |
-
time.sleep(0.1)
|
| 43 |
-
output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
|
| 44 |
-
|
| 45 |
-
return output
|
| 46 |
-
|
| 47 |
-
def _get_executable(self, component, deck, method):
|
| 48 |
-
if component.startswith("deck."):
|
| 49 |
-
component = component.split(".")[1]
|
| 50 |
-
instrument = getattr(deck, component)
|
| 51 |
-
else:
|
| 52 |
-
temp_connections = global_config.defined_variables
|
| 53 |
-
instrument = temp_connections.get(component)
|
| 54 |
-
function_executable = getattr(instrument, method)
|
| 55 |
-
return function_executable
|
| 56 |
-
|
| 57 |
-
def _run_single_step(self, component, method, kwargs, current_app=None):
|
| 58 |
-
try:
|
| 59 |
-
function_executable = self._get_executable(component, deck, method)
|
| 60 |
-
method_name = f"{function_executable.__self__.__class__.__name__}.{function_executable.__name__}"
|
| 61 |
-
except Exception as e:
|
| 62 |
-
self.lock.release()
|
| 63 |
-
return {"status": "error", "msg": e.__str__()}
|
| 64 |
-
|
| 65 |
-
# with self.lock:
|
| 66 |
-
with current_app.app_context():
|
| 67 |
-
step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=False, start_time=datetime.now())
|
| 68 |
-
db.session.add(step)
|
| 69 |
-
db.session.commit()
|
| 70 |
-
global_config.runner_status = {"id":step.id, "type": "task"}
|
| 71 |
-
try:
|
| 72 |
-
output = function_executable(**kwargs)
|
| 73 |
-
step.output = output
|
| 74 |
-
step.end_time = datetime.now()
|
| 75 |
-
except Exception as e:
|
| 76 |
-
step.run_error = e.__str__()
|
| 77 |
-
step.end_time = datetime.now()
|
| 78 |
-
finally:
|
| 79 |
-
db.session.commit()
|
| 80 |
-
self.lock.release()
|
| 81 |
-
return output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/utils/utils.py
DELETED
|
@@ -1,422 +0,0 @@
|
|
| 1 |
-
import ast
|
| 2 |
-
import importlib
|
| 3 |
-
import inspect
|
| 4 |
-
import logging
|
| 5 |
-
import os
|
| 6 |
-
import pickle
|
| 7 |
-
import socket
|
| 8 |
-
import subprocess
|
| 9 |
-
import sys
|
| 10 |
-
from collections import Counter
|
| 11 |
-
|
| 12 |
-
import flask
|
| 13 |
-
from flask import session
|
| 14 |
-
from flask_socketio import SocketIO
|
| 15 |
-
|
| 16 |
-
from ivoryos.utils.db_models import Script
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def get_script_file():
|
| 20 |
-
"""Get script from Flask session and returns the script"""
|
| 21 |
-
session_script = session.get("scripts")
|
| 22 |
-
if session_script:
|
| 23 |
-
s = Script()
|
| 24 |
-
s.__dict__.update(**session_script)
|
| 25 |
-
return s
|
| 26 |
-
else:
|
| 27 |
-
return Script(author=session.get('user'))
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def post_script_file(script, is_dict=False):
|
| 31 |
-
"""
|
| 32 |
-
Post script to Flask. Script will be converted to a dict if it is a Script object
|
| 33 |
-
:param script: Script to post
|
| 34 |
-
:param is_dict: if the script is a dictionary,
|
| 35 |
-
"""
|
| 36 |
-
if is_dict:
|
| 37 |
-
session['scripts'] = script
|
| 38 |
-
else:
|
| 39 |
-
session['scripts'] = script.as_dict()
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def create_gui_dir(parent_path):
|
| 43 |
-
"""
|
| 44 |
-
Creates folders for ivoryos data
|
| 45 |
-
"""
|
| 46 |
-
os.makedirs(parent_path, exist_ok=True)
|
| 47 |
-
for path in ["config_csv", "scripts", "results", "pseudo_deck"]:
|
| 48 |
-
os.makedirs(os.path.join(parent_path, path), exist_ok=True)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def save_to_history(filepath, history_path):
|
| 52 |
-
"""
|
| 53 |
-
For manual deck connection only
|
| 54 |
-
save deck file path that successfully connected to ivoryos to a history file
|
| 55 |
-
"""
|
| 56 |
-
connections = []
|
| 57 |
-
try:
|
| 58 |
-
with open(history_path, 'r') as file:
|
| 59 |
-
lines = file.read()
|
| 60 |
-
connections = lines.split('\n')
|
| 61 |
-
except FileNotFoundError:
|
| 62 |
-
pass
|
| 63 |
-
if filepath not in connections:
|
| 64 |
-
with open(history_path, 'a') as file:
|
| 65 |
-
file.writelines(f"{filepath}\n")
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def import_history(history_path):
|
| 69 |
-
"""
|
| 70 |
-
For manual deck connection only
|
| 71 |
-
load deck connection history from history file
|
| 72 |
-
"""
|
| 73 |
-
connections = []
|
| 74 |
-
try:
|
| 75 |
-
with open(history_path, 'r') as file:
|
| 76 |
-
lines = file.read()
|
| 77 |
-
connections = lines.split('\n')
|
| 78 |
-
except FileNotFoundError:
|
| 79 |
-
pass
|
| 80 |
-
connections = [i for i in connections if not i == '']
|
| 81 |
-
return connections
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def available_pseudo_deck(path):
|
| 85 |
-
"""
|
| 86 |
-
load pseudo deck (snapshot) from connection history
|
| 87 |
-
"""
|
| 88 |
-
return os.listdir(path)
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def _inspect_class(class_object=None, debug=False):
|
| 92 |
-
"""
|
| 93 |
-
inspect class object: inspect function signature if not name.startswith("_")
|
| 94 |
-
:param class_object: class object
|
| 95 |
-
:param debug: debug mode will inspect function.startswith("_")
|
| 96 |
-
:return: function: Dict[str, Dict[str, Union[Signature, str, None]]]
|
| 97 |
-
"""
|
| 98 |
-
functions = {}
|
| 99 |
-
under_score = "_"
|
| 100 |
-
if debug:
|
| 101 |
-
under_score = "__"
|
| 102 |
-
for function, method in inspect.getmembers(type(class_object), predicate=callable):
|
| 103 |
-
if not function.startswith(under_score) and not function.isupper():
|
| 104 |
-
try:
|
| 105 |
-
annotation = inspect.signature(method)
|
| 106 |
-
docstring = inspect.getdoc(method)
|
| 107 |
-
functions[function] = dict(signature=annotation, docstring=docstring)
|
| 108 |
-
|
| 109 |
-
except Exception:
|
| 110 |
-
pass
|
| 111 |
-
return functions
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def _get_type_from_parameters(arg, parameters):
|
| 115 |
-
"""get argument types from inspection"""
|
| 116 |
-
arg_type = ''
|
| 117 |
-
if type(parameters) is inspect.Signature:
|
| 118 |
-
annotation = parameters.parameters[arg].annotation
|
| 119 |
-
elif type(parameters) is dict:
|
| 120 |
-
annotation = parameters[arg]
|
| 121 |
-
if annotation is not inspect._empty:
|
| 122 |
-
# print(p[arg].annotation)
|
| 123 |
-
if annotation.__module__ == 'typing':
|
| 124 |
-
|
| 125 |
-
if hasattr(annotation, '__origin__'):
|
| 126 |
-
origin = annotation.__origin__
|
| 127 |
-
if hasattr(origin, '_name') and origin._name in ["Optional", "Union"]:
|
| 128 |
-
arg_type = [i.__name__ for i in annotation.__args__]
|
| 129 |
-
elif hasattr(origin, '__name__'):
|
| 130 |
-
arg_type = origin.__name__
|
| 131 |
-
# todo other types
|
| 132 |
-
elif annotation.__module__ == 'types':
|
| 133 |
-
arg_type = [i.__name__ for i in annotation.__args__]
|
| 134 |
-
|
| 135 |
-
else:
|
| 136 |
-
arg_type = annotation.__name__
|
| 137 |
-
return arg_type
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
def _convert_by_str(args, arg_types):
|
| 141 |
-
"""
|
| 142 |
-
Converts a value to type through eval(f'{type}("{args}")')
|
| 143 |
-
"""
|
| 144 |
-
if type(arg_types) is not list:
|
| 145 |
-
arg_types = [arg_types]
|
| 146 |
-
for arg_type in arg_types:
|
| 147 |
-
if not arg_type == "any":
|
| 148 |
-
try:
|
| 149 |
-
args = eval(f'{arg_type}("{args}")') if type(args) is str else eval(f'{arg_type}({args})')
|
| 150 |
-
return args
|
| 151 |
-
except Exception:
|
| 152 |
-
raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def _convert_by_class(args, arg_types):
|
| 156 |
-
"""
|
| 157 |
-
Converts a value to type through type(arg)
|
| 158 |
-
"""
|
| 159 |
-
if arg_types.__module__ == 'builtins':
|
| 160 |
-
args = arg_types(args)
|
| 161 |
-
return args
|
| 162 |
-
elif arg_types.__module__ == "typing":
|
| 163 |
-
for i in arg_types.__args__: # for typing.Union
|
| 164 |
-
try:
|
| 165 |
-
args = i(args)
|
| 166 |
-
return args
|
| 167 |
-
except Exception:
|
| 168 |
-
pass
|
| 169 |
-
raise TypeError("Input type error.")
|
| 170 |
-
# else:
|
| 171 |
-
# args = globals()[args]
|
| 172 |
-
return args
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def convert_config_type(args, arg_types, is_class: bool = False):
|
| 176 |
-
"""
|
| 177 |
-
Converts an argument from str to an arg type
|
| 178 |
-
"""
|
| 179 |
-
if args:
|
| 180 |
-
for arg in args:
|
| 181 |
-
if arg not in arg_types.keys():
|
| 182 |
-
raise ValueError("config file format not supported.")
|
| 183 |
-
if args[arg] == '' or args[arg] == "None":
|
| 184 |
-
args[arg] = None
|
| 185 |
-
# elif args[arg] == "True" or args[arg] == "False":
|
| 186 |
-
# args[arg] = bool_dict[args[arg]]
|
| 187 |
-
else:
|
| 188 |
-
arg_type = arg_types[arg]
|
| 189 |
-
try:
|
| 190 |
-
args[arg] = ast.literal_eval(args[arg])
|
| 191 |
-
except ValueError:
|
| 192 |
-
pass
|
| 193 |
-
if type(args[arg]) is not arg_type and not type(args[arg]).__name__ == arg_type:
|
| 194 |
-
if is_class:
|
| 195 |
-
# if arg_type.__module__ == 'builtins':
|
| 196 |
-
args[arg] = _convert_by_class(args[arg], arg_type)
|
| 197 |
-
else:
|
| 198 |
-
args[arg] = _convert_by_str(args[arg], arg_type)
|
| 199 |
-
return args
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def import_module_by_filepath(filepath: str, name: str):
|
| 203 |
-
"""
|
| 204 |
-
Import module by file path
|
| 205 |
-
:param filepath: full path of module
|
| 206 |
-
:param name: module's name
|
| 207 |
-
"""
|
| 208 |
-
spec = importlib.util.spec_from_file_location(name, filepath)
|
| 209 |
-
module = importlib.util.module_from_spec(spec)
|
| 210 |
-
spec.loader.exec_module(module)
|
| 211 |
-
return module
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
class SocketIOHandler(logging.Handler):
|
| 215 |
-
def __init__(self, socketio: SocketIO):
|
| 216 |
-
super().__init__()
|
| 217 |
-
self.formatter = logging.Formatter('%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
| 218 |
-
self.socketio = socketio
|
| 219 |
-
|
| 220 |
-
def emit(self, record):
|
| 221 |
-
message = self.format(record)
|
| 222 |
-
# session["last_log"] = message
|
| 223 |
-
self.socketio.emit('log', {'message': message})
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
def start_logger(socketio: SocketIO, logger_name: str, log_filename: str = None):
|
| 227 |
-
"""
|
| 228 |
-
stream logger to web through web socketIO
|
| 229 |
-
"""
|
| 230 |
-
# logging.basicConfig( format='%(asctime)s - %(message)s')
|
| 231 |
-
formatter = logging.Formatter(fmt='%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
| 232 |
-
logger = logging.getLogger(logger_name)
|
| 233 |
-
logger.setLevel(logging.INFO)
|
| 234 |
-
file_handler = logging.FileHandler(filename=log_filename, )
|
| 235 |
-
file_handler.setFormatter(formatter)
|
| 236 |
-
logger.addHandler(file_handler)
|
| 237 |
-
# console_logger = logging.StreamHandler() # stream to console
|
| 238 |
-
# logger.addHandler(console_logger)
|
| 239 |
-
socketio_handler = SocketIOHandler(socketio)
|
| 240 |
-
logger.addHandler(socketio_handler)
|
| 241 |
-
return logger
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
def get_arg_type(args, parameters):
|
| 245 |
-
"""get argument type from signature"""
|
| 246 |
-
arg_types = {}
|
| 247 |
-
# print(args, parameters)
|
| 248 |
-
parameters = parameters.get("signature")
|
| 249 |
-
if args:
|
| 250 |
-
for arg in args:
|
| 251 |
-
arg_types[arg] = _get_type_from_parameters(arg, parameters)
|
| 252 |
-
return arg_types
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
def install_and_import(package, package_name=None):
|
| 256 |
-
"""
|
| 257 |
-
Install the package and import it
|
| 258 |
-
:param package: package to import and install
|
| 259 |
-
:param package_name: pip install package name if different from package
|
| 260 |
-
"""
|
| 261 |
-
try:
|
| 262 |
-
# Check if the package is already installed
|
| 263 |
-
importlib.import_module(package)
|
| 264 |
-
# print(f"{package} is already installed.")
|
| 265 |
-
except ImportError:
|
| 266 |
-
# If not installed, install it
|
| 267 |
-
# print(f"{package} is not installed. Installing now...")
|
| 268 |
-
subprocess.check_call([sys.executable, "-m", "pip", "install", package_name or package])
|
| 269 |
-
# print(f"{package} has been installed successfully.")
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
def web_config_entry_wrapper(data: dict, config_type: list):
|
| 273 |
-
"""
|
| 274 |
-
Wrap the data dictionary from web config entries during execution configuration
|
| 275 |
-
:param data: data dictionary
|
| 276 |
-
:param config_type: data entry types ["str", "int", "float", "bool"]
|
| 277 |
-
"""
|
| 278 |
-
rows = {} # Dictionary to hold webui_data organized by rows
|
| 279 |
-
|
| 280 |
-
# Organize webui_data by rows
|
| 281 |
-
for key, value in data.items():
|
| 282 |
-
if value: # Only process non-empty values
|
| 283 |
-
# Extract the field name and row index
|
| 284 |
-
field_name, row_index = key.split('[')
|
| 285 |
-
row_index = int(row_index.rstrip(']'))
|
| 286 |
-
|
| 287 |
-
# If row not in rows, create a new dictionary for that row
|
| 288 |
-
if row_index not in rows:
|
| 289 |
-
rows[row_index] = {}
|
| 290 |
-
|
| 291 |
-
# Add or update the field value in the specific row's dictionary
|
| 292 |
-
rows[row_index][field_name] = value
|
| 293 |
-
|
| 294 |
-
# Filter out any empty rows and create a list of dictionaries
|
| 295 |
-
filtered_rows = [row for row in rows.values() if len(row) == len(config_type)]
|
| 296 |
-
|
| 297 |
-
return filtered_rows
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
def create_deck_snapshot(deck, save: bool = False, output_path: str = '', exclude_names: list[str] = []):
|
| 301 |
-
"""
|
| 302 |
-
Create a deck snapshot of the given script
|
| 303 |
-
:param deck: python module name to create the deck snapshot from e.g. __main__
|
| 304 |
-
:param save: save the deck snapshot into pickle file
|
| 305 |
-
:param output_path: path to save the pickle file
|
| 306 |
-
:param exclude_names: module names to exclude from deck snapshot
|
| 307 |
-
"""
|
| 308 |
-
exclude_classes = (flask.Blueprint, logging.Logger)
|
| 309 |
-
|
| 310 |
-
deck_snapshot = {}
|
| 311 |
-
included = {}
|
| 312 |
-
excluded = {}
|
| 313 |
-
failed = {}
|
| 314 |
-
|
| 315 |
-
for name, val in vars(deck).items():
|
| 316 |
-
qualified_name = f"deck.{name}"
|
| 317 |
-
|
| 318 |
-
# Exclusion checks
|
| 319 |
-
if (
|
| 320 |
-
type(val).__module__ == 'builtins'
|
| 321 |
-
or name[0].isupper()
|
| 322 |
-
or name.startswith("_")
|
| 323 |
-
or isinstance(val, exclude_classes)
|
| 324 |
-
or name in exclude_names
|
| 325 |
-
):
|
| 326 |
-
excluded[qualified_name] = type(val).__name__
|
| 327 |
-
continue
|
| 328 |
-
|
| 329 |
-
try:
|
| 330 |
-
deck_snapshot[qualified_name] = _inspect_class(val)
|
| 331 |
-
included[qualified_name] = type(val).__name__
|
| 332 |
-
except Exception as e:
|
| 333 |
-
failed[qualified_name] = str(e)
|
| 334 |
-
|
| 335 |
-
# Final result
|
| 336 |
-
deck_summary = {
|
| 337 |
-
"included": included,
|
| 338 |
-
# "excluded": excluded,
|
| 339 |
-
"failed": failed
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
def print_deck_snapshot(deck_summary):
|
| 343 |
-
def print_section(title, items):
|
| 344 |
-
print(f"\n=== {title} ({len(items)}) ===")
|
| 345 |
-
if not items:
|
| 346 |
-
return
|
| 347 |
-
for name, class_type in items.items():
|
| 348 |
-
print(f" {name}: {class_type}")
|
| 349 |
-
|
| 350 |
-
print_section("✅ INCLUDED", deck_summary["included"])
|
| 351 |
-
print_section("❌ FAILED", deck_summary["failed"])
|
| 352 |
-
print("\n")
|
| 353 |
-
|
| 354 |
-
print_deck_snapshot(deck_summary)
|
| 355 |
-
|
| 356 |
-
if deck_snapshot and save:
|
| 357 |
-
# pseudo_deck = parse_dict
|
| 358 |
-
parse_dict = deck_snapshot.copy()
|
| 359 |
-
parse_dict["deck_name"] = os.path.splitext(os.path.basename(deck.__file__))[
|
| 360 |
-
0] if deck.__name__ == "__main__" else deck.__name__
|
| 361 |
-
with open(os.path.join(output_path, f"{parse_dict['deck_name']}.pkl"), 'wb') as file:
|
| 362 |
-
pickle.dump(parse_dict, file)
|
| 363 |
-
return deck_snapshot
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
def load_deck(pkl_name: str):
|
| 367 |
-
"""
|
| 368 |
-
Loads a pickled deck snapshot from disk on offline mode
|
| 369 |
-
:param pkl_name: name of the pickle file
|
| 370 |
-
"""
|
| 371 |
-
if not pkl_name:
|
| 372 |
-
return None
|
| 373 |
-
try:
|
| 374 |
-
with open(pkl_name, 'rb') as f:
|
| 375 |
-
pseudo_deck = pickle.load(f)
|
| 376 |
-
return pseudo_deck
|
| 377 |
-
except FileNotFoundError:
|
| 378 |
-
return None
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
def check_config_duplicate(config):
|
| 382 |
-
"""
|
| 383 |
-
Checks if the config entry has any duplicate
|
| 384 |
-
:param config: [{"arg": 1}, {"arg": 1}, {"arg": 1}]
|
| 385 |
-
:return: [True, False]
|
| 386 |
-
"""
|
| 387 |
-
hashable_data = [tuple(sorted(d.items())) for d in config]
|
| 388 |
-
return any(count > 1 for count in Counter(hashable_data).values())
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
def get_method_from_workflow(function_string, func_name="workflow"):
|
| 392 |
-
"""Creates a function from a string and assigns it a new name."""
|
| 393 |
-
|
| 394 |
-
namespace = {}
|
| 395 |
-
exec(function_string, globals(), namespace) # Execute the string in a safe namespace
|
| 396 |
-
# func_name = next(iter(namespace))
|
| 397 |
-
# Get the function name dynamically
|
| 398 |
-
return namespace[func_name]
|
| 399 |
-
|
| 400 |
-
# def load_workflows(script):
|
| 401 |
-
#
|
| 402 |
-
# class RegisteredWorkflows:
|
| 403 |
-
# pass
|
| 404 |
-
# deck_name = script.deck
|
| 405 |
-
# workflows = Script.query.filter(Script.deck == deck_name, Script.name != script.name, Script.registered==True).all()
|
| 406 |
-
# for workflow in workflows:
|
| 407 |
-
# compiled_strs = workflow.compile().get('script', "")
|
| 408 |
-
# method = get_method_from_workflow(compiled_strs, func_name=workflow.name)
|
| 409 |
-
# setattr(RegisteredWorkflows, workflow.name, staticmethod(method))
|
| 410 |
-
# global_config.registered_workflows = RegisteredWorkflows()
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
def get_local_ip():
|
| 414 |
-
try:
|
| 415 |
-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
| 416 |
-
s.connect(('10.255.255.255', 1)) # Dummy address to get interface IP
|
| 417 |
-
ip = s.getsockname()[0]
|
| 418 |
-
except Exception:
|
| 419 |
-
ip = '127.0.0.1'
|
| 420 |
-
finally:
|
| 421 |
-
s.close()
|
| 422 |
-
return ip
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ivoryos/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
__version__ = "1.0.7"
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,11 +1 @@
|
|
| 1 |
-
|
| 2 |
-
bcrypt
|
| 3 |
-
Flask-Login
|
| 4 |
-
Flask-Session
|
| 5 |
-
Flask-SocketIO
|
| 6 |
-
Flask-SQLAlchemy
|
| 7 |
-
SQLAlchemy-Utils~=0.41
|
| 8 |
-
wtforms~=3.2.1
|
| 9 |
-
flask-wtf
|
| 10 |
-
eventlet
|
| 11 |
-
python-dotenv
|
|
|
|
| 1 |
+
ivoryos>=1.0.7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|