ivoryos 1.1.0__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (135) hide show
  1. {ivoryos-1.1.0/ivoryos.egg-info → ivoryos-1.2.0}/PKG-INFO +1 -1
  2. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/__init__.py +12 -5
  3. ivoryos-1.2.0/ivoryos/routes/api/api.py +56 -0
  4. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/control/control.py +46 -43
  5. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/control/control_file.py +4 -4
  6. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/control/control_new_device.py +10 -10
  7. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/control/templates/controllers.html +38 -9
  8. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/control/templates/controllers_new.html +1 -1
  9. ivoryos-1.2.0/ivoryos/routes/data/data.py +129 -0
  10. ivoryos-1.2.0/ivoryos/routes/data/templates/components/step_card.html +13 -0
  11. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/data/templates/workflow_database.html +10 -4
  12. ivoryos-1.2.0/ivoryos/routes/design/design.py +428 -0
  13. ivoryos-1.2.0/ivoryos/routes/design/design_file.py +68 -0
  14. ivoryos-1.2.0/ivoryos/routes/design/design_step.py +145 -0
  15. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/action_form.html +4 -3
  16. ivoryos-1.1.0/ivoryos/routes/design/templates/components/instrument_panel.html → ivoryos-1.2.0/ivoryos/routes/design/templates/components/actions_panel.html +7 -5
  17. ivoryos-1.2.0/ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  18. ivoryos-1.2.0/ivoryos/routes/design/templates/components/canvas.html +5 -0
  19. ivoryos-1.2.0/ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  20. ivoryos-1.2.0/ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  21. ivoryos-1.2.0/ivoryos/routes/design/templates/components/canvas_main.html +34 -0
  22. ivoryos-1.2.0/ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  23. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/edit_action_form.html +16 -7
  24. ivoryos-1.2.0/ivoryos/routes/design/templates/components/instruments_panel.html +66 -0
  25. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/modals/drop_modal.html +3 -5
  26. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +1 -2
  27. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/modals/rename_modal.html +5 -5
  28. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +2 -2
  29. ivoryos-1.2.0/ivoryos/routes/design/templates/components/python_code_overlay.html +39 -0
  30. ivoryos-1.2.0/ivoryos/routes/design/templates/components/sidebar.html +15 -0
  31. ivoryos-1.2.0/ivoryos/routes/design/templates/experiment_builder.html +41 -0
  32. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/execute.py +157 -13
  33. ivoryos-1.2.0/ivoryos/routes/execute/execute_file.py +78 -0
  34. ivoryos-1.2.0/ivoryos/routes/execute/templates/components/tab_bayesian.html +398 -0
  35. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/components/tab_configuration.html +1 -1
  36. ivoryos-1.2.0/ivoryos/routes/library/library.py +159 -0
  37. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/library/templates/library.html +27 -19
  38. ivoryos-1.2.0/ivoryos/static/js/action_handlers.js +213 -0
  39. ivoryos-1.2.0/ivoryos/static/js/db_delete.js +23 -0
  40. ivoryos-1.2.0/ivoryos/static/js/script_metadata.js +39 -0
  41. ivoryos-1.2.0/ivoryos/static/js/sortable_design.js +138 -0
  42. ivoryos-1.2.0/ivoryos/static/js/ui_state.js +113 -0
  43. ivoryos-1.2.0/ivoryos/utils/bo_campaign.py +263 -0
  44. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/db_models.py +14 -5
  45. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/form.py +4 -9
  46. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/global_config.py +13 -1
  47. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/script_runner.py +24 -5
  48. ivoryos-1.2.0/ivoryos/utils/serilize.py +203 -0
  49. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/task_runner.py +4 -1
  50. ivoryos-1.2.0/ivoryos/version.py +1 -0
  51. {ivoryos-1.1.0 → ivoryos-1.2.0/ivoryos.egg-info}/PKG-INFO +1 -1
  52. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos.egg-info/SOURCES.txt +8 -5
  53. ivoryos-1.1.0/ivoryos/routes/api/api.py +0 -109
  54. ivoryos-1.1.0/ivoryos/routes/data/data.py +0 -108
  55. ivoryos-1.1.0/ivoryos/routes/data/templates/components/step_card.html +0 -7
  56. ivoryos-1.1.0/ivoryos/routes/design/design.py +0 -365
  57. ivoryos-1.1.0/ivoryos/routes/design/design_file.py +0 -57
  58. ivoryos-1.1.0/ivoryos/routes/design/design_step.py +0 -43
  59. ivoryos-1.1.0/ivoryos/routes/design/templates/components/action_list.html +0 -15
  60. ivoryos-1.1.0/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -14
  61. ivoryos-1.1.0/ivoryos/routes/design/templates/components/canvas.html +0 -14
  62. ivoryos-1.1.0/ivoryos/routes/design/templates/components/canvas_footer.html +0 -5
  63. ivoryos-1.1.0/ivoryos/routes/design/templates/components/canvas_header.html +0 -54
  64. ivoryos-1.1.0/ivoryos/routes/design/templates/components/deck_selector.html +0 -12
  65. ivoryos-1.1.0/ivoryos/routes/design/templates/components/operations_panel.html +0 -43
  66. ivoryos-1.1.0/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -17
  67. ivoryos-1.1.0/ivoryos/routes/design/templates/components/script_info.html +0 -31
  68. ivoryos-1.1.0/ivoryos/routes/design/templates/components/scripts.html +0 -50
  69. ivoryos-1.1.0/ivoryos/routes/design/templates/components/sidebar.html +0 -16
  70. ivoryos-1.1.0/ivoryos/routes/design/templates/experiment_builder.html +0 -41
  71. ivoryos-1.1.0/ivoryos/routes/execute/execute_file.py +0 -44
  72. ivoryos-1.1.0/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -147
  73. ivoryos-1.1.0/ivoryos/routes/library/library.py +0 -204
  74. ivoryos-1.1.0/ivoryos/static/js/sortable_design.js +0 -105
  75. ivoryos-1.1.0/ivoryos/utils/bo_campaign.py +0 -127
  76. ivoryos-1.1.0/ivoryos/version.py +0 -1
  77. {ivoryos-1.1.0 → ivoryos-1.2.0}/LICENSE +0 -0
  78. {ivoryos-1.1.0 → ivoryos-1.2.0}/MANIFEST.in +0 -0
  79. {ivoryos-1.1.0 → ivoryos-1.2.0}/README.md +0 -0
  80. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/config.py +0 -0
  81. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/__init__.py +0 -0
  82. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/auth/__init__.py +0 -0
  83. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/auth/auth.py +0 -0
  84. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/auth/templates/login.html +0 -0
  85. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/auth/templates/signup.html +0 -0
  86. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/control/__init__.py +0 -0
  87. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/control/utils.py +0 -0
  88. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/data/__init__.py +0 -0
  89. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/data/templates/workflow_view.html +0 -0
  90. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/__init__.py +0 -0
  91. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  92. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/modals.html +0 -0
  93. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  94. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/__init__.py +0 -0
  95. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  96. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  97. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  98. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  99. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  100. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  101. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  102. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/library/__init__.py +0 -0
  103. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/main/__init__.py +0 -0
  104. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/main/main.py +0 -0
  105. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/main/templates/help.html +0 -0
  106. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/routes/main/templates/home.html +0 -0
  107. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/socket_handlers.py +0 -0
  108. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/favicon.ico +0 -0
  109. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  110. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  111. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/js/overlay.js +0 -0
  112. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/js/socket_handler.js +0 -0
  113. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/js/sortable_card.js +0 -0
  114. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/logo.webp +0 -0
  115. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/static/style.css +0 -0
  116. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/templates/base.html +0 -0
  117. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/__init__.py +0 -0
  118. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/client_proxy.py +0 -0
  119. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/llm_agent.py +0 -0
  120. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/py_to_json.py +0 -0
  121. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos/utils/utils.py +0 -0
  122. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos.egg-info/dependency_links.txt +0 -0
  123. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos.egg-info/requires.txt +0 -0
  124. {ivoryos-1.1.0 → ivoryos-1.2.0}/ivoryos.egg-info/top_level.txt +0 -0
  125. {ivoryos-1.1.0 → ivoryos-1.2.0}/setup.cfg +0 -0
  126. {ivoryos-1.1.0 → ivoryos-1.2.0}/setup.py +0 -0
  127. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/__init__.py +0 -0
  128. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/conftest.py +0 -0
  129. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/integration/__init__.py +0 -0
  130. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/integration/test_route_auth.py +0 -0
  131. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/integration/test_route_control.py +0 -0
  132. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/integration/test_route_database.py +0 -0
  133. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/integration/test_route_design.py +0 -0
  134. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/integration/test_route_main.py +0 -0
  135. {ivoryos-1.1.0 → ivoryos-1.2.0}/tests/integration/test_sockets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivoryos
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Home-page: https://gitlab.com/heingroup/ivoryos
6
6
  Author: Ivory Zhang
