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
ivoryos/utils/form.py
CHANGED
|
@@ -210,10 +210,10 @@ class FlexibleEnumField(StringField):
|
|
|
210
210
|
if key in self.choices:
|
|
211
211
|
# Convert the string key to Enum instance
|
|
212
212
|
self.data = self.enum_class[key].value
|
|
213
|
-
elif
|
|
213
|
+
elif key.startswith("#"):
|
|
214
214
|
if not self.script.editing_type == "script":
|
|
215
215
|
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
|
216
|
-
self.data =
|
|
216
|
+
self.data = key
|
|
217
217
|
else:
|
|
218
218
|
raise ValidationError(
|
|
219
219
|
f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
|
|
@@ -286,7 +286,9 @@ def create_form_for_method(method, autofill, script=None, design=True):
|
|
|
286
286
|
# enum_class = [(e.name, e.value) for e in param.annotation]
|
|
287
287
|
field_class = FlexibleEnumField
|
|
288
288
|
placeholder_text = f"Choose or type a value for {param.annotation.__name__} (start with # for custom)"
|
|
289
|
+
|
|
289
290
|
extra_kwargs = {"choices": param.annotation}
|
|
291
|
+
|
|
290
292
|
else:
|
|
291
293
|
# print(param.annotation)
|
|
292
294
|
annotation, optional = parse_annotation(param.annotation)
|
|
@@ -299,6 +301,11 @@ def create_form_for_method(method, autofill, script=None, design=True):
|
|
|
299
301
|
if optional:
|
|
300
302
|
field_kwargs["filters"] = [lambda x: x if x != '' else None]
|
|
301
303
|
|
|
304
|
+
if annotation is bool:
|
|
305
|
+
# Boolean fields should not use InputRequired
|
|
306
|
+
field_kwargs["validators"] = [] # or [Optional()]
|
|
307
|
+
else:
|
|
308
|
+
field_kwargs["validators"] = [InputRequired()] if param.default is param.empty else [Optional()]
|
|
302
309
|
|
|
303
310
|
render_kwargs = {"placeholder": placeholder_text}
|
|
304
311
|
|
|
@@ -325,7 +332,9 @@ def create_add_form(attr, attr_name, autofill: bool, script=None, design: bool =
|
|
|
325
332
|
dynamic_form = create_form_for_method(signature, autofill, script, design)
|
|
326
333
|
if design:
|
|
327
334
|
return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
|
|
335
|
+
batch_action = BooleanField(label='Batch Action', render_kw={"placeholder": "Optional"})
|
|
328
336
|
setattr(dynamic_form, 'return', return_value)
|
|
337
|
+
setattr(dynamic_form, 'batch_action', batch_action)
|
|
329
338
|
hidden_method_name = HiddenField(name=f'hidden_name', description=docstring, render_kw={"value": f'{attr_name}'})
|
|
330
339
|
setattr(dynamic_form, 'hidden_name', hidden_method_name)
|
|
331
340
|
return dynamic_form
|
|
@@ -341,13 +350,16 @@ def create_form_from_module(sdl_module, autofill: bool = False, script=None, des
|
|
|
341
350
|
"""
|
|
342
351
|
method_forms = {}
|
|
343
352
|
for attr_name in dir(sdl_module):
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
353
|
+
try:
|
|
354
|
+
method = getattr(sdl_module, attr_name)
|
|
355
|
+
if inspect.ismethod(method) and not attr_name.startswith('_'):
|
|
356
|
+
signature = inspect.signature(method)
|
|
357
|
+
docstring = inspect.getdoc(method)
|
|
358
|
+
attr = dict(signature=signature, docstring=docstring)
|
|
359
|
+
form_class = create_add_form(attr, attr_name, autofill, script, design)
|
|
360
|
+
method_forms[attr_name] = form_class()
|
|
361
|
+
except Exception as e:
|
|
362
|
+
print(f"Error creating form for {attr_name}: {e}")
|
|
351
363
|
return method_forms
|
|
352
364
|
|
|
353
365
|
|
|
@@ -398,14 +410,24 @@ def create_form_from_action(action: dict, script=None, design=True):
|
|
|
398
410
|
for name, param_type in arg_types.items():
|
|
399
411
|
# formatted_param_name = format_name(name)
|
|
400
412
|
value = args.get(name, "")
|
|
401
|
-
if type(value) is dict:
|
|
413
|
+
if type(value) is dict and value:
|
|
402
414
|
value = next(iter(value))
|
|
415
|
+
if not value or value == "None":
|
|
416
|
+
value = None
|
|
417
|
+
else:
|
|
418
|
+
value = f'{value}'
|
|
419
|
+
|
|
403
420
|
field_kwargs = {
|
|
404
421
|
"label": name,
|
|
405
|
-
"default": f'{value}',
|
|
406
|
-
|
|
422
|
+
"default": f'{value}' if value else None,
|
|
423
|
+
# todo get optional/required from snapshot
|
|
424
|
+
"validators": [Optional()],
|
|
407
425
|
**({"script": script})
|
|
408
426
|
}
|
|
427
|
+
if type(param_type) is list:
|
|
428
|
+
none_type = param_type[1]
|
|
429
|
+
if none_type == "NoneType":
|
|
430
|
+
param_type = param_type[0]
|
|
409
431
|
param_type = param_type if type(param_type) is str else f"{param_type}"
|
|
410
432
|
field_class, placeholder_text = annotation_mapping.get(
|
|
411
433
|
param_type,
|
|
@@ -418,13 +440,16 @@ def create_form_from_action(action: dict, script=None, design=True):
|
|
|
418
440
|
setattr(DynamicForm, name, field)
|
|
419
441
|
|
|
420
442
|
if design:
|
|
443
|
+
if "batch_action" in action:
|
|
444
|
+
batch_action = BooleanField(label='Batch Action', default=bool(action["batch_action"]))
|
|
445
|
+
setattr(DynamicForm, 'batch_action', batch_action)
|
|
421
446
|
return_value = StringField(label='Save value as', default=f"{save_as}", render_kw={"placeholder": "Optional"})
|
|
422
447
|
setattr(DynamicForm, 'return', return_value)
|
|
423
448
|
return DynamicForm()
|
|
424
449
|
|
|
425
450
|
def create_all_builtin_forms(script):
|
|
426
451
|
all_builtin_forms = {}
|
|
427
|
-
for logic_name in ['if', 'while', 'variable', 'wait', 'repeat']:
|
|
452
|
+
for logic_name in ['if', 'while', 'variable', 'wait', 'repeat', 'pause']:
|
|
428
453
|
# signature = info.get('signature', {})
|
|
429
454
|
form_class = create_builtin_form(logic_name, script)
|
|
430
455
|
all_builtin_forms[logic_name] = form_class()
|
|
@@ -439,7 +464,8 @@ def create_builtin_form(logic_type, script):
|
|
|
439
464
|
|
|
440
465
|
placeholder_text = {
|
|
441
466
|
'wait': 'Enter second',
|
|
442
|
-
'repeat': 'Enter an integer'
|
|
467
|
+
'repeat': 'Enter an integer',
|
|
468
|
+
'pause': 'Human Intervention Message'
|
|
443
469
|
}.get(logic_type, 'Enter statement')
|
|
444
470
|
description_text = {
|
|
445
471
|
'variable': 'Your variable can be numbers, boolean (True or False) or text ("text")',
|
|
@@ -529,9 +555,12 @@ def _action_button(action: dict, variables: dict):
|
|
|
529
555
|
"""
|
|
530
556
|
style = {
|
|
531
557
|
"repeat": "background-color: lightsteelblue",
|
|
532
|
-
"if": "background-color:
|
|
533
|
-
"while": "background-color:
|
|
558
|
+
"if": "background-color: mistyrose",
|
|
559
|
+
"while": "background-color: #a8b5a2",
|
|
560
|
+
"pause": "background-color: palegoldenrod",
|
|
534
561
|
}.get(action['instrument'], "")
|
|
562
|
+
if not style:
|
|
563
|
+
style = "background-color: thistle" if 'batch_action' in action and action["batch_action"] else ""
|
|
535
564
|
|
|
536
565
|
if action['instrument'] in ['if', 'while', 'repeat']:
|
|
537
566
|
text = f"{action['action']} {action['args'].get('statement', '')}"
|
|
@@ -547,9 +576,13 @@ def _action_button(action: dict, variables: dict):
|
|
|
547
576
|
arg_list = []
|
|
548
577
|
for k, v in action['args'].items():
|
|
549
578
|
if isinstance(v, dict):
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
579
|
+
if not v:
|
|
580
|
+
value = v # Keep the original value if not a dict
|
|
581
|
+
else:
|
|
582
|
+
value = next(iter(v)) # Extract the first key if it's a dict
|
|
583
|
+
# show warning color for variable calling when there is no definition
|
|
584
|
+
|
|
585
|
+
style = "background-color: khaki" if v.get(value) == "function_output" and value not in variables.keys() else ""
|
|
553
586
|
else:
|
|
554
587
|
value = v # Keep the original value if not a dict
|
|
555
588
|
arg_list.append(f"{k} = {value}") # Format the key-value pair
|
ivoryos/utils/global_config.py
CHANGED
|
@@ -8,6 +8,7 @@ class GlobalConfig:
|
|
|
8
8
|
if cls._instance is None:
|
|
9
9
|
cls._instance = super(GlobalConfig, cls).__new__(cls, *args, **kwargs)
|
|
10
10
|
cls._instance._deck = None
|
|
11
|
+
cls._instance._building_blocks = None
|
|
11
12
|
cls._instance._registered_workflows = None
|
|
12
13
|
cls._instance._agent = None
|
|
13
14
|
cls._instance._defined_variables = {}
|
|
@@ -16,6 +17,8 @@ class GlobalConfig:
|
|
|
16
17
|
cls._instance._runner_lock = threading.Lock()
|
|
17
18
|
cls._instance._runner_status = None
|
|
18
19
|
cls._instance._optimizers = {}
|
|
20
|
+
cls._instance._notification_handlers = []
|
|
21
|
+
|
|
19
22
|
return cls._instance
|
|
20
23
|
|
|
21
24
|
@property
|
|
@@ -27,6 +30,24 @@ class GlobalConfig:
|
|
|
27
30
|
if self._deck is None:
|
|
28
31
|
self._deck = value
|
|
29
32
|
|
|
33
|
+
def register_notification(self, handler):
|
|
34
|
+
if not callable(handler):
|
|
35
|
+
raise ValueError("Handler must be callable")
|
|
36
|
+
self._notification_handlers.append(handler)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def notification_handlers(self):
|
|
40
|
+
return self._notification_handlers
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def building_blocks(self):
|
|
44
|
+
return self._building_blocks
|
|
45
|
+
|
|
46
|
+
@building_blocks.setter
|
|
47
|
+
def building_blocks(self, value):
|
|
48
|
+
if self._building_blocks is None:
|
|
49
|
+
self._building_blocks = value
|
|
50
|
+
|
|
30
51
|
@property
|
|
31
52
|
def registered_workflows(self):
|
|
32
53
|
return self._registered_workflows
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
class ConditionalStructureError(Exception):
|
|
2
|
+
"""Raised when conditional structure is invalid"""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConditionalStructureError(Exception):
|
|
7
|
+
"""Raised when control flow structure is invalid"""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConditionalStructureError(Exception):
|
|
12
|
+
"""Raised when control flow structure is invalid"""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_and_nest_control_flow(flat_steps):
|
|
17
|
+
"""
|
|
18
|
+
Validates and converts flat control flow structures to nested blocks.
|
|
19
|
+
|
|
20
|
+
Handles:
|
|
21
|
+
- if/else/endif -> if_block/else_block
|
|
22
|
+
- repeat/endrepeat -> repeat_block
|
|
23
|
+
- while/endwhile -> while_block
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ConditionalStructureError: If the control flow structure is invalid
|
|
27
|
+
"""
|
|
28
|
+
nested_steps = []
|
|
29
|
+
i = 0
|
|
30
|
+
|
|
31
|
+
while i < len(flat_steps):
|
|
32
|
+
step = flat_steps[i]
|
|
33
|
+
action = step["action"]
|
|
34
|
+
|
|
35
|
+
# Check for misplaced closing/middle statements
|
|
36
|
+
if action in ["else", "endif", "endrepeat", "endwhile"]:
|
|
37
|
+
raise ConditionalStructureError(
|
|
38
|
+
f"Found '{action}' at position {i} without matching opening statement. "
|
|
39
|
+
f"UUID: {step.get('uuid')}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Handle IF statements
|
|
43
|
+
if action == "if":
|
|
44
|
+
nested_step, steps_consumed = process_if_block(flat_steps, i)
|
|
45
|
+
nested_steps.append(nested_step)
|
|
46
|
+
i += steps_consumed
|
|
47
|
+
|
|
48
|
+
# Handle REPEAT statements
|
|
49
|
+
elif action == "repeat":
|
|
50
|
+
nested_step, steps_consumed = process_repeat_block(flat_steps, i)
|
|
51
|
+
nested_steps.append(nested_step)
|
|
52
|
+
i += steps_consumed
|
|
53
|
+
|
|
54
|
+
# Handle WHILE statements
|
|
55
|
+
elif action == "while":
|
|
56
|
+
nested_step, steps_consumed = process_while_block(flat_steps, i)
|
|
57
|
+
nested_steps.append(nested_step)
|
|
58
|
+
i += steps_consumed
|
|
59
|
+
|
|
60
|
+
else:
|
|
61
|
+
# Regular step - add as-is
|
|
62
|
+
nested_steps.append(step)
|
|
63
|
+
i += 1
|
|
64
|
+
|
|
65
|
+
return nested_steps
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def process_if_block(flat_steps, start_index):
|
|
69
|
+
"""
|
|
70
|
+
Process an if/else/endif block starting at start_index.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
tuple: (nested_step_with_blocks, total_steps_consumed)
|
|
74
|
+
"""
|
|
75
|
+
step = flat_steps[start_index].copy()
|
|
76
|
+
if_uuid = step["uuid"]
|
|
77
|
+
if_block = []
|
|
78
|
+
else_block = []
|
|
79
|
+
current_block = if_block
|
|
80
|
+
found_else = False
|
|
81
|
+
found_endif = False
|
|
82
|
+
i = start_index + 1
|
|
83
|
+
|
|
84
|
+
while i < len(flat_steps):
|
|
85
|
+
current_step = flat_steps[i]
|
|
86
|
+
current_action = current_step["action"]
|
|
87
|
+
|
|
88
|
+
if current_action == "else":
|
|
89
|
+
if current_step["uuid"] == if_uuid:
|
|
90
|
+
if found_else:
|
|
91
|
+
raise ConditionalStructureError(
|
|
92
|
+
f"Multiple 'else' blocks found for if statement with UUID {if_uuid}. "
|
|
93
|
+
f"Second 'else' at position {i}"
|
|
94
|
+
)
|
|
95
|
+
current_block = else_block
|
|
96
|
+
found_else = True
|
|
97
|
+
i += 1
|
|
98
|
+
continue
|
|
99
|
+
else:
|
|
100
|
+
raise ConditionalStructureError(
|
|
101
|
+
f"Found 'else' with UUID {current_step['uuid']} at position {i}, "
|
|
102
|
+
f"but expecting endif for if with UUID {if_uuid}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
elif current_action == "endif":
|
|
106
|
+
if current_step["uuid"] == if_uuid:
|
|
107
|
+
found_endif = True
|
|
108
|
+
i += 1
|
|
109
|
+
break
|
|
110
|
+
else:
|
|
111
|
+
raise ConditionalStructureError(
|
|
112
|
+
f"Found 'endif' with UUID {current_step['uuid']} at position {i}, "
|
|
113
|
+
f"but expecting endif for if with UUID {if_uuid}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
elif current_action == "if":
|
|
117
|
+
# Nested if - process it
|
|
118
|
+
try:
|
|
119
|
+
nested_step, steps_consumed = process_if_block(flat_steps, i)
|
|
120
|
+
current_block.append(nested_step)
|
|
121
|
+
i += steps_consumed
|
|
122
|
+
except ConditionalStructureError as e:
|
|
123
|
+
raise ConditionalStructureError(
|
|
124
|
+
f"Error in nested if starting at position {i}: {str(e)}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
elif current_action == "repeat":
|
|
128
|
+
# Nested repeat - process it
|
|
129
|
+
try:
|
|
130
|
+
nested_step, steps_consumed = process_repeat_block(flat_steps, i)
|
|
131
|
+
current_block.append(nested_step)
|
|
132
|
+
i += steps_consumed
|
|
133
|
+
except ConditionalStructureError as e:
|
|
134
|
+
raise ConditionalStructureError(
|
|
135
|
+
f"Error in nested repeat starting at position {i}: {str(e)}"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
elif current_action == "while":
|
|
139
|
+
# Nested while - process it
|
|
140
|
+
try:
|
|
141
|
+
nested_step, steps_consumed = process_while_block(flat_steps, i)
|
|
142
|
+
current_block.append(nested_step)
|
|
143
|
+
i += steps_consumed
|
|
144
|
+
except ConditionalStructureError as e:
|
|
145
|
+
raise ConditionalStructureError(
|
|
146
|
+
f"Error in nested while starting at position {i}: {str(e)}"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
else:
|
|
150
|
+
# Regular step
|
|
151
|
+
current_block.append(current_step)
|
|
152
|
+
i += 1
|
|
153
|
+
|
|
154
|
+
if not found_endif:
|
|
155
|
+
raise ConditionalStructureError(
|
|
156
|
+
f"Missing 'endif' for if statement with UUID {if_uuid} starting at position {start_index}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
step["if_block"] = if_block
|
|
160
|
+
step["else_block"] = else_block
|
|
161
|
+
|
|
162
|
+
return step, i - start_index
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def process_repeat_block(flat_steps, start_index):
|
|
166
|
+
"""
|
|
167
|
+
Process a repeat/endrepeat block starting at start_index.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
tuple: (nested_step_with_block, total_steps_consumed)
|
|
171
|
+
"""
|
|
172
|
+
step = flat_steps[start_index].copy()
|
|
173
|
+
repeat_uuid = step["uuid"]
|
|
174
|
+
repeat_block = []
|
|
175
|
+
found_endrepeat = False
|
|
176
|
+
i = start_index + 1
|
|
177
|
+
|
|
178
|
+
while i < len(flat_steps):
|
|
179
|
+
current_step = flat_steps[i]
|
|
180
|
+
current_action = current_step["action"]
|
|
181
|
+
|
|
182
|
+
if current_action == "endrepeat":
|
|
183
|
+
if current_step["uuid"] == repeat_uuid:
|
|
184
|
+
found_endrepeat = True
|
|
185
|
+
i += 1
|
|
186
|
+
break
|
|
187
|
+
else:
|
|
188
|
+
raise ConditionalStructureError(
|
|
189
|
+
f"Found 'endrepeat' with UUID {current_step['uuid']} at position {i}, "
|
|
190
|
+
f"but expecting endrepeat for repeat with UUID {repeat_uuid}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
elif current_action == "if":
|
|
194
|
+
# Nested if - process it
|
|
195
|
+
try:
|
|
196
|
+
nested_step, steps_consumed = process_if_block(flat_steps, i)
|
|
197
|
+
repeat_block.append(nested_step)
|
|
198
|
+
i += steps_consumed
|
|
199
|
+
except ConditionalStructureError as e:
|
|
200
|
+
raise ConditionalStructureError(
|
|
201
|
+
f"Error in nested if starting at position {i}: {str(e)}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
elif current_action == "repeat":
|
|
205
|
+
# Nested repeat - process it
|
|
206
|
+
try:
|
|
207
|
+
nested_step, steps_consumed = process_repeat_block(flat_steps, i)
|
|
208
|
+
repeat_block.append(nested_step)
|
|
209
|
+
i += steps_consumed
|
|
210
|
+
except ConditionalStructureError as e:
|
|
211
|
+
raise ConditionalStructureError(
|
|
212
|
+
f"Error in nested repeat starting at position {i}: {str(e)}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
elif current_action == "while":
|
|
216
|
+
# Nested while - process it
|
|
217
|
+
try:
|
|
218
|
+
nested_step, steps_consumed = process_while_block(flat_steps, i)
|
|
219
|
+
repeat_block.append(nested_step)
|
|
220
|
+
i += steps_consumed
|
|
221
|
+
except ConditionalStructureError as e:
|
|
222
|
+
raise ConditionalStructureError(
|
|
223
|
+
f"Error in nested while starting at position {i}: {str(e)}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
else:
|
|
227
|
+
# Regular step
|
|
228
|
+
repeat_block.append(current_step)
|
|
229
|
+
i += 1
|
|
230
|
+
|
|
231
|
+
if not found_endrepeat:
|
|
232
|
+
raise ConditionalStructureError(
|
|
233
|
+
f"Missing 'endrepeat' for repeat statement with UUID {repeat_uuid} starting at position {start_index}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
step["repeat_block"] = repeat_block
|
|
237
|
+
|
|
238
|
+
return step, i - start_index
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def process_while_block(flat_steps, start_index):
|
|
242
|
+
"""
|
|
243
|
+
Process a while/endwhile block starting at start_index.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
tuple: (nested_step_with_block, total_steps_consumed)
|
|
247
|
+
"""
|
|
248
|
+
step = flat_steps[start_index].copy()
|
|
249
|
+
while_uuid = step["uuid"]
|
|
250
|
+
while_block = []
|
|
251
|
+
found_endwhile = False
|
|
252
|
+
i = start_index + 1
|
|
253
|
+
|
|
254
|
+
while i < len(flat_steps):
|
|
255
|
+
current_step = flat_steps[i]
|
|
256
|
+
current_action = current_step["action"]
|
|
257
|
+
|
|
258
|
+
if current_action == "endwhile":
|
|
259
|
+
if current_step["uuid"] == while_uuid:
|
|
260
|
+
found_endwhile = True
|
|
261
|
+
i += 1
|
|
262
|
+
break
|
|
263
|
+
else:
|
|
264
|
+
raise ConditionalStructureError(
|
|
265
|
+
f"Found 'endwhile' with UUID {current_step['uuid']} at position {i}, "
|
|
266
|
+
f"but expecting endwhile for while with UUID {while_uuid}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
elif current_action == "if":
|
|
270
|
+
# Nested if - process it
|
|
271
|
+
try:
|
|
272
|
+
nested_step, steps_consumed = process_if_block(flat_steps, i)
|
|
273
|
+
while_block.append(nested_step)
|
|
274
|
+
i += steps_consumed
|
|
275
|
+
except ConditionalStructureError as e:
|
|
276
|
+
raise ConditionalStructureError(
|
|
277
|
+
f"Error in nested if starting at position {i}: {str(e)}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
elif current_action == "repeat":
|
|
281
|
+
# Nested repeat - process it
|
|
282
|
+
try:
|
|
283
|
+
nested_step, steps_consumed = process_repeat_block(flat_steps, i)
|
|
284
|
+
while_block.append(nested_step)
|
|
285
|
+
i += steps_consumed
|
|
286
|
+
except ConditionalStructureError as e:
|
|
287
|
+
raise ConditionalStructureError(
|
|
288
|
+
f"Error in nested repeat starting at position {i}: {str(e)}"
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
elif current_action == "while":
|
|
292
|
+
# Nested while - process it
|
|
293
|
+
try:
|
|
294
|
+
nested_step, steps_consumed = process_while_block(flat_steps, i)
|
|
295
|
+
while_block.append(nested_step)
|
|
296
|
+
i += steps_consumed
|
|
297
|
+
except ConditionalStructureError as e:
|
|
298
|
+
raise ConditionalStructureError(
|
|
299
|
+
f"Error in nested while starting at position {i}: {str(e)}"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
else:
|
|
303
|
+
# Regular step
|
|
304
|
+
while_block.append(current_step)
|
|
305
|
+
i += 1
|
|
306
|
+
|
|
307
|
+
if not found_endwhile:
|
|
308
|
+
raise ConditionalStructureError(
|
|
309
|
+
f"Missing 'endwhile' for while statement with UUID {while_uuid} starting at position {start_index}"
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
step["while_block"] = while_block
|
|
313
|
+
|
|
314
|
+
return step, i - start_index
|
ivoryos/utils/py_to_json.py
CHANGED
|
@@ -2,6 +2,20 @@ import ast
|
|
|
2
2
|
import json
|
|
3
3
|
import uuid
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
7
|
+
|
|
8
|
+
global_config = GlobalConfig()
|
|
9
|
+
|
|
10
|
+
if global_config.building_blocks:
|
|
11
|
+
building_blocks = {
|
|
12
|
+
inner_key: f"{block_key}.{inner_key}"
|
|
13
|
+
for block_key, block_value in global_config.building_blocks.items()
|
|
14
|
+
for inner_key in block_value.keys()
|
|
15
|
+
}
|
|
16
|
+
else:
|
|
17
|
+
building_blocks = {}
|
|
18
|
+
|
|
5
19
|
def generate_uuid():
|
|
6
20
|
return int(str(uuid.uuid4().int)[:15])
|
|
7
21
|
|
|
@@ -37,10 +51,14 @@ def convert_to_cards(source_code: str):
|
|
|
37
51
|
def is_supported_assignment(node):
|
|
38
52
|
return (
|
|
39
53
|
isinstance(node.targets[0], ast.Name) and
|
|
40
|
-
isinstance(node.value,
|
|
54
|
+
isinstance(node.value, ast.Constant)
|
|
41
55
|
)
|
|
42
56
|
|
|
43
57
|
class CardVisitor(ast.NodeVisitor):
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self.defined_types = {} # <-- always exists
|
|
60
|
+
|
|
61
|
+
|
|
44
62
|
def visit_FunctionDef(self, node):
|
|
45
63
|
self.defined_types = {
|
|
46
64
|
arg.arg: ast.unparse(arg.annotation) if arg.annotation else "float"
|
|
@@ -128,14 +146,20 @@ def convert_to_cards(source_code: str):
|
|
|
128
146
|
"return": "",
|
|
129
147
|
"uuid": generate_uuid()
|
|
130
148
|
})
|
|
149
|
+
elif isinstance(node.value, ast.Await):
|
|
150
|
+
self.handle_call(node.value.value, ret_var=node.targets[0].id, awaited=True)
|
|
151
|
+
|
|
131
152
|
elif isinstance(node.value, ast.Call):
|
|
132
153
|
self.handle_call(node.value, ret_var=node.targets[0].id)
|
|
133
154
|
|
|
134
155
|
def visit_Expr(self, node):
|
|
135
|
-
if isinstance(node.value, ast.
|
|
156
|
+
if isinstance(node.value, ast.Await):
|
|
157
|
+
# node.value is ast.Await
|
|
158
|
+
self.handle_call(node.value.value, awaited=True)
|
|
159
|
+
elif isinstance(node.value, ast.Call):
|
|
136
160
|
self.handle_call(node.value)
|
|
137
161
|
|
|
138
|
-
def handle_call(self, node, ret_var=""):
|
|
162
|
+
def handle_call(self, node, ret_var="", awaited=False):
|
|
139
163
|
func_parts = []
|
|
140
164
|
f = node.func
|
|
141
165
|
while isinstance(f, ast.Attribute):
|
|
@@ -143,11 +167,48 @@ def convert_to_cards(source_code: str):
|
|
|
143
167
|
f = f.value
|
|
144
168
|
if isinstance(f, ast.Name):
|
|
145
169
|
func_parts.insert(0, f.id)
|
|
146
|
-
|
|
170
|
+
|
|
171
|
+
full_func_name = ".".join(func_parts)
|
|
172
|
+
|
|
173
|
+
# Check if this is a deck call or a building block
|
|
174
|
+
if full_func_name.startswith("deck.") or full_func_name.startswith("blocks."):
|
|
175
|
+
instrument = ".".join(func_parts[:-1])
|
|
176
|
+
action = func_parts[-1]
|
|
177
|
+
# not starting with deck or block, check if it's a decorated function
|
|
178
|
+
# ["general", "action"] or ["action"]
|
|
179
|
+
elif func_parts[-1] in building_blocks.keys():
|
|
180
|
+
instrument = building_blocks.get(func_parts[-1])
|
|
181
|
+
action = func_parts[-1]
|
|
182
|
+
else:
|
|
183
|
+
# ignore other calls
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# --- special case for time.sleep ---
|
|
189
|
+
if instrument == "time" and action == "sleep":
|
|
190
|
+
wait_value = None
|
|
191
|
+
if node.args:
|
|
192
|
+
arg_node = node.args[0]
|
|
193
|
+
if isinstance(arg_node, ast.Constant):
|
|
194
|
+
wait_value = arg_node.value
|
|
195
|
+
elif isinstance(arg_node, ast.Name):
|
|
196
|
+
wait_value = f"#{arg_node.id}"
|
|
197
|
+
else:
|
|
198
|
+
wait_value = ast.unparse(arg_node)
|
|
199
|
+
|
|
200
|
+
add_card({
|
|
201
|
+
"action": "wait",
|
|
202
|
+
"arg_types": {"statement": infer_type(wait_value)},
|
|
203
|
+
"args": {"statement": wait_value},
|
|
204
|
+
"id": new_id(),
|
|
205
|
+
"instrument": "wait",
|
|
206
|
+
"return": ret_var,
|
|
207
|
+
"uuid": generate_uuid()
|
|
208
|
+
})
|
|
147
209
|
return
|
|
210
|
+
# -----------------------------------
|
|
148
211
|
|
|
149
|
-
instrument = ".".join(func_parts[:-1])
|
|
150
|
-
action = func_parts[-1]
|
|
151
212
|
|
|
152
213
|
args = {}
|
|
153
214
|
arg_types = {}
|
|
@@ -155,7 +216,7 @@ def convert_to_cards(source_code: str):
|
|
|
155
216
|
for kw in node.keywords:
|
|
156
217
|
if kw.arg is None and isinstance(kw.value, ast.Dict):
|
|
157
218
|
for k_node, v_node in zip(kw.value.keys, kw.value.values):
|
|
158
|
-
key = k_node.
|
|
219
|
+
key = k_node.value if isinstance(k_node, ast.Constant) else ast.unparse(k_node)
|
|
159
220
|
if isinstance(v_node, ast.Constant):
|
|
160
221
|
value = v_node.value
|
|
161
222
|
elif isinstance(v_node, ast.Name):
|
|
@@ -178,7 +239,7 @@ def convert_to_cards(source_code: str):
|
|
|
178
239
|
else infer_type(value)
|
|
179
240
|
)
|
|
180
241
|
|
|
181
|
-
|
|
242
|
+
card = {
|
|
182
243
|
"action": action,
|
|
183
244
|
"arg_types": arg_types,
|
|
184
245
|
"args": args,
|
|
@@ -186,7 +247,12 @@ def convert_to_cards(source_code: str):
|
|
|
186
247
|
"instrument": instrument,
|
|
187
248
|
"return": ret_var,
|
|
188
249
|
"uuid": generate_uuid()
|
|
189
|
-
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if awaited:
|
|
253
|
+
card["coroutine"] = True # mark as coroutine if awaited
|
|
254
|
+
|
|
255
|
+
add_card(card)
|
|
190
256
|
|
|
191
257
|
CardVisitor().visit(tree)
|
|
192
258
|
return cards
|
|
@@ -216,10 +282,14 @@ if __name__ == "__main__":
|
|
|
216
282
|
# Step 4: Analyze the sample
|
|
217
283
|
analysis_results = deck.sdl.analyze(param_1=1, param_2=2)
|
|
218
284
|
|
|
285
|
+
# test block
|
|
286
|
+
result = blocks.general.test(**{'a': 1, 'b': 2})
|
|
287
|
+
|
|
219
288
|
# Brief pause for system stability
|
|
220
289
|
time.sleep(1.0)
|
|
221
290
|
|
|
222
291
|
# Return only analysis results
|
|
223
292
|
return {'analysis_results': analysis_results}
|
|
224
293
|
'''
|
|
225
|
-
|
|
294
|
+
from pprint import pprint
|
|
295
|
+
pprint(json.dumps(convert_to_cards(test)))
|