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.
Files changed (75) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +16 -246
  3. ivoryos/app.py +154 -0
  4. ivoryos/optimizer/ax_optimizer.py +55 -28
  5. ivoryos/optimizer/base_optimizer.py +20 -1
  6. ivoryos/optimizer/baybe_optimizer.py +27 -17
  7. ivoryos/optimizer/nimo_optimizer.py +173 -0
  8. ivoryos/optimizer/registry.py +3 -1
  9. ivoryos/routes/auth/auth.py +35 -8
  10. ivoryos/routes/auth/templates/change_password.html +32 -0
  11. ivoryos/routes/control/control.py +58 -28
  12. ivoryos/routes/control/control_file.py +12 -15
  13. ivoryos/routes/control/control_new_device.py +21 -11
  14. ivoryos/routes/control/templates/controllers.html +27 -0
  15. ivoryos/routes/control/utils.py +2 -0
  16. ivoryos/routes/data/data.py +110 -44
  17. ivoryos/routes/data/templates/components/step_card.html +78 -13
  18. ivoryos/routes/data/templates/workflow_view.html +343 -113
  19. ivoryos/routes/design/design.py +59 -10
  20. ivoryos/routes/design/design_file.py +3 -3
  21. ivoryos/routes/design/design_step.py +43 -17
  22. ivoryos/routes/design/templates/components/action_form.html +2 -2
  23. ivoryos/routes/design/templates/components/canvas_main.html +6 -1
  24. ivoryos/routes/design/templates/components/edit_action_form.html +18 -3
  25. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  26. ivoryos/routes/design/templates/components/instruments_panel.html +23 -1
  27. ivoryos/routes/design/templates/components/python_code_overlay.html +27 -10
  28. ivoryos/routes/design/templates/experiment_builder.html +3 -0
  29. ivoryos/routes/execute/execute.py +82 -22
  30. ivoryos/routes/execute/templates/components/logging_panel.html +50 -25
  31. ivoryos/routes/execute/templates/components/run_tabs.html +45 -2
  32. ivoryos/routes/execute/templates/components/tab_bayesian.html +447 -325
  33. ivoryos/routes/execute/templates/components/tab_configuration.html +303 -18
  34. ivoryos/routes/execute/templates/components/tab_repeat.html +6 -2
  35. ivoryos/routes/execute/templates/experiment_run.html +0 -264
  36. ivoryos/routes/library/library.py +9 -11
  37. ivoryos/routes/main/main.py +30 -2
  38. ivoryos/server.py +180 -0
  39. ivoryos/socket_handlers.py +1 -1
  40. ivoryos/static/ivoryos_logo.png +0 -0
  41. ivoryos/static/js/action_handlers.js +259 -88
  42. ivoryos/static/js/socket_handler.js +40 -5
  43. ivoryos/static/js/sortable_design.js +29 -11
  44. ivoryos/templates/base.html +61 -2
  45. ivoryos/utils/bo_campaign.py +18 -17
  46. ivoryos/utils/client_proxy.py +267 -36
  47. ivoryos/utils/db_models.py +286 -60
  48. ivoryos/utils/decorators.py +34 -0
  49. ivoryos/utils/form.py +52 -19
  50. ivoryos/utils/global_config.py +21 -0
  51. ivoryos/utils/nest_script.py +314 -0
  52. ivoryos/utils/py_to_json.py +80 -10
  53. ivoryos/utils/script_runner.py +573 -189
  54. ivoryos/utils/task_runner.py +69 -22
  55. ivoryos/utils/utils.py +48 -5
  56. ivoryos/version.py +1 -1
  57. {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/METADATA +109 -47
  58. ivoryos-1.4.4.dist-info/RECORD +119 -0
  59. ivoryos-1.4.4.dist-info/top_level.txt +3 -0
  60. tests/__init__.py +0 -0
  61. tests/conftest.py +133 -0
  62. tests/integration/__init__.py +0 -0
  63. tests/integration/test_route_auth.py +80 -0
  64. tests/integration/test_route_control.py +94 -0
  65. tests/integration/test_route_database.py +61 -0
  66. tests/integration/test_route_design.py +36 -0
  67. tests/integration/test_route_main.py +35 -0
  68. tests/integration/test_sockets.py +26 -0
  69. tests/unit/test_type_conversion.py +42 -0
  70. tests/unit/test_util.py +3 -0
  71. ivoryos/routes/api/api.py +0 -56
  72. ivoryos-1.2.5.dist-info/RECORD +0 -100
  73. ivoryos-1.2.5.dist-info/top_level.txt +0 -1
  74. {ivoryos-1.2.5.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +0 -0
  75. {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 self.data.startswith("#"):
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 = 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
- method = getattr(sdl_module, attr_name)
345
- if inspect.ismethod(method) and not attr_name.startswith('_'):
346
- signature = inspect.signature(method)
347
- docstring = inspect.getdoc(method)
348
- attr = dict(signature=signature, docstring=docstring)
349
- form_class = create_add_form(attr, attr_name, autofill, script, design)
350
- method_forms[attr_name] = form_class()
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
- "validators": [InputRequired()],
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: salmon",
533
- "while": "background-color: salmon",
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
- value = next(iter(v)) # Extract the first key if it's a dict
551
- # show warning color for variable calling when there is no definition
552
- style = "background-color: khaki" if value not in variables.keys() else ""
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
@@ -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
@@ -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, (ast.Constant, ast.Num, ast.Str, ast.NameConstant))
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.Call):
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
- if not func_parts or not func_parts[0].startswith("deck"):
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.s if isinstance(k_node, ast.Constant) else ast.unparse(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
- add_card({
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
- print(json.dumps(convert_to_cards(test)))
294
+ from pprint import pprint
295
+ pprint(json.dumps(convert_to_cards(test)))