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,383 @@
1
+ {# Configuration tab component #}
2
+ <div class="tab-pane fade {{ 'show active' if config_list else '' }}" id="tab2" role="tabpanel" aria-labelledby="tab2-tab">
3
+ <!-- File Management Section -->
4
+ <div class="card mb-4">
5
+ <div class="card-header d-flex justify-content-between align-items-center">
6
+ <h6 class="mb-0"><i class="bi bi-file-earmark-text"></i> Configuration File</h6>
7
+ <small class="text-muted">
8
+ <a href="{{ url_for('execute.execute_files.download_empty_config', filetype='configure') }}">
9
+ <i class="bi bi-download"></i> Download Empty Template
10
+ </a>
11
+ </small>
12
+ </div>
13
+ <div class="card-body">
14
+ <div class="row g-3">
15
+ <!-- File Selection -->
16
+ <div class="col-md-6">
17
+ <form name="filenameForm" id="filenameForm" method="GET" action="{{ url_for('execute.experiment_run') }}" enctype="multipart/form-data">
18
+ <div class="input-group">
19
+ <label class="input-group-text"><i class="bi bi-folder2-open"></i></label>
20
+ <select class="form-select" name="filename" id="filenameSelect" onchange="document.getElementById('filenameForm').submit();">
21
+ <option {{ 'selected' if not filename else '' }} value="">-- Select existing file --</option>
22
+ {% for config_file in config_file_list %}
23
+ <option {{ 'selected' if filename == config_file else '' }} value="{{ config_file }}">{{ config_file }}</option>
24
+ {% endfor %}
25
+ </select>
26
+ </div>
27
+ </form>
28
+ </div>
29
+
30
+ <!-- File Upload -->
31
+ <div class="col-md-6">
32
+ <form method="POST" id="loadFile" name="loadFile" action="{{ url_for('execute.execute_files.upload') }}" enctype="multipart/form-data">
33
+ <div class="input-group">
34
+ <input class="form-control" name="file" type="file" accept=".csv" required="required" onchange="document.getElementById('loadFile').submit();">
35
+ </div>
36
+ </form>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </div>
41
+
42
+ <!-- Configuration Table -->
43
+ <div class="card mb-4">
44
+ <div class="card-header position-relative">
45
+ <div class="position-absolute top-50 end-0 translate-middle-y me-3">
46
+ <span id="saveStatus" class="badge bg-success" style="display: none;">
47
+ <i class="bi bi-check-circle"></i> Auto-saved
48
+ </span>
49
+ <span id="modifiedStatus" class="badge bg-warning" style="display: none;">
50
+ <i class="bi bi-pencil"></i> Modified
51
+ </span>
52
+ </div>
53
+ </div>
54
+ <div class="card-body p-0">
55
+ <form method="POST" name="online-config" id="online-config" action="{{url_for('execute.experiment_run')}}">
56
+ <div class="table-responsive">
57
+ <table id="dataInputTable" class="table table-striped table-hover mb-0">
58
+ <thead class="table-dark">
59
+ <tr>
60
+ <th style="width: 40px;">#</th>
61
+ {% for column in config_list %}
62
+ <th>{{ column }}</th>
63
+ {% endfor %}
64
+ <th></th>
65
+ </tr>
66
+ </thead>
67
+ <tbody id="tableBody">
68
+ </tbody>
69
+ </table>
70
+ </div>
71
+
72
+ <div class="card-footer">
73
+ <div class="d-flex justify-content-between align-items-center flex-wrap gap-3">
74
+ <!-- Left side: Action buttons -->
75
+ <div class="d-flex gap-2 flex-wrap">
76
+ <button type="button" class="btn btn-success" onclick="addRow()">
77
+ <i class="bi bi-plus-circle"></i> Add Row
78
+ </button>
79
+ <button type="button" class="btn btn-warning" onclick="clearAllRows()">
80
+ <i class="bi bi-trash"></i> Clear All
81
+ </button>
82
+ <button type="button" class="btn btn-info" onclick="resetToFile()" id="resetToFileBtn" style="display: none;">
83
+ <i class="bi bi-arrow-clockwise"></i> Reset to File
84
+ </button>
85
+ </div>
86
+
87
+ <!-- Right side: Batch size and Run button -->
88
+ <div class="d-flex gap-2 align-items-center flex-wrap">
89
+ <div class="input-group" style="width: auto;">
90
+ <label class="input-group-text" for="batch_size">Batch size</label>
91
+ <input class="form-control" type="number" id="batch_size" name="batch_size" min="1" max="1000" value="1" style="width: 80px;">
92
+ </div>
93
+ <button type="submit" name="online-config" class="btn btn-primary btn-lg">
94
+ <i class="bi bi-play-circle"></i> Run
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ </form>
101
+ </div>
102
+ <!-- Config Preview (if loaded from file) -->
103
+ {% if config_preview %}
104
+ <div class="alert alert-info">
105
+ <small><i class="bi bi-info-circle"></i> {{ config_preview|length }} rows loaded from {{ filename }}</small>
106
+ </div>
107
+ {% endif %}
108
+ </div>
109
+ </div>
110
+ <script>
111
+ var rowCount = 0;
112
+ var configColumns = [
113
+ {% for column in config_list %}
114
+ '{{ column }}'{{ ',' if not loop.last else '' }}
115
+ {% endfor %}
116
+ ];
117
+ var configTypes = {
118
+ {% for column, type in config_type_list.items() %}
119
+ '{{ column }}': '{{ type }}'{{ ',' if not loop.last else '' }}
120
+ {% endfor %}
121
+ };
122
+
123
+ // State management
124
+ var originalFileData = null;
125
+ var isModifiedFromFile = false;
126
+ var saveTimeout = null;
127
+ var lastSavedData = null;
128
+
129
+ function addRow(data = null, skipSave = false) {
130
+ rowCount++;
131
+ var tableBody = document.getElementById("tableBody");
132
+ var newRow = tableBody.insertRow(-1);
133
+
134
+ // Row number cell
135
+ var rowNumCell = newRow.insertCell(-1);
136
+ rowNumCell.innerHTML = '<span class="badge bg-secondary">' + rowCount + '</span>';
137
+
138
+ // Data cells
139
+ configColumns.forEach(function(column, index) {
140
+ var cell = newRow.insertCell(-1);
141
+ var value = data && data[column] ? data[column] : '';
142
+ var placeholder = configTypes[column] || 'value';
143
+ cell.innerHTML = '<input type="text" class="form-control form-control-sm" name="' +
144
+ column + '[' + rowCount + ']" value="' + value + '" placeholder="' + placeholder +
145
+ '" oninput="onInputChange()" onchange="onInputChange()">';
146
+ });
147
+
148
+ // Action cell
149
+ var actionCell = newRow.insertCell(-1);
150
+ actionCell.innerHTML = '<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRow(this)" title="Remove row">' +
151
+ '<i class="bi bi-trash"></i></button>';
152
+
153
+ if (!skipSave) {
154
+ markAsModified();
155
+ debouncedSave();
156
+ }
157
+ }
158
+
159
+ function removeRow(button) {
160
+ var row = button.closest('tr');
161
+ row.remove();
162
+ updateRowNumbers();
163
+ markAsModified();
164
+ debouncedSave();
165
+ }
166
+
167
+ function updateRowNumbers() {
168
+ var tableBody = document.getElementById("tableBody");
169
+ var rows = tableBody.getElementsByTagName('tr');
170
+ for (var i = 0; i < rows.length; i++) {
171
+ var badge = rows[i].querySelector('.badge');
172
+ if (badge) {
173
+ badge.textContent = i + 1;
174
+ }
175
+ }
176
+ }
177
+
178
+ function clearAllRows() {
179
+ if (confirm('Are you sure you want to clear all rows?')) {
180
+ var tableBody = document.getElementById("tableBody");
181
+ tableBody.innerHTML = '';
182
+ rowCount = 0;
183
+ markAsModified();
184
+ clearSavedData();
185
+ // Add 5 empty rows by default
186
+ for (let i = 0; i < 5; i++) {
187
+ addRow(null, true);
188
+ }
189
+ debouncedSave();
190
+ }
191
+ }
192
+
193
+ function resetToFile() {
194
+ if (originalFileData && confirm('Reset to original file data? This will lose all manual changes.')) {
195
+ loadDataFromSource(originalFileData, false);
196
+ isModifiedFromFile = false;
197
+ updateStatusIndicators();
198
+ debouncedSave();
199
+ }
200
+ }
201
+
202
+ function onInputChange() {
203
+ markAsModified();
204
+ debouncedSave();
205
+ }
206
+
207
+ function markAsModified() {
208
+ if (originalFileData) {
209
+ isModifiedFromFile = true;
210
+ updateStatusIndicators();
211
+ }
212
+ }
213
+
214
+ function updateStatusIndicators() {
215
+ var modifiedStatus = document.getElementById('modifiedStatus');
216
+ var resetBtn = document.getElementById('resetToFileBtn');
217
+
218
+ if (isModifiedFromFile && originalFileData) {
219
+ modifiedStatus.style.display = 'inline-block';
220
+ resetBtn.style.display = 'inline-block';
221
+ } else {
222
+ modifiedStatus.style.display = 'none';
223
+ resetBtn.style.display = 'none';
224
+ }
225
+ }
226
+
227
+ function showSaveStatus() {
228
+ var saveStatus = document.getElementById('saveStatus');
229
+ saveStatus.style.display = 'inline-block';
230
+ setTimeout(function() {
231
+ saveStatus.style.display = 'none';
232
+ }, 2000);
233
+ }
234
+
235
+ function debouncedSave() {
236
+ clearTimeout(saveTimeout);
237
+ saveTimeout = setTimeout(function() {
238
+ saveFormData();
239
+ showSaveStatus();
240
+ }, 1000); // Save 1 second after user stops typing
241
+ }
242
+
243
+ function saveFormData() {
244
+ var formData = getCurrentFormData();
245
+ try {
246
+ sessionStorage.setItem('configFormData', JSON.stringify(formData));
247
+ sessionStorage.setItem('configModified', isModifiedFromFile.toString());
248
+ lastSavedData = formData;
249
+ console.log('Tab2: Saved form data', formData.length, 'rows');
250
+ } catch (e) {
251
+ console.warn('Could not save form data to sessionStorage:', e);
252
+ }
253
+ }
254
+
255
+ function getCurrentFormData() {
256
+ var tableBody = document.getElementById("tableBody");
257
+ var rows = tableBody.getElementsByTagName('tr');
258
+ var data = [];
259
+
260
+ for (var i = 0; i < rows.length; i++) {
261
+ var inputs = rows[i].getElementsByTagName('input');
262
+ var rowData = {};
263
+ var hasData = false;
264
+
265
+ for (var j = 0; j < inputs.length; j++) {
266
+ var input = inputs[j];
267
+ var name = input.name;
268
+ if (name) {
269
+ var columnName = name.substring(0, name.indexOf('['));
270
+ rowData[columnName] = input.value;
271
+ if (input.value.trim() !== '') {
272
+ hasData = true;
273
+ }
274
+ }
275
+ }
276
+
277
+ if (hasData) {
278
+ data.push(rowData);
279
+ }
280
+ }
281
+
282
+ return data;
283
+ }
284
+
285
+ function loadSavedData() {
286
+ try {
287
+ var savedData = sessionStorage.getItem('configFormData');
288
+ var savedModified = sessionStorage.getItem('configModified');
289
+
290
+ if (savedData) {
291
+ var parsedData = JSON.parse(savedData);
292
+ isModifiedFromFile = savedModified === 'true';
293
+ console.log('Tab2: Loaded saved data', parsedData.length, 'rows');
294
+ return parsedData;
295
+ }
296
+ } catch (e) {
297
+ console.warn('Could not load saved form data:', e);
298
+ }
299
+ return null;
300
+ }
301
+
302
+ function clearSavedData() {
303
+ try {
304
+ sessionStorage.removeItem('configFormData');
305
+ sessionStorage.removeItem('configModified');
306
+ console.log('Tab2: Cleared saved data');
307
+ } catch (e) {
308
+ console.warn('Could not clear saved data:', e);
309
+ }
310
+ }
311
+
312
+ function loadDataFromSource(data, isFromFile = false) {
313
+ // Clear existing rows
314
+ var tableBody = document.getElementById("tableBody");
315
+ tableBody.innerHTML = '';
316
+ rowCount = 0;
317
+
318
+ // Add rows with data
319
+ data.forEach(function(rowData) {
320
+ addRow(rowData, true);
321
+ });
322
+
323
+ // Add a few empty rows for additional input
324
+ for (let i = 0; i < 3; i++) {
325
+ addRow(null, true);
326
+ }
327
+
328
+ if (isFromFile) {
329
+ originalFileData = JSON.parse(JSON.stringify(data)); // Deep copy
330
+ isModifiedFromFile = false;
331
+ clearSavedData(); // Clear saved data when loading from file
332
+ }
333
+
334
+ updateStatusIndicators();
335
+ }
336
+
337
+ function loadConfigData() {
338
+ // Check for saved form data first
339
+ var savedData = loadSavedData();
340
+
341
+ {% if config_preview %}
342
+ var fileData = {{ config_preview | tojson | safe }};
343
+ originalFileData = JSON.parse(JSON.stringify(fileData)); // Deep copy
344
+
345
+ if (savedData && savedData.length > 0) {
346
+ // Load saved data if available
347
+ loadDataFromSource(savedData, false);
348
+ console.log('Tab2: Loaded saved form data');
349
+ } else {
350
+ // Load from file
351
+ loadDataFromSource(fileData, true);
352
+ console.log('Tab2: Loaded file data');
353
+ }
354
+ {% else %}
355
+ if (savedData && savedData.length > 0) {
356
+ // Load saved data
357
+ loadDataFromSource(savedData, false);
358
+ console.log('Tab2: Loaded saved form data');
359
+ } else {
360
+ // Add default empty rows
361
+ for (let i = 0; i < 5; i++) {
362
+ addRow(null, true);
363
+ }
364
+ }
365
+ {% endif %}
366
+ }
367
+
368
+ // Handle page unload
369
+ window.addEventListener('beforeunload', function() {
370
+ saveFormData();
371
+ });
372
+
373
+ // Initialize table when page loads
374
+ document.addEventListener("DOMContentLoaded", function() {
375
+ loadConfigData();
376
+ });
377
+
378
+ // ============================================
379
+ // EXPOSE FUNCTIONS GLOBALLY FOR COORDINATION
380
+ // ============================================
381
+ window.saveFormData = saveFormData;
382
+ window.loadConfigData = loadConfigData;
383
+ </script>
@@ -0,0 +1,18 @@
1
+ {# Repeat tab component #}
2
+ <div class="tab-pane fade {{ 'show active' if not config_list else '' }}" id="tab1" role="tabpanel" aria-labelledby="tab1-tab">
3
+ <p></p>
4
+ <form role="form" method='POST' name="run" action="{{url_for('execute.experiment_run')}}">
5
+ <div class="input-group mb-3">
6
+ <label class="input-group-text" for="batch_size">Batch size </label>
7
+ <input class="form-control" type="number" id="batch_size" name="batch_size" min="1" max="1000" value="1">
8
+ </div>
9
+ <div class="input-group mb-3">
10
+ <label class="input-group-text" for="repeat">Repeat for </label>
11
+ <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="1">
12
+ <label class="input-group-text" for="repeat"> times</label>
13
+ </div>
14
+ <div class="input-group mb-3">
15
+ <button class="form-control" type="submit" class="btn btn-dark">Run</button>
16
+ </div>
17
+ </form>
18
+ </div>
@@ -0,0 +1,30 @@
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
+ {% if no_deck_warning and not dismiss %}
8
+ {# auto pop import when there is no deck#}
9
+ <script type="text/javascript">
10
+ function OpenBootstrapPopup() {
11
+ $("#importModal").modal('show');
12
+ }
13
+ window.onload = function () {
14
+ OpenBootstrapPopup();
15
+ };
16
+ </script>
17
+ {% endif %}
18
+
19
+ <div class="row">
20
+ {% include 'components/run_panel.html' %}
21
+ {% include 'components/progress_panel.html' %}
22
+ {% include 'components/logging_panel.html' %}
23
+ </div>
24
+
25
+ {# Include error modal #}
26
+ {% include 'components/error_modal.html' %}
27
+
28
+ <script src="{{ url_for('static', filename='js/socket_handler.js') }}"></script>
29
+
30
+ {% endblock %}
File without changes
@@ -0,0 +1,157 @@
1
+ from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
2
+ from flask_login import login_required, current_user
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
+ library = Blueprint('library', __name__, template_folder='templates')
8
+
9
+
10
+
11
+ @library.route("/<string:script_name>", methods=["GET", "POST", "DELETE"])
12
+ @login_required
13
+ def workflow_script(script_name:str):
14
+ # todo: split this into two routes, one for GET and POST, another for DELETE
15
+ """
16
+ .. :quickref: Workflow Script Database; get, post, delete a workflow script
17
+
18
+ .. http:get:: /library/<string: script_name>
19
+
20
+ :param script_name: script name
21
+ :type script_name: str
22
+ :status 302: redirect to :http:get:`/ivoryos/draft`
23
+
24
+ .. http:post:: /library/<string: script_name>
25
+
26
+ :param script_name: script name
27
+ :type script_name: str
28
+ :status 200: json response with success status
29
+
30
+ .. http:delete:: /library/<string: script_name>
31
+
32
+ :param script_name: script name
33
+ :type script_name: str
34
+ :status 302: redirect to :http:get:`/ivoryos/draft`
35
+
36
+ """
37
+ row = Script.query.get(script_name)
38
+ if request.method == "DELETE":
39
+ if not row:
40
+ return jsonify(success=False)
41
+ db.session.delete(row)
42
+ db.session.commit()
43
+ return jsonify(success=True)
44
+ if request.method == "GET":
45
+ if not row:
46
+ return jsonify(success=False)
47
+ script = Script(**row.as_dict())
48
+ post_script_file(script)
49
+ pseudo_name = session.get("pseudo_deck", "")
50
+ off_line = current_app.config["OFF_LINE"]
51
+ if off_line and pseudo_name and not script.deck == pseudo_name:
52
+ flash(f"Choose the deck with name {script.deck}")
53
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
54
+ return jsonify({
55
+ "script": script.as_dict(),
56
+ "python_script": script.compile(),
57
+ })
58
+ return redirect(url_for('design.experiment_builder'))
59
+ if request.method == "POST":
60
+ status = publish()
61
+ return jsonify(status)
62
+ return None
63
+
64
+
65
+ def publish():
66
+ script = get_script_file()
67
+
68
+ if script.author is None:
69
+ script.author = current_user.get_id()
70
+ if not script.name or not script.deck:
71
+ return {"success": False, "error": "Deck cannot be empty, try to re-submit deck configuration on the left panel"}
72
+ row = Script.query.get(script.name)
73
+ if row and row.status == "finalized":
74
+ return {"success": False, "error": "This is a protected script, use save as to rename."}
75
+
76
+ elif row and current_user.get_id() != row.author:
77
+ return {"success": False, "error": "You are not the author, use save as to rename."}
78
+ else:
79
+ db.session.merge(script)
80
+ db.session.commit()
81
+ return {"success": True, "message": "Script published successfully"}
82
+
83
+
84
+ @library.get("/", strict_slashes=False)
85
+ @login_required
86
+ def load_from_database():
87
+ """
88
+ .. :quickref: Script Database; database page
89
+
90
+ backend control through http requests
91
+
92
+ .. http:get:: /library
93
+
94
+
95
+ """
96
+ session.pop('edit_action', None) # reset cache
97
+ query = Script.query
98
+ search_term = request.args.get("keyword", None)
99
+ deck_name = request.args.get("deck", None)
100
+ if search_term:
101
+ query = query.filter(Script.name.like(f'%{search_term}%'))
102
+ if deck_name is None:
103
+ temp = Script.query.with_entities(Script.deck).distinct().all()
104
+ deck_list = [i[0] for i in temp]
105
+ else:
106
+ query = query.filter(Script.deck == deck_name)
107
+ deck_list = ["ALL"]
108
+ page = request.args.get('page', default=1, type=int)
109
+ per_page = 10
110
+
111
+ scripts = query.paginate(page=page, per_page=per_page, error_out=False)
112
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
113
+ scripts = query.all()
114
+ script_names = [script.name for script in scripts]
115
+ return jsonify({
116
+ "workflows": script_names,
117
+ })
118
+ else:
119
+ # return HTML
120
+ return render_template("library.html", scripts=scripts, deck_list=deck_list, deck_name=deck_name)
121
+
122
+
123
+
124
+
125
+ @library.post("/", strict_slashes=False)
126
+ @login_required
127
+ def save_as():
128
+ """
129
+ .. :quickref: Script Database; save the script as
130
+
131
+ save the current workflow script as
132
+
133
+ .. http:post:: /library
134
+
135
+ : form run_name: new workflow name
136
+ :status 302: redirect to :http:get:`/ivoryos/draft`
137
+
138
+ """
139
+ if request.method == "POST":
140
+ run_name = request.form.get("run_name")
141
+ ## TODO: check if run_name is valid
142
+ register_workflow = request.form.get("register_workflow")
143
+ script = get_script_file()
144
+ script.save_as(run_name)
145
+ script.registered = register_workflow == "on"
146
+ script.author = current_user.get_id()
147
+ post_script_file(script)
148
+ status = publish()
149
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
150
+ return jsonify(status)
151
+ else:
152
+ if status["success"]:
153
+ flash("Script saved successfully")
154
+ else:
155
+ flash(status["error"], "error")
156
+ return redirect(url_for('design.experiment_builder'))
157
+