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.
- docs/source/conf.py +84 -0
- ivoryos/__init__.py +17 -207
- ivoryos/app.py +154 -0
- ivoryos/config.py +1 -0
- ivoryos/optimizer/ax_optimizer.py +191 -0
- ivoryos/optimizer/base_optimizer.py +84 -0
- ivoryos/optimizer/baybe_optimizer.py +193 -0
- ivoryos/optimizer/nimo_optimizer.py +173 -0
- ivoryos/optimizer/registry.py +11 -0
- ivoryos/routes/auth/auth.py +43 -14
- ivoryos/routes/auth/templates/change_password.html +32 -0
- ivoryos/routes/control/control.py +101 -366
- ivoryos/routes/control/control_file.py +33 -0
- ivoryos/routes/control/control_new_device.py +152 -0
- ivoryos/routes/control/templates/controllers.html +193 -0
- ivoryos/routes/control/templates/controllers_new.html +112 -0
- ivoryos/routes/control/utils.py +40 -0
- ivoryos/routes/data/data.py +197 -0
- ivoryos/routes/data/templates/components/step_card.html +78 -0
- ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
- ivoryos/routes/data/templates/workflow_view.html +360 -0
- ivoryos/routes/design/__init__.py +4 -0
- ivoryos/routes/design/design.py +348 -657
- ivoryos/routes/design/design_file.py +68 -0
- ivoryos/routes/design/design_step.py +171 -0
- ivoryos/routes/design/templates/components/action_form.html +53 -0
- ivoryos/routes/design/templates/components/actions_panel.html +25 -0
- ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
- ivoryos/routes/design/templates/components/canvas.html +5 -0
- ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
- ivoryos/routes/design/templates/components/canvas_header.html +75 -0
- ivoryos/routes/design/templates/components/canvas_main.html +39 -0
- ivoryos/routes/design/templates/components/deck_selector.html +10 -0
- ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
- ivoryos/routes/design/templates/components/info_modal.html +318 -0
- ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
- ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
- ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
- ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
- ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
- ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
- ivoryos/routes/design/templates/components/modals.html +6 -0
- ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
- ivoryos/routes/design/templates/components/sidebar.html +15 -0
- ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
- ivoryos/routes/design/templates/experiment_builder.html +44 -0
- ivoryos/routes/execute/__init__.py +0 -0
- ivoryos/routes/execute/execute.py +377 -0
- ivoryos/routes/execute/execute_file.py +78 -0
- ivoryos/routes/execute/templates/components/error_modal.html +20 -0
- ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
- ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
- ivoryos/routes/execute/templates/components/run_panel.html +9 -0
- ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
- ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
- ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
- ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
- ivoryos/routes/execute/templates/experiment_run.html +30 -0
- ivoryos/routes/library/__init__.py +0 -0
- ivoryos/routes/library/library.py +157 -0
- ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
- ivoryos/routes/main/main.py +31 -3
- ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
- ivoryos/server.py +180 -0
- ivoryos/socket_handlers.py +52 -0
- ivoryos/static/ivoryos_logo.png +0 -0
- ivoryos/static/js/action_handlers.js +384 -0
- ivoryos/static/js/db_delete.js +23 -0
- ivoryos/static/js/script_metadata.js +39 -0
- ivoryos/static/js/socket_handler.js +40 -5
- ivoryos/static/js/sortable_design.js +107 -56
- ivoryos/static/js/ui_state.js +114 -0
- ivoryos/templates/base.html +67 -8
- ivoryos/utils/bo_campaign.py +180 -3
- ivoryos/utils/client_proxy.py +267 -36
- ivoryos/utils/db_models.py +300 -65
- ivoryos/utils/decorators.py +34 -0
- ivoryos/utils/form.py +63 -29
- ivoryos/utils/global_config.py +34 -1
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/py_to_json.py +295 -0
- ivoryos/utils/script_runner.py +599 -165
- ivoryos/utils/serilize.py +201 -0
- ivoryos/utils/task_runner.py +71 -21
- ivoryos/utils/utils.py +50 -6
- ivoryos/version.py +1 -1
- ivoryos-1.4.4.dist-info/METADATA +263 -0
- ivoryos-1.4.4.dist-info/RECORD +119 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
- tests/unit/test_type_conversion.py +42 -0
- tests/unit/test_util.py +3 -0
- ivoryos/routes/control/templates/control/controllers.html +0 -78
- ivoryos/routes/control/templates/control/controllers_home.html +0 -55
- ivoryos/routes/control/templates/control/controllers_new.html +0 -89
- ivoryos/routes/database/database.py +0 -306
- ivoryos/routes/database/templates/database/step_card.html +0 -7
- ivoryos/routes/database/templates/database/workflow_view.html +0 -130
- ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
- ivoryos/routes/design/templates/design/experiment_run.html +0 -558
- ivoryos-1.0.9.dist-info/METADATA +0 -218
- ivoryos-1.0.9.dist-info/RECORD +0 -61
- /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
- /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
- /ivoryos/routes/{database → data}/__init__.py +0 -0
- /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
- {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
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
|
+
"""
|