ivoryos 1.2.5__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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +16 -246
- ivoryos/app.py +154 -0
- ivoryos/optimizer/ax_optimizer.py +55 -28
- ivoryos/optimizer/base_optimizer.py +20 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +3 -1
- ivoryos/routes/auth/auth.py +35 -8
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +58 -28
- ivoryos/routes/control/control_file.py +12 -15
- ivoryos/routes/control/control_new_device.py +21 -11
- ivoryos/routes/control/templates/controllers.html +27 -0
- ivoryos/routes/control/utils.py +2 -0
- ivoryos/routes/data/data.py +110 -44
- ivoryos/routes/data/templates/components/step_card.html +78 -13
- ivoryos/routes/data/templates/workflow_view.html +343 -113
- ivoryos/routes/design/design.py +59 -10
- ivoryos/routes/design/design_file.py +3 -3
- ivoryos/routes/design/design_step.py +43 -17
- ivoryos/routes/design/templates/components/action_form.html +2 -2
- ivoryos/routes/design/templates/components/canvas_main.html +6 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +3 -0
- ivoryos/routes/execute/execute.py +82 -22
- ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
- ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
- ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
- ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
- ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
- ivoryos/routes/execute/templates/experiment_run.html +0 -264
- ivoryos/routes/library/library.py +9 -11
- ivoryos/routes/main/main.py +30 -2
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +1 -1
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +259 -88
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +29 -11
- ivoryos/templates/base.html +61 -2
- ivoryos/utils/bo_campaign.py +18 -17
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +286 -60
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +52 -19
- ivoryos/utils/global_config.py +21 -0
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +80 -10
- ivoryos/utils/script_runner.py +573 -189
- ivoryos/utils/task_runner.py +69 -22
- ivoryos/utils/utils.py +48 -5
- ivoryos/version.py +1 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- ivoryos-1.4.4.dist-info/top_level.txt +3 -0
- tests/__init__.py +0 -0
- tests/conftest.py +133 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_route_auth.py +80 -0
- tests/integration/test_route_control.py +94 -0
- tests/integration/test_route_database.py +61 -0
- tests/integration/test_route_design.py +36 -0
- tests/integration/test_route_main.py +35 -0
- tests/integration/test_sockets.py +26 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/api/api.py +0 -56
- ivoryos-1.2.5.dist-info/RECORD +0 -100
- ivoryos-1.2.5.dist-info/top_level.txt +0 -1
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
- {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
document.addEventListener("DOMContentLoaded", function() {
|
|
2
|
-
var socket = io
|
|
2
|
+
var socket = io();
|
|
3
3
|
socket.on('connect', function() {
|
|
4
4
|
console.log('Connected');
|
|
5
5
|
});
|
|
@@ -37,13 +37,43 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
37
37
|
console.error("Error received:", errorData);
|
|
38
38
|
var progressBar = document.getElementById('progress-bar-inner');
|
|
39
39
|
|
|
40
|
-
progressBar.classList.remove('bg-success');
|
|
41
|
-
progressBar.classList.add('bg-danger');
|
|
42
|
-
|
|
40
|
+
progressBar.classList.remove('bg-success', 'bg-warning');
|
|
41
|
+
progressBar.classList.add('bg-danger');
|
|
42
|
+
|
|
43
43
|
var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
|
|
44
|
-
document.getElementById('
|
|
44
|
+
document.getElementById('errorModalLabel').innerText = "Error Detected";
|
|
45
|
+
document.getElementById('error-message').innerText =
|
|
46
|
+
"An error occurred: " + errorData.message;
|
|
47
|
+
|
|
48
|
+
// Show all buttons again
|
|
49
|
+
document.getElementById('retry-btn').style.display = "inline-block";
|
|
50
|
+
document.getElementById('continue-btn').style.display = "inline-block";
|
|
51
|
+
document.getElementById('stop-btn').style.display = "inline-block";
|
|
52
|
+
|
|
45
53
|
errorModal.show();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
socket.on('human_intervention', function(data) {
|
|
58
|
+
console.warn("Human intervention required:", data);
|
|
59
|
+
var progressBar = document.getElementById('progress-bar-inner');
|
|
46
60
|
|
|
61
|
+
// Set progress bar to yellow
|
|
62
|
+
progressBar.classList.remove('bg-success', 'bg-danger');
|
|
63
|
+
progressBar.classList.add('bg-warning');
|
|
64
|
+
|
|
65
|
+
// Reuse error modal but update content
|
|
66
|
+
var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
|
|
67
|
+
document.getElementById('errorModalLabel').innerText = "Human Intervention Required";
|
|
68
|
+
document.getElementById('error-message').innerText =
|
|
69
|
+
"Workflow paused: " + (data.message || "Please check and manually resume.");
|
|
70
|
+
|
|
71
|
+
// Optionally: hide retry button, since it may not apply
|
|
72
|
+
document.getElementById('retry-btn').style.display = "none";
|
|
73
|
+
document.getElementById('continue-btn').style.display = "inline-block";
|
|
74
|
+
document.getElementById('stop-btn').style.display = "inline-block";
|
|
75
|
+
|
|
76
|
+
errorModal.show();
|
|
47
77
|
});
|
|
48
78
|
|
|
49
79
|
// Handle Pause/Resume Button
|
|
@@ -71,6 +101,11 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
71
101
|
document.getElementById('continue-btn').addEventListener('click', function() {
|
|
72
102
|
socket.emit('pause'); // Resume execution
|
|
73
103
|
console.log("Execution resumed.");
|
|
104
|
+
|
|
105
|
+
// Reset progress bar color to running (blue)
|
|
106
|
+
var progressBar = document.getElementById('progress-bar-inner');
|
|
107
|
+
progressBar.classList.remove('bg-danger', 'bg-warning');
|
|
108
|
+
progressBar.classList.add('bg-primary');
|
|
74
109
|
});
|
|
75
110
|
|
|
76
111
|
document.getElementById('retry-btn').addEventListener('click', function() {
|
|
@@ -106,6 +106,7 @@ function initializeCanvas() {
|
|
|
106
106
|
document.activeElement?.blur();
|
|
107
107
|
triggerModal(formHtml, actionName, actionId, state.dropTargetId);
|
|
108
108
|
});
|
|
109
|
+
initializeCodeOverlay();
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
function insertDropPlaceholder($target) {
|
|
@@ -115,20 +116,37 @@ function insertDropPlaceholder($target) {
|
|
|
115
116
|
|
|
116
117
|
// Add this function to sortable_design.js
|
|
117
118
|
function initializeDragHandlers() {
|
|
118
|
-
$(".accordion-item
|
|
119
|
-
let formHtml = $(this).find(".accordion-body form").prop('outerHTML');
|
|
120
|
-
|
|
121
|
-
if (!formHtml) {
|
|
122
|
-
console.error("Form not found in accordion-body");
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
119
|
+
const $cards = $(".accordion-item.design-control");
|
|
125
120
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
// Toggle draggable based on mouse/touch position
|
|
122
|
+
$cards.off("mousedown touchstart").on("mousedown touchstart", function (event) {
|
|
123
|
+
this.setAttribute("draggable", $(event.target).closest(".input-group").length ? "false" : "true");
|
|
124
|
+
});
|
|
129
125
|
|
|
130
|
-
|
|
126
|
+
// Handle the actual drag
|
|
127
|
+
$cards.off("dragstart dragend").on({
|
|
128
|
+
dragstart: function (event) {
|
|
129
|
+
if (this.getAttribute("draggable") !== "true") {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const formHtml = $(this).find(".accordion-body form").prop("outerHTML");
|
|
135
|
+
if (!formHtml) return false;
|
|
136
|
+
|
|
137
|
+
event.originalEvent.dataTransfer.setData("form", formHtml);
|
|
138
|
+
event.originalEvent.dataTransfer.setData("action", $(this).find(".draggable-action").data("action"));
|
|
139
|
+
event.originalEvent.dataTransfer.setData("id", $(this).find(".draggable-action").attr("id"));
|
|
140
|
+
|
|
141
|
+
$(this).addClass("dragging");
|
|
142
|
+
},
|
|
143
|
+
dragend: function () {
|
|
144
|
+
$(this).removeClass("dragging").attr("draggable", "false");
|
|
145
|
+
}
|
|
131
146
|
});
|
|
147
|
+
|
|
148
|
+
// Prevent form inputs from being draggable
|
|
149
|
+
$(".accordion-item input, .accordion-item select").attr("draggable", "false");
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
// Make sure it's called in the document ready function
|
ivoryos/templates/base.html
CHANGED
|
@@ -24,9 +24,25 @@
|
|
|
24
24
|
<nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
|
|
25
25
|
<div class= "container">
|
|
26
26
|
{# {{ module_config }}#}
|
|
27
|
+
|
|
27
28
|
<a class="navbar-brand" href="{{ url_for('main.index') }}">
|
|
28
|
-
|
|
29
|
+
{% if current_user.is_authenticated %}
|
|
30
|
+
{% if current_user.settings.logo_mode == 'replace' and current_user.settings.logo_filename %}
|
|
31
|
+
<img src="{{ url_for('static', filename='user_logos/' ~ current_user.settings.logo_filename) }}" alt="User Logo" height="50">
|
|
32
|
+
{% else %}
|
|
33
|
+
|
|
34
|
+
{% if current_user.settings.logo_mode == 'add' and current_user.settings.logo_filename %}
|
|
35
|
+
<img src="{{ url_for('static', filename='user_logos/' ~ current_user.settings.logo_filename) }}" alt="User Logo" height="50" class="ms-2">
|
|
36
|
+
{% endif %}
|
|
37
|
+
<img src="{{ url_for('static', filename='ivoryos_logo.png') }}" alt="IvoryOS" height="50">
|
|
38
|
+
{% endif %}
|
|
39
|
+
{% else %}
|
|
40
|
+
<img src="{{ url_for('static', filename='ivoryos_logo.png') }}" alt="IvoryOS" height="50">
|
|
41
|
+
{% endif %}
|
|
29
42
|
</a>
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
30
46
|
<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
47
|
<span class="navbar-toggler-icon"></span>
|
|
32
48
|
</button>
|
|
@@ -69,11 +85,12 @@
|
|
|
69
85
|
{% endif %}
|
|
70
86
|
</ul>
|
|
71
87
|
<ul class="navbar-nav ms-auto">
|
|
72
|
-
{# {{ current_user }}#}
|
|
73
88
|
{% if current_user.is_authenticated %}
|
|
74
89
|
<div class="dropdown">
|
|
75
90
|
<li class="nav-item " aria-expanded="false"><i class="bi bi-person-circle"></i> {{ current_user.get_id() }}</li>
|
|
76
91
|
<ul class="dropdown-menu">
|
|
92
|
+
<li><a class="dropdown-item" href="{{ url_for("auth.change_password") }}" role="button" aria-expanded="false">Change Password</a></li>
|
|
93
|
+
<li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#logoModal" role="button" aria-expanded="false">Customize Logo</a></li>
|
|
77
94
|
<li><a class="dropdown-item" href="{{ url_for("auth.logout") }}" role="button" aria-expanded="false">Logout</a></li>
|
|
78
95
|
</ul>
|
|
79
96
|
|
|
@@ -153,5 +170,47 @@
|
|
|
153
170
|
</div>
|
|
154
171
|
</div>
|
|
155
172
|
</div>
|
|
173
|
+
|
|
174
|
+
<!-- Logo Customization Modal -->
|
|
175
|
+
<div class="modal fade" id="logoModal" tabindex="-1" aria-labelledby="logoModalLabel" aria-hidden="true">
|
|
176
|
+
<div class="modal-dialog modal-dialog-centered">
|
|
177
|
+
<div class="modal-content shadow-lg">
|
|
178
|
+
<div class="modal-header">
|
|
179
|
+
<h5 class="modal-title" id="logoModalLabel">Customize Navbar Logo</h5>
|
|
180
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
181
|
+
</div>
|
|
182
|
+
<form method="POST"
|
|
183
|
+
action="{{ url_for('main.customize_logo') }}"
|
|
184
|
+
enctype="multipart/form-data">
|
|
185
|
+
<div class="modal-body">
|
|
186
|
+
<div class="mb-3">
|
|
187
|
+
<label class="form-label fw-semibold">Upload your logo</label>
|
|
188
|
+
<input type="file" name="logo" accept="image/*" class="form-control" required>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="mb-2">
|
|
191
|
+
<label class="form-label fw-semibold">Display mode</label>
|
|
192
|
+
<div class="form-check">
|
|
193
|
+
<input class="form-check-input" type="radio" name="mode" id="add" value="add" checked>
|
|
194
|
+
<label class="form-check-label" for="add">
|
|
195
|
+
Add next to IvoryOS logo
|
|
196
|
+
</label>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="form-check">
|
|
199
|
+
<input class="form-check-input" type="radio" name="mode" id="replace" value="replace">
|
|
200
|
+
<label class="form-check-label" for="replace">
|
|
201
|
+
Replace IvoryOS logo
|
|
202
|
+
</label>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
<div class="modal-footer">
|
|
207
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
208
|
+
<button type="submit" class="btn btn-primary">Apply</button>
|
|
209
|
+
</div>
|
|
210
|
+
</form>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
156
215
|
</body>
|
|
157
216
|
</html>
|
ivoryos/utils/bo_campaign.py
CHANGED
|
@@ -134,7 +134,7 @@ def parse_optimization_form(form_data: Dict[str, str]):
|
|
|
134
134
|
Parse dynamic form data into structured optimization configuration.
|
|
135
135
|
|
|
136
136
|
Expected form field patterns:
|
|
137
|
-
- Objectives: {name}
|
|
137
|
+
- Objectives: {name}_obj_min, {name}_weight
|
|
138
138
|
- Parameters: {name}_type, {name}_min, {name}_max, {name}_choices, {name}_value_type
|
|
139
139
|
- Config: step{n}_model, step{n}_num_samples
|
|
140
140
|
"""
|
|
@@ -149,25 +149,25 @@ def parse_optimization_form(form_data: Dict[str, str]):
|
|
|
149
149
|
|
|
150
150
|
# Parse objectives
|
|
151
151
|
for field_name, value in form_data.items():
|
|
152
|
-
if field_name.endswith('
|
|
152
|
+
if field_name.endswith('_obj_min') and value:
|
|
153
153
|
# Extract objective name
|
|
154
|
-
obj_name = field_name.replace('
|
|
154
|
+
obj_name = field_name.replace('_obj_min', '')
|
|
155
155
|
if obj_name in processed_objectives:
|
|
156
156
|
continue
|
|
157
157
|
|
|
158
158
|
# Check if corresponding weight exists
|
|
159
159
|
weight_field = f"{obj_name}_weight"
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
"minimize": value == "minimize",
|
|
164
|
-
"weight": float(form_data[weight_field])
|
|
165
|
-
})
|
|
166
|
-
else:
|
|
167
|
-
objectives.append({
|
|
160
|
+
early_stop_field = f"{obj_name}_obj_threshold"
|
|
161
|
+
|
|
162
|
+
config = {
|
|
168
163
|
"name": obj_name,
|
|
169
164
|
"minimize": value == "minimize",
|
|
170
|
-
}
|
|
165
|
+
}
|
|
166
|
+
if weight_field in form_data and form_data[weight_field]:
|
|
167
|
+
config["weight"] = float(form_data[weight_field])
|
|
168
|
+
if early_stop_field in form_data and form_data[early_stop_field]:
|
|
169
|
+
config["early_stop"] = float(form_data[early_stop_field])
|
|
170
|
+
objectives.append(config)
|
|
171
171
|
processed_objectives.add(obj_name)
|
|
172
172
|
|
|
173
173
|
# Parse parameters
|
|
@@ -192,11 +192,11 @@ def parse_optimization_form(form_data: Dict[str, str]):
|
|
|
192
192
|
if value == "range":
|
|
193
193
|
min_field = f"{param_name}_min"
|
|
194
194
|
max_field = f"{param_name}_max"
|
|
195
|
-
|
|
195
|
+
step_field = f"{param_name}_step"
|
|
196
196
|
if min_field in form_data and max_field in form_data:
|
|
197
197
|
min_val = form_data[min_field]
|
|
198
198
|
max_val = form_data[max_field]
|
|
199
|
-
|
|
199
|
+
step_val = form_data[step_field] if step_field in form_data else None
|
|
200
200
|
if min_val and max_val:
|
|
201
201
|
# Convert based on value_type
|
|
202
202
|
if value_type == "int":
|
|
@@ -204,12 +204,13 @@ def parse_optimization_form(form_data: Dict[str, str]):
|
|
|
204
204
|
elif value_type == "float":
|
|
205
205
|
bounds = [float(min_val), float(max_val)]
|
|
206
206
|
else: # string
|
|
207
|
-
bounds = [
|
|
208
|
-
|
|
207
|
+
bounds = [float(min_val), float(max_val)]
|
|
208
|
+
if step_val:
|
|
209
|
+
bounds.append(float(step_val))
|
|
209
210
|
parameter["bounds"] = bounds
|
|
210
211
|
|
|
211
212
|
elif value == "choice":
|
|
212
|
-
choices_field = f"{param_name}
|
|
213
|
+
choices_field = f"{param_name}_value"
|
|
213
214
|
if choices_field in form_data and form_data[choices_field]:
|
|
214
215
|
# Split choices by comma and clean whitespace
|
|
215
216
|
choices = [choice.strip() for choice in form_data[choices_field].split(',')]
|
ivoryos/utils/client_proxy.py
CHANGED
|
@@ -1,57 +1,288 @@
|
|
|
1
|
-
# import argparse
|
|
2
1
|
import os
|
|
2
|
+
import re
|
|
3
|
+
from typing import Dict, Set, Any, Optional
|
|
3
4
|
|
|
4
|
-
# import requests
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
class ProxyGenerator:
|
|
7
|
+
"""
|
|
8
|
+
A class to generate Python proxy interfaces for API clients.
|
|
7
9
|
|
|
10
|
+
This generator creates client classes that wrap API endpoints,
|
|
11
|
+
automatically handling request/response cycles and error handling.
|
|
12
|
+
"""
|
|
8
13
|
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
# Common typing symbols to scan for in function signatures
|
|
15
|
+
TYPING_SYMBOLS = {
|
|
16
|
+
"Optional", "Union", "List", "Dict", "Tuple",
|
|
17
|
+
"Any", "Callable", "Iterable", "Sequence", "Set"
|
|
18
|
+
}
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
def __init__(self, base_url: str, api_path_template: str = "ivoryos/instruments/deck.{class_name}"):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the ProxyGenerator.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
base_url: The base URL for the API
|
|
26
|
+
api_path_template: Template for API paths, with {class_name} placeholder
|
|
27
|
+
"""
|
|
28
|
+
self.base_url = base_url.rstrip('/')
|
|
29
|
+
self.api_path_template = api_path_template
|
|
30
|
+
self.used_typing_symbols: Set[str] = set()
|
|
31
|
+
|
|
32
|
+
def extract_typing_from_signatures(self, functions: Dict[str, Dict[str, Any]]) -> Set[str]:
|
|
33
|
+
"""
|
|
34
|
+
Scan function signatures for typing symbols and track usage.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
functions: Dictionary of function definitions with signatures
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Set of typing symbols found in the signatures
|
|
41
|
+
"""
|
|
42
|
+
for function_data in functions.values():
|
|
43
|
+
signature = function_data.get("signature", "")
|
|
44
|
+
for symbol in self.TYPING_SYMBOLS:
|
|
45
|
+
if re.search(rf"\b{symbol}\b", signature):
|
|
46
|
+
self.used_typing_symbols.add(symbol)
|
|
47
|
+
return self.used_typing_symbols
|
|
48
|
+
|
|
49
|
+
def create_class_definition(self, class_name: str, functions: Dict[str, Dict[str, Any]]) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Generate a class definition string for one API client class.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
class_name: Name of the class to generate
|
|
55
|
+
functions: Dictionary of function definitions
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
String containing the complete class definition
|
|
59
|
+
"""
|
|
60
|
+
capitalized_name = class_name.capitalize()
|
|
61
|
+
api_url = f"{self.base_url}/{self.api_path_template.format(class_name=class_name)}"
|
|
62
|
+
|
|
63
|
+
class_template = f"class {capitalized_name}:\n"
|
|
64
|
+
class_template += f' """Auto-generated API client for {class_name} operations."""\n'
|
|
65
|
+
class_template += f' url = "{api_url}"\n\n'
|
|
66
|
+
|
|
67
|
+
# Add the __init__ with auth
|
|
68
|
+
class_template += self._generate_init()
|
|
69
|
+
|
|
70
|
+
# Add the _auth
|
|
71
|
+
class_template += self._generate_auth()
|
|
72
|
+
|
|
73
|
+
# Add the base _call method
|
|
74
|
+
class_template += self._generate_call_method()
|
|
75
|
+
|
|
76
|
+
# Add individual methods for each function
|
|
77
|
+
for function_name, details in functions.items():
|
|
78
|
+
method_def = self._generate_method(function_name, details)
|
|
79
|
+
class_template += method_def + "\n"
|
|
80
|
+
|
|
81
|
+
return class_template
|
|
82
|
+
|
|
83
|
+
def _generate_call_method(self) -> str:
|
|
84
|
+
"""Generate the base _call method for API communication."""
|
|
85
|
+
return ''' def _call(self, payload):
|
|
86
|
+
"""Make API call with error handling."""
|
|
87
|
+
res = session.post(self.url, json=payload, allow_redirects=False)
|
|
88
|
+
# Handle 302 redirect (likely auth issue)
|
|
89
|
+
if res.status_code == 302:
|
|
90
|
+
try:
|
|
91
|
+
self._auth()
|
|
92
|
+
res = session.post(self.url, json=payload, allow_redirects=False)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise AuthenticationError(
|
|
95
|
+
"Authentication failed during re-attempt. "
|
|
96
|
+
"Please check your credentials or connection."
|
|
97
|
+
) from e
|
|
98
|
+
res.raise_for_status()
|
|
99
|
+
data = res.json()
|
|
100
|
+
if not data.get('success'):
|
|
101
|
+
raise Exception(data.get('output', "Unknown API error."))
|
|
102
|
+
return data.get('output')
|
|
103
|
+
|
|
104
|
+
'''
|
|
105
|
+
|
|
106
|
+
def _generate_method(self, function_name: str, details: Dict[str, Any]) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Generate a single method definition.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
function_name: Name of the method
|
|
112
|
+
details: Function details including signature and docstring
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
String containing the method definition
|
|
116
|
+
"""
|
|
117
|
+
signature = details.get("signature", "(self)")
|
|
118
|
+
docstring = details.get("docstring", "")
|
|
119
|
+
|
|
120
|
+
# Build method header
|
|
121
|
+
method = f" def {function_name}{signature}:\n"
|
|
16
122
|
|
|
17
|
-
# Creating the function definition
|
|
18
|
-
method = f' def {function_name}{signature}:\n'
|
|
19
123
|
if docstring:
|
|
20
124
|
method += f' """{docstring}"""\n'
|
|
21
125
|
|
|
22
|
-
#
|
|
23
|
-
method += '
|
|
24
|
-
|
|
126
|
+
# Build payload
|
|
127
|
+
method += f' payload = {{"hidden_name": "{function_name}"}}\n'
|
|
128
|
+
|
|
129
|
+
# Extract parameters from signature (excluding 'self')
|
|
130
|
+
params = self._extract_parameters(signature)
|
|
131
|
+
|
|
132
|
+
for param_name in params:
|
|
133
|
+
method += f' payload["{param_name}"] = {param_name}\n'
|
|
134
|
+
|
|
135
|
+
method += " return self._call(payload)\n"
|
|
25
136
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
137
|
+
return method
|
|
138
|
+
|
|
139
|
+
def _extract_parameters(self, signature: str) -> list:
|
|
140
|
+
"""
|
|
141
|
+
Extract parameter names from a function signature.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
signature: Function signature string like "(self, param1, param2: int = 5)"
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of parameter names (excluding 'self')
|
|
148
|
+
"""
|
|
149
|
+
# Remove parentheses and split by comma
|
|
150
|
+
param_str = signature.strip("()").strip()
|
|
151
|
+
if not param_str or param_str == "self":
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
params = [param.strip() for param in param_str.split(",")]
|
|
155
|
+
result = []
|
|
29
156
|
|
|
30
157
|
for param in params:
|
|
31
|
-
|
|
32
|
-
|
|
158
|
+
if param and param != "self":
|
|
159
|
+
# Extract parameter name (before : or = if present)
|
|
160
|
+
param_name = param.split(":")[0].split("=")[0].strip()
|
|
161
|
+
if param_name:
|
|
162
|
+
result.append(param_name)
|
|
163
|
+
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
def generate_proxy_file(self,
|
|
167
|
+
snapshot: Dict[str, Dict[str, Any]],
|
|
168
|
+
output_path: str,
|
|
169
|
+
filename: str = "generated_proxy.py") -> str:
|
|
170
|
+
"""
|
|
171
|
+
Generate the complete proxy file from a snapshot of instruments.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
snapshot: Dictionary containing instrument data with functions
|
|
175
|
+
output_path: Directory to write the output file
|
|
176
|
+
filename: Name of the output file
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Path to the generated file
|
|
180
|
+
"""
|
|
181
|
+
class_definitions = {}
|
|
182
|
+
self.used_typing_symbols.clear()
|
|
183
|
+
|
|
184
|
+
# Process each instrument in the snapshot
|
|
185
|
+
for instrument_key, instrument_data in snapshot.items():
|
|
186
|
+
# Convert function signatures to strings if needed
|
|
187
|
+
for function_key, function_data in instrument_data.items():
|
|
188
|
+
if 'signature' in function_data:
|
|
189
|
+
function_data['signature'] = str(function_data['signature'])
|
|
190
|
+
|
|
191
|
+
# Extract class name from instrument path
|
|
192
|
+
class_name = instrument_key.split('.')[-1]
|
|
193
|
+
|
|
194
|
+
# Generate class definition
|
|
195
|
+
class_definitions[class_name] = self.create_class_definition(
|
|
196
|
+
class_name, instrument_data
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Track typing symbols used
|
|
200
|
+
self.extract_typing_from_signatures(instrument_data)
|
|
201
|
+
|
|
202
|
+
# Write the complete file
|
|
203
|
+
filepath = self._write_proxy_file(class_definitions, output_path, filename)
|
|
204
|
+
return filepath
|
|
205
|
+
|
|
206
|
+
def _write_proxy_file(self,
|
|
207
|
+
class_definitions: Dict[str, str],
|
|
208
|
+
output_path: str,
|
|
209
|
+
filename: str) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Write the generated classes to a Python file.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
class_definitions: Dictionary of class names to class definition strings
|
|
215
|
+
output_path: Directory to write the file
|
|
216
|
+
filename: Name of the file
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Full path to the written file
|
|
220
|
+
"""
|
|
221
|
+
filepath = os.path.join(output_path, filename)
|
|
222
|
+
|
|
223
|
+
with open(filepath, "w") as f:
|
|
224
|
+
# Write imports
|
|
225
|
+
f.write("import requests\n")
|
|
226
|
+
if self.used_typing_symbols:
|
|
227
|
+
f.write(f"from typing import {', '.join(sorted(self.used_typing_symbols))}\n")
|
|
228
|
+
f.write("\n")
|
|
229
|
+
|
|
230
|
+
# Write session setup
|
|
231
|
+
f.write("session = requests.Session()\n\n")
|
|
232
|
+
|
|
233
|
+
# Write class definitions
|
|
234
|
+
for class_name, class_def in class_definitions.items():
|
|
235
|
+
f.write(class_def)
|
|
236
|
+
f.write("\n")
|
|
237
|
+
|
|
238
|
+
# Create default instances
|
|
239
|
+
f.write("# Default instances for convenience\n")
|
|
240
|
+
for class_name in class_definitions.keys():
|
|
241
|
+
instance_name = class_name.lower()
|
|
242
|
+
f.write(f"{instance_name} = {class_name.capitalize()}()\n")
|
|
243
|
+
|
|
244
|
+
return filepath
|
|
33
245
|
|
|
34
|
-
|
|
35
|
-
|
|
246
|
+
def generate_from_flask_route(self,
|
|
247
|
+
snapshot: Dict[str, Dict[str, Any]],
|
|
248
|
+
request_url_root: str,
|
|
249
|
+
output_folder: str) -> str:
|
|
250
|
+
"""
|
|
251
|
+
Convenience method that matches the original Flask route behavior.
|
|
36
252
|
|
|
37
|
-
|
|
253
|
+
Args:
|
|
254
|
+
snapshot: The deck snapshot from global_config
|
|
255
|
+
request_url_root: The URL root from Flask request
|
|
256
|
+
output_folder: Output folder path from app config
|
|
38
257
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
f.write('session = requests.Session()\n\n')
|
|
258
|
+
Returns:
|
|
259
|
+
Path to the generated file
|
|
260
|
+
"""
|
|
261
|
+
# Set the base URL from the request
|
|
262
|
+
self.base_url = request_url_root.rstrip('/')
|
|
45
263
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
f.write(class_def)
|
|
49
|
-
f.write('\n')
|
|
264
|
+
# Generate the proxy file
|
|
265
|
+
return self.generate_proxy_file(snapshot, output_folder)
|
|
50
266
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
267
|
+
def _generate_init(self):
|
|
268
|
+
return ''' def __init__(self, username=None, password=None):
|
|
269
|
+
"""Initialize the client with authentication."""
|
|
270
|
+
self.username = username
|
|
271
|
+
self.password = password
|
|
272
|
+
self._auth()
|
|
55
273
|
|
|
274
|
+
'''
|
|
56
275
|
|
|
57
276
|
|
|
277
|
+
def _generate_auth(self):
|
|
278
|
+
return f""" def _auth(self):
|
|
279
|
+
username = self.username or 'admin'
|
|
280
|
+
password = self.password or 'admin'
|
|
281
|
+
res = session.post(
|
|
282
|
+
'{self.base_url}/ivoryos/auth/login',
|
|
283
|
+
data={{"username": username, "password": password}}
|
|
284
|
+
)
|
|
285
|
+
if res.status_code != 200:
|
|
286
|
+
raise Exception("Authentication failed")
|
|
287
|
+
|
|
288
|
+
"""
|