@@ -19,6 +19,7 @@ from ivoryos.routes.main.main import main
19
19
  from ivoryos.utils import utils
20
20
  from ivoryos.utils.db_models import db, User
21
21
  from ivoryos.utils.global_config import GlobalConfig
22
+ from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
22
23
  from ivoryos.utils.script_runner import ScriptRunner
23
24
  from ivoryos.version import __version__ as ivoryos_version
24
25
  from importlib.metadata import entry_points
@@ -42,10 +43,10 @@ app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='sta
42
43
  app.register_blueprint(main, url_prefix=url_prefix)
43
44
  app.register_blueprint(auth, url_prefix=f'{url_prefix}/{auth.name}')
44
45
  app.register_blueprint(library, url_prefix=f'{url_prefix}/{library.name}')
45
- app.register_blueprint(control, url_prefix=f'{url_prefix}/{control.name}')
46
- app.register_blueprint(design, url_prefix=f'{url_prefix}/{design.name}')
47
- app.register_blueprint(execute, url_prefix=f'{url_prefix}/{execute.name}')
48
- app.register_blueprint(data, url_prefix=f'{url_prefix}/{data.name}')
46
+ app.register_blueprint(control, url_prefix=f'{url_prefix}/instruments')
47
+ app.register_blueprint(design, url_prefix=f'{url_prefix}')
48
+ app.register_blueprint(execute, url_prefix=f'{url_prefix}')
49
+ app.register_blueprint(data, url_prefix=f'{url_prefix}')
49
50
  app.register_blueprint(api, url_prefix=f'{url_prefix}/{api.name}')
