ivoryos 1.0.9__py3-none-any.whl → 1.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +17 -207
  3. ivoryos/app.py +154 -0
  4. ivoryos/config.py +1 -0
  5. ivoryos/optimizer/ax_optimizer.py +191 -0
  6. ivoryos/optimizer/base_optimizer.py +84 -0
  7. ivoryos/optimizer/baybe_optimizer.py +193 -0
  8. ivoryos/optimizer/nimo_optimizer.py +173 -0
  9. ivoryos/optimizer/registry.py +11 -0
  10. ivoryos/routes/auth/auth.py +43 -14
  11. ivoryos/routes/auth/templates/change_password.html +32 -0
  12. ivoryos/routes/control/control.py +101 -366
  13. ivoryos/routes/control/control_file.py +33 -0
  14. ivoryos/routes/control/control_new_device.py +152 -0
  15. ivoryos/routes/control/templates/controllers.html +193 -0
  16. ivoryos/routes/control/templates/controllers_new.html +112 -0
  17. ivoryos/routes/control/utils.py +40 -0
  18. ivoryos/routes/data/data.py +197 -0
  19. ivoryos/routes/data/templates/components/step_card.html +78 -0
  20. ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
  21. ivoryos/routes/data/templates/workflow_view.html +360 -0
  22. ivoryos/routes/design/__init__.py +4 -0
  23. ivoryos/routes/design/design.py +348 -657
  24. ivoryos/routes/design/design_file.py +68 -0
  25. ivoryos/routes/design/design_step.py +171 -0
  26. ivoryos/routes/design/templates/components/action_form.html +53 -0
  27. ivoryos/routes/design/templates/components/actions_panel.html +25 -0
  28. ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  29. ivoryos/routes/design/templates/components/canvas.html +5 -0
  30. ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  31. ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  32. ivoryos/routes/design/templates/components/canvas_main.html +39 -0
  33. ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  34. ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
  35. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  36. ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
  37. ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
  38. ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
  39. ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
  40. ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
  41. ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
  42. ivoryos/routes/design/templates/components/modals.html +6 -0
  43. ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  44. ivoryos/routes/design/templates/components/sidebar.html +15 -0
  45. ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
  46. ivoryos/routes/design/templates/experiment_builder.html +44 -0
  47. ivoryos/routes/execute/__init__.py +0 -0
  48. ivoryos/routes/execute/execute.py +377 -0
  49. ivoryos/routes/execute/execute_file.py +78 -0
  50. ivoryos/routes/execute/templates/components/error_modal.html +20 -0
  51. ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
  52. ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
  53. ivoryos/routes/execute/templates/components/run_panel.html +9 -0
  54. ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  55. ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  56. ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  57. ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
  58. ivoryos/routes/execute/templates/experiment_run.html +30 -0
  59. ivoryos/routes/library/__init__.py +0 -0
  60. ivoryos/routes/library/library.py +157 -0
  61. ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
  62. ivoryos/routes/main/main.py +31 -3
  63. ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
  64. ivoryos/server.py +180 -0
  65. ivoryos/socket_handlers.py +52 -0
  66. ivoryos/static/ivoryos_logo.png +0 -0
  67. ivoryos/static/js/action_handlers.js +384 -0
  68. ivoryos/static/js/db_delete.js +23 -0
  69. ivoryos/static/js/script_metadata.js +39 -0
  70. ivoryos/static/js/socket_handler.js +40 -5
  71. ivoryos/static/js/sortable_design.js +107 -56
  72. ivoryos/static/js/ui_state.js +114 -0
  73. ivoryos/templates/base.html +67 -8
  74. ivoryos/utils/bo_campaign.py +180 -3
  75. ivoryos/utils/client_proxy.py +267 -36
  76. ivoryos/utils/db_models.py +300 -65
  77. ivoryos/utils/decorators.py +34 -0
  78. ivoryos/utils/form.py +63 -29
  79. ivoryos/utils/global_config.py +34 -1
  80. ivoryos/utils/nest_script.py +314 -0
  81. ivoryos/utils/py_to_json.py +295 -0
  82. ivoryos/utils/script_runner.py +599 -165
  83. ivoryos/utils/serilize.py +201 -0
  84. ivoryos/utils/task_runner.py +71 -21
  85. ivoryos/utils/utils.py +50 -6
  86. ivoryos/version.py +1 -1
  87. ivoryos-1.4.4.dist-info/METADATA +263 -0
  88. ivoryos-1.4.4.dist-info/RECORD +119 -0
  89. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
  90. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
  91. tests/unit/test_type_conversion.py +42 -0
  92. tests/unit/test_util.py +3 -0
  93. ivoryos/routes/control/templates/control/controllers.html +0 -78
  94. ivoryos/routes/control/templates/control/controllers_home.html +0 -55
  95. ivoryos/routes/control/templates/control/controllers_new.html +0 -89
  96. ivoryos/routes/database/database.py +0 -306
  97. ivoryos/routes/database/templates/database/step_card.html +0 -7
  98. ivoryos/routes/database/templates/database/workflow_view.html +0 -130
  99. ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
  100. ivoryos/routes/design/templates/design/experiment_run.html +0 -558
  101. ivoryos-1.0.9.dist-info/METADATA +0 -218
  102. ivoryos-1.0.9.dist-info/RECORD +0 -61
  103. /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
  104. /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
  105. /ivoryos/routes/{database → data}/__init__.py +0 -0
  106. /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
  107. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,197 @@
