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/form.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Union, Any
|
|
3
|
+
try:
|
|
4
|
+
from typing import get_origin, get_args
|
|
5
|
+
except ImportError:
|
|
6
|
+
# For Python versions = 3.7, use typing_extensions
|
|
7
|
+
from typing_extensions import get_origin, get_args
|
|
3
8
|
|
|
4
9
|
from wtforms.fields.choices import SelectField
|
|
5
10
|
from wtforms.fields.core import Field
|
|
@@ -205,20 +210,15 @@ class FlexibleEnumField(StringField):
|
|
|
205
210
|
if key in self.choices:
|
|
206
211
|
# Convert the string key to Enum instance
|
|
207
212
|
self.data = self.enum_class[key].value
|
|
208
|
-
elif
|
|
213
|
+
elif key.startswith("#"):
|
|
209
214
|
if not self.script.editing_type == "script":
|
|
210
215
|
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
|
211
|
-
self.data =
|
|
216
|
+
self.data = key
|
|
212
217
|
else:
|
|
213
218
|
raise ValidationError(
|
|
214
219
|
f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
|
|
215
220
|
|
|
216
221
|
|
|
217
|
-
def format_name(name):
|
|
218
|
-
"""Converts 'example_name' to 'Example Name'."""
|
|
219
|
-
name = name.split(".")[-1]
|
|
220
|
-
text = ' '.join(word for word in name.split('_'))
|
|
221
|
-
return text.capitalize()
|
|
222
222
|
|
|
223
223
|
def parse_annotation(annotation):
|
|
224
224
|
"""
|
|
@@ -264,7 +264,7 @@ def create_form_for_method(method, autofill, script=None, design=True):
|
|
|
264
264
|
for param in sig.parameters.values():
|
|
265
265
|
if param.name == 'self':
|
|
266
266
|
continue
|
|
267
|
-
formatted_param_name = format_name(param.name)
|
|
267
|
+
# formatted_param_name = format_name(param.name)
|
|
268
268
|
|
|
269
269
|
default_value = None
|
|
270
270
|
if autofill:
|
|
@@ -277,7 +277,7 @@ def create_form_for_method(method, autofill, script=None, design=True):
|
|
|
277
277
|
default_value = param.default
|
|
278
278
|
|
|
279
279
|
field_kwargs = {
|
|
280
|
-
"label":
|
|
280
|
+
"label": param.name,
|
|
281
281
|
"default": default_value,
|
|
282
282
|
"validators": [InputRequired()] if param.default is param.empty else [Optional()],
|
|
283
283
|
**({"script": script} if (autofill or design) else {})
|
|
@@ -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
|
|
|
@@ -321,10 +328,13 @@ def create_add_form(attr, attr_name, autofill: bool, script=None, design: bool =
|
|
|
321
328
|
"""
|
|
322
329
|
signature = attr.get('signature', {})
|
|
323
330
|
docstring = attr.get('docstring', "")
|
|
331
|
+
# print(signature, docstring)
|
|
324
332
|
dynamic_form = create_form_for_method(signature, autofill, script, design)
|
|
325
333
|
if design:
|
|
326
334
|
return_value = StringField(label='Save value as', render_kw={"placeholder": "Optional"})
|
|
335
|
+
batch_action = BooleanField(label='Batch Action', render_kw={"placeholder": "Optional"})
|
|
327
336
|
setattr(dynamic_form, 'return', return_value)
|
|
337
|
+
setattr(dynamic_form, 'batch_action', batch_action)
|
|
328
338
|
hidden_method_name = HiddenField(name=f'hidden_name', description=docstring, render_kw={"value": f'{attr_name}'})
|
|
329
339
|
setattr(dynamic_form, 'hidden_name', hidden_method_name)
|
|
330
340
|
return dynamic_form
|
|
@@ -340,13 +350,16 @@ def create_form_from_module(sdl_module, autofill: bool = False, script=None, des
|
|
|
340
350
|
"""
|
|
341
351
|
method_forms = {}
|
|
342
352
|
for attr_name in dir(sdl_module):
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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}")
|
|
350
363
|
return method_forms
|
|
351
364
|
|
|
352
365
|
|
|
@@ -395,16 +408,26 @@ def create_form_from_action(action: dict, script=None, design=True):
|
|
|
395
408
|
}
|
|
396
409
|
|
|
397
410
|
for name, param_type in arg_types.items():
|
|
398
|
-
formatted_param_name = format_name(name)
|
|
411
|
+
# formatted_param_name = format_name(name)
|
|
399
412
|
value = args.get(name, "")
|
|
400
|
-
if type(value) is dict:
|
|
413
|
+
if type(value) is dict and value:
|
|
401
414
|
value = next(iter(value))
|
|
415
|
+
if not value or value == "None":
|
|
416
|
+
value = None
|
|
417
|
+
else:
|
|
418
|
+
value = f'{value}'
|
|
419
|
+
|
|
402
420
|
field_kwargs = {
|
|
403
|
-
"label":
|
|
404
|
-
"default": f'{value}',
|
|
405
|
-
|
|
421
|
+
"label": name,
|
|
422
|
+
"default": f'{value}' if value else None,
|
|
423
|
+
# todo get optional/required from snapshot
|
|
424
|
+
"validators": [Optional()],
|
|
406
425
|
**({"script": script})
|
|
407
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]
|
|
408
431
|
param_type = param_type if type(param_type) is str else f"{param_type}"
|
|
409
432
|
field_class, placeholder_text = annotation_mapping.get(
|
|
410
433
|
param_type,
|
|
@@ -417,13 +440,16 @@ def create_form_from_action(action: dict, script=None, design=True):
|
|
|
417
440
|
setattr(DynamicForm, name, field)
|
|
418
441
|
|
|
419
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)
|
|
420
446
|
return_value = StringField(label='Save value as', default=f"{save_as}", render_kw={"placeholder": "Optional"})
|
|
421
447
|
setattr(DynamicForm, 'return', return_value)
|
|
422
448
|
return DynamicForm()
|
|
423
449
|
|
|
424
450
|
def create_all_builtin_forms(script):
|
|
425
451
|
all_builtin_forms = {}
|
|
426
|
-
for logic_name in ['if', 'while', 'variable', 'wait', 'repeat']:
|
|
452
|
+
for logic_name in ['if', 'while', 'variable', 'wait', 'repeat', 'pause']:
|
|
427
453
|
# signature = info.get('signature', {})
|
|
428
454
|
form_class = create_builtin_form(logic_name, script)
|
|
429
455
|
all_builtin_forms[logic_name] = form_class()
|
|
@@ -438,7 +464,8 @@ def create_builtin_form(logic_type, script):
|
|
|
438
464
|
|
|
439
465
|
placeholder_text = {
|
|
440
466
|
'wait': 'Enter second',
|
|
441
|
-
'repeat': 'Enter an integer'
|
|
467
|
+
'repeat': 'Enter an integer',
|
|
468
|
+
'pause': 'Human Intervention Message'
|
|
442
469
|
}.get(logic_type, 'Enter statement')
|
|
443
470
|
description_text = {
|
|
444
471
|
'variable': 'Your variable can be numbers, boolean (True or False) or text ("text")',
|
|
@@ -528,9 +555,12 @@ def _action_button(action: dict, variables: dict):
|
|
|
528
555
|
"""
|
|
529
556
|
style = {
|
|
530
557
|
"repeat": "background-color: lightsteelblue",
|
|
531
|
-
"if": "background-color:
|
|
532
|
-
"while": "background-color:
|
|
558
|
+
"if": "background-color: mistyrose",
|
|
559
|
+
"while": "background-color: #a8b5a2",
|
|
560
|
+
"pause": "background-color: palegoldenrod",
|
|
533
561
|
}.get(action['instrument'], "")
|
|
562
|
+
if not style:
|
|
563
|
+
style = "background-color: thistle" if 'batch_action' in action and action["batch_action"] else ""
|
|
534
564
|
|
|
535
565
|
if action['instrument'] in ['if', 'while', 'repeat']:
|
|
536
566
|
text = f"{action['action']} {action['args'].get('statement', '')}"
|
|
@@ -546,9 +576,13 @@ def _action_button(action: dict, variables: dict):
|
|
|
546
576
|
arg_list = []
|
|
547
577
|
for k, v in action['args'].items():
|
|
548
578
|
if isinstance(v, dict):
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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 ""
|
|
552
586
|
else:
|
|
553
587
|
value = v # Keep the original value if not a dict
|
|
554
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 = {}
|
|
@@ -15,6 +16,9 @@ class GlobalConfig:
|
|
|
15
16
|
cls._instance._deck_snapshot = {}
|
|
16
17
|
cls._instance._runner_lock = threading.Lock()
|
|
17
18
|
cls._instance._runner_status = None
|
|
19
|
+
cls._instance._optimizers = {}
|
|
20
|
+
cls._instance._notification_handlers = []
|
|
21
|
+
|
|
18
22
|
return cls._instance
|
|
19
23
|
|
|
20
24
|
@property
|
|
@@ -26,6 +30,24 @@ class GlobalConfig:
|
|
|
26
30
|
if self._deck is None:
|
|
27
31
|
self._deck = value
|
|
28
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
|
+
|
|
29
51
|
@property
|
|
30
52
|
def registered_workflows(self):
|
|
31
53
|
return self._registered_workflows
|
|
@@ -84,4 +106,15 @@ class GlobalConfig:
|
|
|
84
106
|
|
|
85
107
|
@runner_status.setter
|
|
86
108
|
def runner_status(self, value):
|
|
87
|
-
self._runner_status = value
|
|
109
|
+
self._runner_status = value
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def optimizers(self):
|
|
113
|
+
return self._optimizers
|
|
114
|
+
|
|
115
|
+
@optimizers.setter
|
|
116
|
+
def optimizers(self, value):
|
|
117
|
+
if isinstance(value, dict):
|
|
118
|
+
self._optimizers = value
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError("Optimizers must be a dictionary.")
|
|
@@ -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
|