50
51
 
51
52
  @login_manager.user_loader
@@ -94,6 +95,12 @@ def create_app(config_class=None):
94
95
  def redirect_to_prefix():
95
96
  return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
96
97
 
98
+ @app.template_filter('format_name')
99
+ def format_name(name):
100
+ name = name.split(".")[-1]
101
+ text = ' '.join(word for word in name.split('_'))
102
+ return text.capitalize()
103
+
97
104
  return app
98
105
 
99
106
 
@@ -144,7 +151,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
144
151
  app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
145
152
  logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
146
153
  dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
147
-
154
+ global_config.optimizers = OPTIMIZER_REGISTRY
148
155
  if module:
149
156
  app.config["MODULE"] = module
150
157
  app.config["OFF_LINE"] = False
@@ -0,0 +1,56 @@
1
+ import os
2
+ from flask import Blueprint, jsonify, request, current_app
3
+
4
+ from ivoryos.routes.control.control import find_instrument_by_name
5
+ from ivoryos.utils.form import create_form_from_module
6
+ from ivoryos.utils.global_config import GlobalConfig
7
+ from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
8
+
9
+ from ivoryos.socket_handlers import abort_pending, abort_current, pause, retry, runner
10
+ from ivoryos.utils.task_runner import TaskRunner
11
+
12
+ api = Blueprint('api', __name__)
13
+ global_config = GlobalConfig()
14
+ task_runner = TaskRunner()
15
+
16
+ #TODO: add authentication and authorization to the API endpoints
17
+
18
+
19
+ @api.route("/control/", strict_slashes=False, methods=['GET'])
20
+ @api.route("/control/<string:instrument>", methods=['POST'])
21
+ def backend_control(instrument: str=None):
22
+ """
23
+ .. :quickref: Backend Control; backend control
24
+
25
+ backend control through http requests
26
+
27
+ .. http:get:: /api/control/
28
+
29
+ :param instrument: instrument name
30
+ :type instrument: str
31
+
32
+ .. http:post:: /api/control/
33
+
34
+ """
35
+ if instrument:
36
+ inst_object = find_instrument_by_name(instrument)
37
+ forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
38
+
39
+ if request.method == 'POST':
40
+ method_name = request.form.get("hidden_name", None)
41
+ form = forms.get(method_name, None)
42
+ if form:
43
+ kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
44
+ wait = request.form.get("hidden_wait", "true") == "true"
45
+ output = task_runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
46
+ current_app=current_app._get_current_object())
47
+ return jsonify(output), 200
48
+
49
+ snapshot = global_config.deck_snapshot.copy()
50
+ # Iterate through each instrument in the snapshot
51
+ for instrument_key, instrument_data in snapshot.items():
52
+ # Iterate through each function associated with the current instrument
53
+ for function_key, function_data in instrument_data.items():
54
+ # Convert the function signature to a string representation
55
+ function_data['signature'] = str(function_data['signature'])
56
+ return jsonify(snapshot), 200
@@ -1,11 +1,11 @@
1
- from flask import Blueprint, redirect, flash, request, render_template, session, current_app
1
+ from flask import Blueprint, redirect, flash, request, render_template, session, current_app, jsonify
2
2
  from flask_login import login_required