1
+ import os
2
+
3
+ from flask import Blueprint, redirect, url_for, request, render_template, current_app, jsonify, send_file
4
+ from flask_login import login_required
5
+
6
+ from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep, WorkflowPhase
7
+
8
+ data = Blueprint('data', __name__, template_folder='templates')
9
+
10
+
11
+
12
+ @data.route('/executions/records')
13
+ @login_required
14
+ def list_workflows():
15
+ """
16
+ .. :quickref: Workflow Execution Database; list all workflow execution records
17
+
18
+ list all workflow execution records
19
+
20
+ .. http:get:: /executions/records
21
+
22
+ """
23
+ query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
24
+ search_term = request.args.get("keyword", None)
25
+ if search_term:
26
+ query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
27
+ page = request.args.get('page', default=1, type=int)
28
+ per_page = 10
29
+
30
+ workflows = query.paginate(page=page, per_page=per_page, error_out=False)
31
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
32
+ workflows = query.all()
33
+ workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
34
+ return jsonify({
35
+ "workflow_data": workflow_data,
36
+ })
37
+ else:
38
+ return render_template('workflow_database.html', workflows=workflows)
39
+
40
+ @data.get("/executions/records/<int:workflow_id>")
41
+ def workflow_logs(workflow_id:int):
42
+ """
43
+ .. :quickref: Workflow Data Database; get workflow data, steps, and logs
44
+
45
+ get workflow data logs by workflow id
46
+
47
+ .. http:get:: /executions/records/<int:workflow_id>
48
+
49
+ :param workflow_id: workflow id
50
+ :type workflow_id: int
51
+ """
52
+ workflow = db.session.get(WorkflowRun, workflow_id)
53
+ if not workflow:
54
+ return jsonify({"error": "Workflow not found"}), 404
55
+
56
+ # Query all phases for this run, ordered by start_time
57
+ phases = WorkflowPhase.query.filter_by(run_id=workflow_id).order_by(WorkflowPhase.start_time).all()
58
+
59
+ # Prepare grouped data for template (full objects)
60
+ grouped = {
61
+ "prep": [],
62
+ "script": {},
63
+ "cleanup": [],
64
+ }
65
+
66
+ # Prepare grouped data for JSON (dicts)
67
+ grouped_json = {
68
+ "prep": [],
69
+ "script": {},
70
+ "cleanup": [],
71
+ }
72
+
73
+ for phase in phases:
74
+ phase_dict = phase.as_dict()
75
+
76
+ # Steps sorted by step_index
77
+ steps = sorted(phase.steps, key=lambda s: s.step_index)
78
+ phase_steps_dicts = [s.as_dict() for s in steps]
79
+
80
+ if phase.name == "prep":
81
+ grouped["prep"].append(phase)
82
+ grouped_json["prep"].append({
83
+ **phase_dict,
84
+ "steps": phase_steps_dicts
85
+ })
86
+
87
+ elif phase.name == "main":
88
+ grouped["script"].setdefault(phase.repeat_index, []).append(phase)
89
+ grouped_json["script"].setdefault(phase.repeat_index, []).append({
90
+ **phase_dict,
91
+ "steps": phase_steps_dicts
92
+ })
93
+
94
+ elif phase.name == "cleanup":
95
+ grouped["cleanup"].append(phase)
96
+ grouped_json["cleanup"].append({
97
+ **phase_dict,
98
+ "steps": phase_steps_dicts
99
+ })
100
+
101
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
102
+ return jsonify({
103
+ "workflow_info": workflow.as_dict(),
104
+ "phases": grouped_json,
105
+ })
106
+ else:
107
+ return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
108
+
109
+
110
+ @data.get("/executions/data/<int:workflow_id>")
111
+ def workflow_phase_data(workflow_id: int):
112
+ """
113
+ .. :quickref: Workflow Data Database; get workflow data for plotting
114
+
115
+ get workflow data for plotting by workflow id
116
+
117
+ .. http:get:: /executions/data/<int: workflow_id>
118
+
119
+ :param workflow_id: workflow id
120
+ """
121
+
122
+ workflow = db.session.get(WorkflowRun, workflow_id)
123
+ if not workflow:
124
+ return jsonify({})
125
+
126
+ phase_data = {}
127
+ main_phases = WorkflowPhase.query.filter_by(run_id=workflow_id, name='main') \
128
+ .order_by(WorkflowPhase.repeat_index).all()
129
+
130
+ for phase in main_phases:
131
+ outputs = phase.outputs or {}
132
+ phase_index = phase.repeat_index
133
+ phase_data[phase_index] = {}
134
+
135
+ # Normalize everything to a list of dicts
136
+ if isinstance(outputs, dict):
137
+ outputs = [outputs]
138
+ elif isinstance(outputs, list):
139
+ # flatten if it’s nested like [[{...}, {...}]]
140
+ outputs = [
141
+ item for sublist in outputs
142
+ for item in (sublist if isinstance(sublist, list) else [sublist])
143
+ ]
144
+
145
+ # convert each output entry to plotting format
146
+ for out in outputs:
147
+ if not isinstance(out, dict):
148
+ continue
149
+ for k, v in out.items():
150
+ if isinstance(v, (int, float)):
151
+ phase_data[phase_index].setdefault(k, []).append(
152
+ {"x": phase_index, "y": v}
153
+ )
154
+ elif isinstance(v, list) and all(isinstance(i, (int, float)) for i in v):
155
+ phase_data[phase_index].setdefault(k, []).extend(
156
+ {"x": phase_index, "y": val} for val in v
157
+ )
158
+
159
+ return jsonify(phase_data)
160
+
161
+
162
+ @data.delete("/executions/records/<int:workflow_id>")
163
+ @login_required
164
+ def delete_workflow_record(workflow_id: int):
165
+ """
166
+ .. :quickref: Workflow Data Database; delete a workflow execution record
167
+
168
+ delete a workflow execution record by workflow id
169
+
170
+ .. http:delete:: /executions/records/<int: workflow_id>
171
+
172
+ :param workflow_id: workflow id
173
+ :type workflow_id: int
174
+ :status 200: return success message
175
+ """
176
+ run = WorkflowRun.query.get(workflow_id)
177
+ db.session.delete(run)
178
+ db.session.commit()
179
+ return jsonify(success=True)
180
+
181
+
182
+ @data.route('/files/execution-data/<string:filename>')
183
+ @login_required
184
+ def download_results(filename:str):
185
+ """
186
+ .. :quickref: Workflow data; download a workflow data file (.CSV)
187
+
188
+ .. http:get:: /files/execution-data/<string:filename>
189
+
190
+ :param filename: workflow data filename
191
+ :type filename: str
192
+
193
+ # :status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions/records`
194
+ """
195
+
196
+ filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
197
+ return send_file(os.path.abspath(filepath), as_attachment=True)
@@ -0,0 +1,78 @@
1
+ <div class="card mb-2 {{ 'border-danger text-danger bg-light' if phase.run_error else 'border-secondary' }}">
2
+ <div class="card-body p-2">
3
+ <small class="text-muted">
4
+ <i class="fas fa-play-circle me-1"></i> Start: {{ phase.start_time.strftime('%H:%M:%S') if phase.start_time else 'N/A' }}
5
+ <i class="fas fa-stop-circle ms-2 me-1"></i> End: {{ phase.end_time.strftime('%H:%M:%S') if phase.end_time else 'N/A' }}
6
+ </small>
7
+ {% if phase.parameters %}
8
+ <div class="mt-2">
9
+ <strong>Parameters: </strong>
10
+ {% if phase.parameters is mapping %}
11
+ {% if phase.parameters %}
12
+ <div class="mt-2">
13
+ <strong>Parameters: </strong>
14
+ {% for key, value in phase.parameters.items() %}
15
+ <span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
16
+ {% endfor %}
17
+ </div>
18
+ {% endif %}
19
+ {% else %}
20
+ {% for batch in phase.parameters %}
21
+ <div class="mt-1">
22
+ <span class="badge bg-info text-dark me-1">Batch {{ loop.index }}</span>
23
+ {% for key, value in batch.items() %}
24
+ <span class="badge bg-secondary me-1">{{ key }}: {{ value }}</span>
25
+ {% endfor %}
26
+ </div>
27
+ {% endfor %}
28
+ {% endif %}
29
+ </div>
30
+ {% endif %}
31
+
32
+ {% if phase.steps %}
33
+ <div class="mt-2">
34
+ <strong>Steps:</strong>
35
+ <ul class="mb-0">
36
+ {% for step in phase.steps %}
37
+ <li class="{{ 'text-danger' if step.run_error else '' }}">
38
+ {{ step.method_name }}
39
+ <small class="text-muted">
40
+ ({{ step.start_time.strftime('%H:%M:%S') if step.start_time else 'N/A' }} –
41
+ {{ step.end_time.strftime('%H:%M:%S') if step.end_time else 'N/A' }})
42
+ </small>
43
+ </li>
44
+ {% endfor %}
45
+ </ul>
46
+ </div>
47
+ {% endif %}
48
+ {% if phase.outputs %}
49
+ <div class="mt-2">
50
+ <strong>Outputs:</strong>
51
+
52
+ {% if phase.outputs is mapping %}
53
+ {% for key, value in phase.outputs.items() %}
54
+ <span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
55
+ {% endfor %}
56
+
57
+ {% elif phase.outputs is sequence %}
58
+ {% for batch in phase.outputs %}
59
+ <div class="mt-1">
60
+ <span class="badge bg-info text-dark me-1">Batch {{ loop.index }}</span>
61
+ {% if batch is mapping %}
62
+ {% for key, value in batch.items() %}
63
+ <span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
64
+ {% endfor %}
65
+ {% elif batch is sequence %}
66
+ {% for kwargs in batch %}
67
+ {% for key, value in kwargs.items() %}
68
+ <span class="badge bg-success me-1">{{ key }}: {{ value }}</span>
69
+ {% endfor %}
70
+ {% endfor %}
71
+ {% endif %}
72
+ </div>
73
+ {% endfor %}
74
+ {% endif %}
75
+ </div>
76
+ {% endif %}
77
+ </div>
78
+ </div>
@@ -3,7 +3,7 @@
3
3
  {% block title %}IvoryOS | Design Database{% endblock %}
