ivoryos 1.3.9__py3-none-any.whl → 1.4.1__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.
Potentially problematic release.
This version of ivoryos might be problematic. Click here for more details.
- ivoryos/optimizer/ax_optimizer.py +47 -25
- ivoryos/optimizer/base_optimizer.py +19 -1
- ivoryos/optimizer/baybe_optimizer.py +27 -17
- ivoryos/optimizer/nimo_optimizer.py +25 -16
- ivoryos/routes/data/data.py +27 -9
- ivoryos/routes/data/templates/components/step_card.html +47 -11
- ivoryos/routes/data/templates/workflow_view.html +14 -5
- ivoryos/routes/design/design.py +31 -1
- ivoryos/routes/design/design_step.py +2 -1
- ivoryos/routes/design/templates/components/edit_action_form.html +16 -3
- ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
- ivoryos/routes/design/templates/experiment_builder.html +1 -0
- ivoryos/routes/execute/execute.py +71 -13
- 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/socket_handlers.py +1 -1
- ivoryos/static/js/action_handlers.js +252 -118
- ivoryos/static/js/sortable_design.js +1 -0
- ivoryos/utils/bo_campaign.py +17 -16
- ivoryos/utils/db_models.py +122 -18
- ivoryos/utils/decorators.py +1 -0
- ivoryos/utils/form.py +32 -12
- ivoryos/utils/nest_script.py +314 -0
- ivoryos/utils/script_runner.py +436 -143
- ivoryos/utils/utils.py +11 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/METADATA +6 -4
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/RECORD +35 -34
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/WHEEL +0 -0
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {ivoryos-1.3.9.dist-info → ivoryos-1.4.1.dist-info}/top_level.txt +0 -0
ivoryos/utils/db_models.py
CHANGED
|
@@ -122,7 +122,7 @@ class Script(db.Model):
|
|
|
122
122
|
pass
|
|
123
123
|
raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
|
|
124
124
|
|
|
125
|
-
def update_by_uuid(self, uuid, args, output):
|
|
125
|
+
def update_by_uuid(self, uuid, args, output, batch_action=False):
|
|
126
126
|
action = self.find_by_uuid(uuid)
|
|
127
127
|
if not action:
|
|
128
128
|
return
|
|
@@ -134,6 +134,7 @@ class Script(db.Model):
|
|
|
134
134
|
pass
|
|
135
135
|
action['args'] = args
|
|
136
136
|
action['return'] = output
|
|
137
|
+
action['batch_action'] = batch_action
|
|
137
138
|
|
|
138
139
|
@staticmethod
|
|
139
140
|
def eval_list(args, arg_types):
|
|
@@ -402,7 +403,7 @@ class Script(db.Model):
|
|
|
402
403
|
"""
|
|
403
404
|
|
|
404
405
|
return_list = set([action['return'] for action in self.script_dict['script'] if not action['return'] == ''])
|
|
405
|
-
output_str = "
|
|
406
|
+
output_str = "{"
|
|
406
407
|
for i in return_list:
|
|
407
408
|
output_str += "'" + i + "':" + i + ","
|
|
408
409
|
output_str += "}"
|
|
@@ -451,7 +452,80 @@ class Script(db.Model):
|
|
|
451
452
|
]
|
|
452
453
|
return line_collection
|
|
453
454
|
|
|
454
|
-
def
|
|
455
|
+
def render_script_lines(self, script_dict):
|
|
456
|
+
"""
|
|
457
|
+
Convert the script_dict structure into a dict of displayable Python-like lines,
|
|
458
|
+
keeping ID consistency for highlighting.
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
def render_args(args):
|
|
462
|
+
if not args:
|
|
463
|
+
return ""
|
|
464
|
+
return ", ".join(f"{k}={v}" for k, v in args.items())
|
|
465
|
+
|
|
466
|
+
def parse_block(block):
|
|
467
|
+
lines = []
|
|
468
|
+
indent = 0
|
|
469
|
+
stack = []
|
|
470
|
+
|
|
471
|
+
for action in block:
|
|
472
|
+
act = action["action"]
|
|
473
|
+
_id = action["id"]
|
|
474
|
+
|
|
475
|
+
# Handle control structures
|
|
476
|
+
if act == "if":
|
|
477
|
+
stmt = action["args"].get("statement", "")
|
|
478
|
+
lines.append(" " * indent + f"if {stmt}: # id:{_id}")
|
|
479
|
+
indent += 1
|
|
480
|
+
stack.append("if")
|
|
481
|
+
|
|
482
|
+
elif act == "else":
|
|
483
|
+
indent -= 1
|
|
484
|
+
lines.append(" " * indent + f"else: # id:{_id}")
|
|
485
|
+
indent += 1
|
|
486
|
+
|
|
487
|
+
elif act in ("endif", "endwhile"):
|
|
488
|
+
if stack:
|
|
489
|
+
stack.pop()
|
|
490
|
+
indent = max(indent - 1, 0)
|
|
491
|
+
lines.append(" " * indent + f"# {act} # id:{_id}")
|
|
492
|
+
|
|
493
|
+
elif act == "while":
|
|
494
|
+
stmt = action["args"].get("statement", "")
|
|
495
|
+
lines.append(" " * indent + f"while {stmt}: # id:{_id}")
|
|
496
|
+
indent += 1
|
|
497
|
+
stack.append("while")
|
|
498
|
+
|
|
499
|
+
else:
|
|
500
|
+
# Regular function call
|
|
501
|
+
instr = action["instrument"]
|
|
502
|
+
args = render_args(action.get("args", {}))
|
|
503
|
+
ret = action.get("return")
|
|
504
|
+
line = " " * indent
|
|
505
|
+
if ret:
|
|
506
|
+
line += f"{ret} = {instr}.{act}({args}) # id:{_id}"
|
|
507
|
+
else:
|
|
508
|
+
line += f"{instr}.{act}({args}) # id:{_id}"
|
|
509
|
+
lines.append(line)
|
|
510
|
+
|
|
511
|
+
# Ensure empty control blocks get "pass"
|
|
512
|
+
final_lines = []
|
|
513
|
+
for i, line in enumerate(lines):
|
|
514
|
+
final_lines.append(line)
|
|
515
|
+
# if line.strip().startswith("else") and (
|
|
516
|
+
# i == len(lines) - 1 or lines[i + 1].startswith("#") or "endif" in lines[i + 1]
|
|
517
|
+
# ):
|
|
518
|
+
# final_lines.append(" " * (indent) + "pass")
|
|
519
|
+
|
|
520
|
+
return final_lines
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
"prep": parse_block(script_dict.get("prep", [])),
|
|
524
|
+
"script": parse_block(script_dict.get("script", [])),
|
|
525
|
+
"cleanup": parse_block(script_dict.get("cleanup", []))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
def compile(self, script_path=None, batch=False, mode="sample"):
|
|
455
529
|
"""
|
|
456
530
|
Compile the current script to a Python file.
|
|
457
531
|
:return: String to write to a Python file.
|
|
@@ -467,7 +541,7 @@ class Script(db.Model):
|
|
|
467
541
|
for i in self.stypes:
|
|
468
542
|
if self.script_dict[i]:
|
|
469
543
|
is_async = any(a.get("coroutine", False) for a in self.script_dict[i])
|
|
470
|
-
func_str = self._generate_function_header(run_name, i, is_async) + self._generate_function_body(i)
|
|
544
|
+
func_str = self._generate_function_header(run_name, i, is_async, batch) + self._generate_function_body(i, batch, mode)
|
|
471
545
|
exec_str_collection[i] = func_str
|
|
472
546
|
if script_path:
|
|
473
547
|
self._write_to_file(script_path, run_name, exec_str_collection)
|
|
@@ -485,7 +559,7 @@ class Script(db.Model):
|
|
|
485
559
|
name += '_'
|
|
486
560
|
return name
|
|
487
561
|
|
|
488
|
-
def _generate_function_header(self, run_name, stype, is_async):
|
|
562
|
+
def _generate_function_header(self, run_name, stype, is_async, batch=False):
|
|
489
563
|
"""
|
|
490
564
|
Generate the function header.
|
|
491
565
|
"""
|
|
@@ -499,37 +573,54 @@ class Script(db.Model):
|
|
|
499
573
|
function_header = f"{async_str}def {run_name}{script_type}("
|
|
500
574
|
|
|
501
575
|
if stype == "script":
|
|
502
|
-
|
|
503
|
-
|
|
576
|
+
if batch:
|
|
577
|
+
function_header += "param_list" if configure else "n: int"
|
|
578
|
+
else:
|
|
579
|
+
function_header += ", ".join(configure)
|
|
504
580
|
function_header += "):"
|
|
505
581
|
# function_header += self.indent(1) + f"global {run_name}_{stype}"
|
|
506
582
|
return function_header
|
|
507
583
|
|
|
508
|
-
def _generate_function_body(self, stype):
|
|
584
|
+
def _generate_function_body(self, stype, batch=False, mode="sample"):
|
|
509
585
|
"""
|
|
510
586
|
Generate the function body for each type in stypes.
|
|
511
587
|
"""
|
|
512
588
|
body = ''
|
|
513
589
|
indent_unit = 1
|
|
590
|
+
if batch and stype == "script":
|
|
591
|
+
return_str, return_list = self.config_return()
|
|
592
|
+
if return_list and stype == "script":
|
|
593
|
+
body += self.indent(indent_unit) + "result_list = []"
|
|
594
|
+
for index, action in enumerate(self.script_dict[stype]):
|
|
595
|
+
text, indent_unit = self._process_action(indent_unit, action, index, stype, batch)
|
|
596
|
+
body += text
|
|
597
|
+
if return_list and stype == "script":
|
|
598
|
+
body += self.indent(indent_unit) + f"result_list.append({return_str})"
|
|
599
|
+
body += self.indent(indent_unit) + "return result_list"
|
|
514
600
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
601
|
+
else:
|
|
602
|
+
for index, action in enumerate(self.script_dict[stype]):
|
|
603
|
+
text, indent_unit = self._process_action(indent_unit, action, index, stype)
|
|
604
|
+
body += text
|
|
605
|
+
return_str, return_list = self.config_return()
|
|
606
|
+
if return_list and stype == "script":
|
|
607
|
+
body += self.indent(indent_unit) + f"return {return_str}"
|
|
521
608
|
return body
|
|
522
609
|
|
|
523
|
-
def _process_action(self, indent_unit, action, index, stype):
|
|
610
|
+
def _process_action(self, indent_unit, action, index, stype, batch=False, mode="sample"):
|
|
524
611
|
"""
|
|
525
612
|
Process each action within the script dictionary.
|
|
526
613
|
"""
|
|
614
|
+
configure, config_type = self.config(stype)
|
|
615
|
+
|
|
527
616
|
instrument = action['instrument']
|
|
528
617
|
statement = action['args'].get('statement')
|
|
529
618
|
args = self._process_args(action['args'])
|
|
530
619
|
|
|
531
620
|
save_data = action['return']
|
|
532
621
|
action_name = action['action']
|
|
622
|
+
batch_action = action.get("batch_action", False)
|
|
623
|
+
|
|
533
624
|
next_action = self._get_next_action(stype, index)
|
|
534
625
|
# print(args)
|
|
535
626
|
if instrument == 'if':
|
|
@@ -550,7 +641,8 @@ class Script(db.Model):
|
|
|
550
641
|
# return inspect.getsource(my_function)
|
|
551
642
|
else:
|
|
552
643
|
is_async = action.get("coroutine", False)
|
|
553
|
-
|
|
644
|
+
dynamic_arg = len(configure) > 0
|
|
645
|
+
return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data, is_async, dynamic_arg, batch, batch_action)
|
|
554
646
|
|
|
555
647
|
def _process_args(self, args):
|
|
556
648
|
"""
|
|
@@ -610,7 +702,8 @@ class Script(db.Model):
|
|
|
610
702
|
indent_unit -= 1
|
|
611
703
|
return exec_string, indent_unit
|
|
612
704
|
|
|
613
|
-
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data, is_async=False
|
|
705
|
+
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data, is_async=False, dynamic_arg=False,
|
|
706
|
+
batch=False, batch_action=False):
|
|
614
707
|
"""
|
|
615
708
|
Process actions related to instruments.
|
|
616
709
|
"""
|
|
@@ -632,7 +725,18 @@ class Script(db.Model):
|
|
|
632
725
|
if save_data:
|
|
633
726
|
save_data += " = "
|
|
634
727
|
|
|
635
|
-
|
|
728
|
+
if batch and not batch_action:
|
|
729
|
+
arg_list = [args[arg][1:] for arg in args if isinstance(args[arg], str) and args[arg].startswith("#")]
|
|
730
|
+
param_str = [f"param['{arg_list}']" for arg_list in arg_list if arg_list]
|
|
731
|
+
args_str = self.indent(indent_unit + 1) + ", ".join(arg_list) + " = " + ", ".join(param_str) if arg_list else ""
|
|
732
|
+
if dynamic_arg:
|
|
733
|
+
for_string = self.indent(indent_unit) + "for param in param_list:" + args_str
|
|
734
|
+
else:
|
|
735
|
+
for_string = self.indent(indent_unit) + "for _ in range(n):"
|
|
736
|
+
output_code = for_string + self.indent(indent_unit + 1) + save_data + single_line
|
|
737
|
+
else:
|
|
738
|
+
output_code = self.indent(indent_unit) + save_data + single_line
|
|
739
|
+
return output_code, indent_unit
|
|
636
740
|
|
|
637
741
|
def _process_dict_args(self, args):
|
|
638
742
|
"""
|
ivoryos/utils/decorators.py
CHANGED
ivoryos/utils/form.py
CHANGED
|
@@ -332,7 +332,9 @@ def create_add_form(attr, attr_name, autofill: bool, script=None, design: bool =
|
|
|
332
332
|
dynamic_form = create_form_for_method(signature, autofill, script, design)
|
|
333
333
|
if design:
|
|
334
334
|
return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
|
|
335
|
+
batch_action = BooleanField(label='Batch Action', render_kw={"placeholder": "Optional"})
|
|
335
336
|
setattr(dynamic_form, 'return', return_value)
|
|
337
|
+
setattr(dynamic_form, 'batch_action', batch_action)
|
|
336
338
|
hidden_method_name = HiddenField(name=f'hidden_name', description=docstring, render_kw={"value": f'{attr_name}'})
|
|
337
339
|
setattr(dynamic_form, 'hidden_name', hidden_method_name)
|
|
338
340
|
return dynamic_form
|
|
@@ -348,13 +350,16 @@ def create_form_from_module(sdl_module, autofill: bool = False, script=None, des
|
|
|
348
350
|
"""
|
|
349
351
|
method_forms = {}
|
|
350
352
|
for attr_name in dir(sdl_module):
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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}")
|
|
358
363
|
return method_forms
|
|
359
364
|
|
|
360
365
|
|
|
@@ -407,12 +412,22 @@ def create_form_from_action(action: dict, script=None, design=True):
|
|
|
407
412
|
value = args.get(name, "")
|
|
408
413
|
if type(value) is dict and value:
|
|
409
414
|
value = next(iter(value))
|
|
415
|
+
if not value or value == "None":
|
|
416
|
+
value = None
|
|
417
|
+
else:
|
|
418
|
+
value = f'{value}'
|
|
419
|
+
|
|
410
420
|
field_kwargs = {
|
|
411
421
|
"label": name,
|
|
412
|
-
"default": f'{value}',
|
|
413
|
-
|
|
422
|
+
"default": f'{value}' if value else None,
|
|
423
|
+
# todo get optional/required from snapshot
|
|
424
|
+
"validators": [Optional()],
|
|
414
425
|
**({"script": script})
|
|
415
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]
|
|
416
431
|
param_type = param_type if type(param_type) is str else f"{param_type}"
|
|
417
432
|
field_class, placeholder_text = annotation_mapping.get(
|
|
418
433
|
param_type,
|
|
@@ -425,6 +440,9 @@ def create_form_from_action(action: dict, script=None, design=True):
|
|
|
425
440
|
setattr(DynamicForm, name, field)
|
|
426
441
|
|
|
427
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)
|
|
428
446
|
return_value = StringField(label='Save value as', default=f"{save_as}", render_kw={"placeholder": "Optional"})
|
|
429
447
|
setattr(DynamicForm, 'return', return_value)
|
|
430
448
|
return DynamicForm()
|
|
@@ -537,10 +555,12 @@ def _action_button(action: dict, variables: dict):
|
|
|
537
555
|
"""
|
|
538
556
|
style = {
|
|
539
557
|
"repeat": "background-color: lightsteelblue",
|
|
540
|
-
"if": "background-color:
|
|
541
|
-
"while": "background-color:
|
|
542
|
-
"pause": "background-color:
|
|
558
|
+
"if": "background-color: mistyrose",
|
|
559
|
+
"while": "background-color: #a8b5a2",
|
|
560
|
+
"pause": "background-color: palegoldenrod",
|
|
543
561
|
}.get(action['instrument'], "")
|
|
562
|
+
if not style:
|
|
563
|
+
style = "background-color: thistle" if 'batch_action' in action and action["batch_action"] else ""
|
|
544
564
|
|
|
545
565
|
if action['instrument'] in ['if', 'while', 'repeat']:
|
|
546
566
|
text = f"{action['action']} {action['args'].get('statement', '')}"
|
|
@@ -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
|