3
3
 
4
4
  from ivoryos.routes.control.control_file import control_file
5
5
  from ivoryos.routes.control.control_new_device import control_temp
6
6
  from ivoryos.routes.control.utils import post_session_by_instrument, get_session_by_instrument, find_instrument_by_name
7
7
  from ivoryos.utils.global_config import GlobalConfig
8
- from ivoryos.utils.form import create_form_from_module, format_name
8
+ from ivoryos.utils.form import create_form_from_module
9
9
  from ivoryos.utils.task_runner import TaskRunner
10
10
 
11
11
  global_config = GlobalConfig()
@@ -18,17 +18,36 @@ control.register_blueprint(control_temp)
18
18
 
19
19
 
20
20
 
21
- @control.route("/home", strict_slashes=False, methods=["GET", "POST"])
21
+ @control.route("/", strict_slashes=False, methods=["GET", "POST"])
22
+ @control.route("/<string:instrument>", strict_slashes=False, methods=["GET", "POST"])
22
23
  @login_required
23
24
  def deck_controllers():
24
25
  """
25
- Combined controllers page: sidebar with all instruments, main area with method cards for selected instrument.
26
+ .. :quickref: Direct Control; device (instruments) and methods
27
+
28
+ device home interface for listing all instruments and methods, selecting an instrument to run its methods
29
+
30
+ .. http:get:: /instruments
31
+
32
+ get all instruments for home page
33
+
34
+ .. http:get:: /instruments/<string:instrument>
35
+
36
+ get all methods of the given <instrument>
37
+
38
+ .. http:post:: /instruments/<string:instrument>
39
+
40
+ send POST request to run a method of the given <instrument>
41
+
42
+ :param instrument: instrument name, if not provided, list all instruments
43
+ :type instrument: str
44
+ :status 200: render template with instruments and methods
45
+
26
46
  """
27
47
  deck_variables = global_config.deck_snapshot.keys()
28
48
  temp_variables = global_config.defined_variables.keys()
29
49
  instrument = request.args.get('instrument')
30
50
  forms = None
31
- # format_name_fn = format_name
32
51
  if instrument:
33
52
  inst_object = find_instrument_by_name(instrument)
34
53
  _forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
@@ -47,12 +66,12 @@ def deck_controllers():
47
66
  form = forms.get(method_name)
48
67
  kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'} if form else {}
49
68
  if form and form.validate_on_submit():
50
- try:
51
- kwargs.pop("hidden_name", None)
52
- output = runner.run_single_step(instrument, method_name, kwargs, wait=True, current_app=current_app._get_current_object())
53
- flash(f"\nRun Success! Output value: {output}.")
54
- except Exception as e:
55
- flash(str(e))
69
+ kwargs.pop("hidden_name", None)
70
+ output = runner.run_single_step(instrument, method_name, kwargs, wait=True, current_app=current_app._get_current_object())
71
+ if output["success"]:
72
+ flash(f"\nRun Success! Output value: {output.get('output', 'None')}.")
73
+ else:
74
+ flash(f"\nRun Error! {output.get('output', 'Unknown error occurred.')}", "error")
56
75
  else:
57
76
  if form:
58
77
  flash(form.errors)
@@ -64,16 +83,15 @@ def deck_controllers():
64
83
  temp_variables=temp_variables,
65
84
  instrument=instrument,
66
85
  forms=forms,
67
- format_name=format_name,
68
86
  session=session
69
87
  )
70
88
 
71
- @control.route('/<instrument>/save-order', methods=['POST'])
89
+ @control.route('/<string:instrument>/actions/order', methods=['POST'])
72
90
  def save_order(instrument: str):