4
4
  {% block body %}
5
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">
6
+ <form id="search" style="display: inline-block;float: right;" action="{{url_for('data.list_workflows',deck_name=deck_name)}}" method="GET">
7
7
  <div class="input-group">
8
8
  <div class="form-outline">
9
9
  <input type="search" name="keyword" id="keyword" class="form-control" placeholder="Search workflows...">
@@ -28,19 +28,25 @@
28
28
  <tbody>
29
29
  {% for workflow in workflows %}
30
30
  <tr>
31
- <td><a href="{{ url_for('database.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.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('database.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 %}
@@ -53,13 +59,13 @@
53
59
  {# paging#}
54
60
  <div class="pagination justify-content-center">
55
61
  <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>
62
+ <a class="page-link" href="{{ url_for('data.list_workflows', page=workflows.prev_num) }}">Previous</a>
57
63
  </div>
58
64
 
59
65
  {% for num in workflows.iter_pages() %}
60
66
  {% if num %}
61
67
  <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>
68
+ <a class="page-link" href="{{ url_for('data.list_workflows', page=num) }}">{{ num }}</a>
63
69
  </div>
64
70
  {% else %}
65
71
  <div class="page-item disabled">
@@ -69,7 +75,7 @@
69
75
  {% endfor %}
70
76
 
71
77
  <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>
78
+ <a class="page-link" href="{{ url_for('data.list_workflows', page=workflows.next_num) }}">Next</a>
73
79
  </div>
74
80
  </div>
75
81
 
@@ -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 %}