73
91
  """
74
92
  .. :quickref: Control Customization; Save functions' order
75
93
 
76
- .. http:post:: /control/save-order
94
+ .. http:post:: instruments/<string:instrument>/actions/order
77
95
 
78
96
  save function drag and drop order for the given <instrument>
79
97
 
@@ -83,47 +101,32 @@ def save_order(instrument: str):
83
101
  post_session_by_instrument('card_order', instrument, data['order'])
84
102
  return '', 204
85
103
 
86
-
87
- @control.route('/<instrument>/<function>/hide')
88
- def hide_function(instrument, function):
104
+ @control.route('/<string:instrument>/actions/<string:function>', methods=["PATCH"])
105
+ def hide_function(instrument: str, function: str):
89
106
  """
90
- .. :quickref: Control Customization; Hide function
107
+ .. :quickref: Control Customization; Toggle function visibility
91
108
 
92
- .. http:get:: //control/<instrument>/<function>/hide
109
+ .. http:patch:: /instruments/<instrument>/actions/<function>
93
110
 
94
- Hide the given <instrument> and <function>
111
+ Toggle visibility for the given <instrument> and <function>
95
112
 
96
113
  """
97
114
  back = request.referrer
115
+ data = request.get_json()
116
+ hidden = data.get('hidden', True)
98
117
  functions = get_session_by_instrument("hidden_functions", instrument)
99
118
  order = get_session_by_instrument("card_order", instrument)
100
- if function not in functions:
119
+ if hidden and function not in functions:
101
120
  functions.append(function)
102
- order.remove(function)
103
- post_session_by_instrument('hidden_functions', instrument, functions)
104
- post_session_by_instrument('card_order', instrument, order)
105
- return redirect(back)
106
-
107
-
108
- @control.route('/<instrument>/<function>/unhide')
109
- def remove_hidden(instrument: str, function: str):
110
- """
111
- .. :quickref: Control Customization; Remove a hidden function
112
-
113
- .. http:get:: /control/<instrument>/<function>/unhide
114
-
115
- Un-hide the given <instrument> and <function>
116
-
117
- """
118
- back = request.referrer
119
- functions = get_session_by_instrument("hidden_functions", instrument)
120
- order = get_session_by_instrument("card_order", instrument)
121
- if function in functions:
121
+ if function in order:
122
+ order.remove(function)
123
+ elif not hidden and function in functions:
122
124
  functions.remove(function)
123
- order.append(function)
125
+ if function not in order:
126
+ order.append(function)
124
127
  post_session_by_instrument('hidden_functions', instrument, functions)
125
128
  post_session_by_instrument('card_order', instrument, order)
126
- return redirect(back)
129
+ return jsonify(success=True, message="Visibility updated")
127
130
 
128
131
 
129
132
 
@@ -10,15 +10,15 @@ global_config = GlobalConfig()
10
10
  control_file = Blueprint('file', __name__)
11
11
 
12
12
 
13
- @control_file.route("/download/proxy", strict_slashes=False)
13
+ @control_file.route("/files/proxy", strict_slashes=False)
14
14
  @login_required
15
15
  def download_proxy():
16
16
  """
17
- .. :quickref: Direct Control; download proxy interface
17
+ .. :quickref: Direct Control Files; download proxy interface
18
18
 
19
- download proxy interface
19
+ download proxy Python interface
20
20
 
21
- .. http:get:: /control/download
21
+ .. http:get:: /files/proxy
22
22
  """
23
23
  snapshot = global_config.deck_snapshot.copy()
24
24
  class_definitions = {}
@@ -10,18 +10,18 @@ global_config = GlobalConfig()
10
10
 
11
11
  control_temp = Blueprint('temp', __name__)
12
12
 
13
- @control_temp.route("/import/module", methods=['POST'])
13
+ @control_temp.route("/new/module", methods=['POST'])
14
14
  def import_api():
15
15
  """
16
16
  .. :quickref: Advanced Features; Manually import API module(s)
17
17
 
18
18
  importing other Python modules
19
19
 
20
- .. http:post:: /control/import/module
20
+ .. http:post:: /instruments/new/module
21
21
 
22
22
  :form filepath: API (Python class) module filepath
23
23
 
24
- import the module and redirect to :http:get:`/ivoryos/control/new/`
24
+ import the module and redirect to :http:get:`/ivoryos/instruments/new/`
25
25
 
26
26
  """
27
27
  filepath = request.form.get('filepath')
@@ -45,12 +45,12 @@ def import_api():
45
45
 
46
46
 
47
47
 
48
- @control_temp.route("/import/deck", methods=['POST'])
48
+ @control_temp.route("/new/deck-python", methods=['POST'])
49
49
  def import_deck():
50
50
  """
51
51
  .. :quickref: Advanced Features; Manually import a deck
52
52
 
53
- .. http:post:: /control/import_deck
53
+ .. http:post:: /instruments/new/deck-python
54
54
 
55
55
  :form filepath: deck module filepath
56
56
 
@@ -84,20 +84,20 @@ def import_deck():
84
84
 
85
85
 
86
86
  @control_temp.route("/new/", strict_slashes=False)
87
- @control_temp.route("/new/<instrument>", methods=['GET', 'POST'])
87
+ @control_temp.route("/new/<string:instrument>", methods=['GET', 'POST'])
88
88
  @login_required
89
- def new_controller(instrument=None):
89
+ def new_controller(instrument:str=None):
90
90
  """
91
- .. :quickref: Direct Control; connect to a new device
91
+ .. :quickref: Advanced Features; connect to a new device
92
92
 
93
93
  interface for connecting a new <instrument>
94
94
 
95
- .. http:get:: /control/new/
95
+ .. http:get:: /instruments/new/
96
96
 
97
97
  :param instrument: instrument name
98
98
  :type instrument: str
99
99
 
100
- .. http:post:: /control/new/
100
+ .. http:post:: /instruments/new/
101
101
 
102
102
  :form device_name: module instance name (e.g. my_instance = MyClass())
103
103
  :form kwargs: dynamic module initialization kwargs fields
@@ -20,7 +20,7 @@
20
20
  <a class="list-group-item list-group-item-action d-flex align-items-center {% if instrument == inst %}active bg-primary text-white border-0{% else %}bg-light{% endif %}"
21
21
  href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
22
22
  style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
23
- <span class="flex-grow-1">{{ format_name(inst) }}</span>
23
+ <span class="flex-grow-1">{{ inst | format_name }}</span>
24
24
  {% if instrument == inst %}
25
25
  <span class="ms-auto">&gt;</span>
26
26
  {% endif %}
@@ -40,7 +40,7 @@
40
40
  <a class="list-group-item list-group-item-action d-flex align-items-center {% if instrument == inst %}active bg-warning text-dark border-0{% else %}bg-light{% endif %}"
41
41
  href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
42
42
  style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
43
- <span class="flex-grow-1">{{ format_name(inst) }}</span>
43
+ <span class="flex-grow-1">{{ inst | format_name }}</span>
44
44
  {% if instrument == inst %}
45
45
  <span class="ms-auto">&gt;</span>
46
46
  {% endif %}
@@ -63,7 +63,7 @@
63
63
 
64
64
  <!-- Main: Method Cards -->
65
65
  <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" style="height: 100vh; overflow-y: auto;">
66
- {% if instrument and forms %}
66
+ {% if instrument%}
67
67
  {# <h2 class="text-secondary">{{ instrument }} controller</h2>#}
68
68
  <div class="grid-container" id="sortable-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1rem; width: 100%;">
69
69
  {% set hidden = session.get('hidden_functions', {}) %}
@@ -73,15 +73,18 @@
73
73
  <div class="card" id="{{function}}" style="margin: 0;">
74
74
  <div class="bg-white rounded shadow-sm h-100">
75
75
  <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>
76
- <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>
77
- <div class="form-control" style="border: none">
78
- <form role="form" method='POST' name="{{function}}" id="{{function}}" action="{{ url_for('control.deck_controllers') }}?instrument={{ instrument }}">
76
+ <a href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"
77
+ data-method="patch" data-payload='{"hidden":true}' class="toggle-visibility">
78
+ <i style="float: right;" class="bi bi-eye-slash-fill text-muted" title="Hide function"></i>
79
+ </a>
80
+ <div class="form-control" style="border: none">
81
+ <form role="form" method='POST' name="{{function}}" id="{{function}}" action="{{ url_for('control.deck_controllers') }}?instrument={{ instrument }}">
79
82
  <div class="form-group">
80
83
  {{ form.hidden_tag() }}
81
84
  {% for field in form %}
82
85
  {% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
83
86
  <div class="input-group mb-3">
84
- <label class="input-group-text">{{ field.label.text }}</label>
87
+ <label class="input-group-text">{{ field.label.text | format_name }}</label>
85
88
  {% if field.type == "SubmitField" %}
86
89
  {{ field(class="btn btn-dark") }}
87
90
  {% elif field.type == "BooleanField" %}
@@ -94,7 +97,9 @@
94
97
  {% endfor %}
95
98
  </div>
96
99
  <div class="input-group mb-3">
97
- <button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">{{format_name(function)}} </button>
100
+ <button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">
101
+ {{ function | format_name }}
102
+ </button>
98
103
  </div>
99
104
  </form>
100
105
  </div>
@@ -116,13 +121,37 @@
116
121
  <div class="accordion-body">
117
122
  {% for function in hidden_instrument %}
118
123
  <div>
119
- {{ function }} <a href="{{ url_for('control.remove_hidden', instrument=instrument, function=function) }}"><i class="bi bi-eye-fill"></i></a>
124
+ {{ function }}
125
+ <a href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"
126
+ data-method="patch" data-payload='{"hidden":false}' class="toggle-visibility">
127
+ <i class="bi bi-eye-fill text-success" title="Show function"></i>
128
+ </a>
120
129
  </div>
121
130
  {% endfor %}
122
131
  </div>
123
132
  </div>
124
133
  </div>
125
134
  <script>
135
+
136
+ document.querySelectorAll('.toggle-visibility').forEach(el => {
137
+ el.addEventListener('click', e => {
138
+ e.preventDefault();
139
+ const url = el.getAttribute('href');
140
+ const payload = JSON.parse(el.getAttribute('data-payload') || '{}');
141
+ fetch(url, {
142
+ method: 'PATCH',
143
+ headers: {
144
+ 'Content-Type': 'application/json'
145
+ },
146
+ body: JSON.stringify(payload)
147
+ }).then(response => {
148
+ if (response.ok) {
149
+ location.reload(); // or update the DOM directly
150
+ }
151
+ });
152
+ });
153
+ });
154
+
126
155
  const saveOrderUrl = `{{ url_for('control.save_order', instrument=instrument) }}`;
127
156
  const buttonIds = {{ session['card_order'][instrument] | tojson }};
128
157
  </script>
@@ -31,7 +31,7 @@
31
31
  <!-- Connecting Device -->
32
32
  <div class="col-xl-5 col-lg-5 col-md-6 mb-4">
33
33
  {% if device %}
34
- {{ device }}
34
+ {# {{ device }}#}
35
35
  <div class="card shadow-sm mb-4">
36
36
  <div class="card-header">
37
37
  <h5 class="mb-0">Connecting</h5>
@@ -0,0 +1,129 @@
1
+ import os
2
+
3
+ from flask import Blueprint, redirect, url_for, request, render_template, current_app, jsonify, send_file
4
+ from flask_login import login_required
5
+
6
+ from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep
7
+
8
+ data = Blueprint('data', __name__, template_folder='templates')
9
+
10
+
11
+
12
+ @data.route('/executions/records')
13
+ def list_workflows():
14
+ """
15
+ .. :quickref: Workflow Execution Database; list all workflow execution records
16
+
17
+ list all workflow execution records
18
+
19
+ .. http:get:: /executions/records
20
+
21
+ """
22
+ query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
23
+ search_term = request.args.get("keyword", None)
24
+ if search_term:
25
+ query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
26
+ page = request.args.get('page', default=1, type=int)
27
+ per_page = 10
28
+
29
+ workflows = query.paginate(page=page, per_page=per_page, error_out=False)
30
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
31
+ workflows = query.all()
32
+ workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
33
+ return jsonify({
34
+ "workflow_data": workflow_data,
35
+ })
36
+ else:
37
+ return render_template('workflow_database.html', workflows=workflows)
38
+
39
+
40
+ @data.get("/executions/records/<int:workflow_id>")
41
+ def workflow_logs(workflow_id:int):
42
+ """
43
+ .. :quickref: Workflow Data Database; get workflow data, steps, and logs
44
+
45
+ get workflow data logs by workflow id
46
+
47
+ .. http:get:: /executions/<int:workflow_id>
48
+
49
+ :param workflow_id: workflow id
50
+ :type workflow_id: int
51
+ """
52
+
53
+ if request.method == 'GET':
54
+ workflow = db.session.get(WorkflowRun, workflow_id)
55
+ steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
56
+
57
+ # Use full objects for template rendering
58
+ grouped = {
59
+ "prep": [],
60
+ "script": {},
61
+ "cleanup": [],
62
+ }
63
+
64
+ # Use dicts for JSON response
65
+ grouped_json = {
66
+ "prep": [],
67
+ "script": {},
68
+ "cleanup": [],
69
+ }
70
+
71
+ for step in steps:
72
+ step_dict = step.as_dict()
73
+
74
+ if step.phase == "prep":
75
+ grouped["prep"].append(step)
76
+ grouped_json["prep"].append(step_dict)
77
+
78
+ elif step.phase == "script":
79
+ grouped["script"].setdefault(step.repeat_index, []).append(step)
80
+ grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
81
+
82
+ elif step.phase == "cleanup" or step.method_name == "stop":
83
+ grouped["cleanup"].append(step)
84
+ grouped_json["cleanup"].append(step_dict)
85
+
86
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
87
+ return jsonify({
88
+ "workflow_info": workflow.as_dict(),
89
+ "steps": grouped_json,
90
+ })
91
+ else:
92
+ return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
93
+
94
+
95
+ @data.delete("/executions/records/<int:workflow_id>")
96
+ @login_required
97
+ def delete_workflow_record(workflow_id: int):
98
+ """
99
+ .. :quickref: Workflow Data Database; delete a workflow execution record
100
+
101
+ delete a workflow execution record by workflow id
102
+
103
+ .. http:delete:: /executions/records/<int:workflow_id>
104
+
105
+ :param workflow_id: workflow id
106
+ :type workflow_id: int
107
+ :status 200: return success message
108
+ """
109
+ run = WorkflowRun.query.get(workflow_id)
110
+ db.session.delete(run)
111
+ db.session.commit()
112
+ return jsonify(success=True)
113
+
114
+
115
+ @data.route('/files/execution-data/<string:filename>')
116
+ def download_results(filename:str):
117
+ """
118
+ .. :quickref: Workflow data; download a workflow data file (.CSV)
119
+
120
+ .. http:get:: /files/execution-data/<string:filename>
121
+
122
+ :param filename: workflow data filename
123
+ :type filename: str
124
+
125
+ # :status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions`
126
+ """
127
+
128
+ filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
129
+ return send_file(os.path.abspath(filepath), as_attachment=True)
@@ -0,0 +1,13 @@
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 | format_name }}</strong>
4
+ <small class="text-muted">
5
+ <i class="fas fa-play-circle me-1"></i> Start: {{ step.start_time.strftime('%H:%M:%S') if step.start_time else 'N/A' }}
6
+ <i class="fas fa-stop-circle ms-2 me-1"></i> End: {{ step.end_time.strftime('%H:%M:%S') if step.end_time else 'N/A' }}
7
+ <!-- {% if step.run_error %}
8
+ <i class="fas fa-stop-circle ms-2 me-1"></i> Error: {{ step.run_error if step.run_error else 'N/A' }}
9
+ {% endif %} -->
10
+ </small>
11
+ <!-- <small>Error: {{ step.run_error }}</small> -->
12
+ </div>
13
+ </div>
@@ -28,19 +28,25 @@
28
28
  <tbody>
29
29
  {% for workflow in workflows %}
30
30
  <tr>
31
- <td><a href="{{ url_for('data.get_workflow_steps', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
31
+ <td><a href="{{ url_for('data.workflow_logs', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
32
32
  <td>{{ workflow.id }}</td>
33
33
  <td>{{ workflow.start_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.start_time else '' }}</td>
34
34
  <td>{{ workflow.end_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.end_time else '' }}</td>
35
35
 
36
36
  <td>
37
37
  {% if workflow.data_path %}
38
- <a href="{{ url_for('design.design_files.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
38
+ <a href="{{ url_for('data.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
39
39
  {% endif %}
40
40
  </td>
41
41
  <td>
42
42
  {% if session['user'] == 'admin' or session['user'] == workflow.author %}
43
- <a href="{{ url_for('data.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>
43
+ {# <a href="{{ url_for('data.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>#}
44
+ <a href="#"
45
+ class="text-danger"
46
+ data-delete-url="{{ url_for('data.delete_workflow_record', workflow_id=workflow.id) }}"
47
+ onclick="deleteWorkflow(this); return false;">
48
+ Delete
49
+ </a>
44
50
  {% else %}
45
51
  <a class="disabled-link">delete</a>
46
52
  {% endif %}
@@ -99,5 +105,5 @@
99
105
  });
100
106
  }
101
107
  </script>
102
-
108
+ <script src="{{ url_for('static', filename='js/db_delete.js') }}"></script>
103
109
  {% endblock %}