ivoryos 1.0.0__py3-none-any.whl → 1.0.3__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.

@@ -3,6 +3,7 @@ import json
3
3
  import os
4
4
  import pickle
5
5
  import sys
6
+ import time
6
7
 
7
8
  from flask import Blueprint, redirect, url_for, flash, jsonify, send_file, request, render_template, session, \
8
9
  current_app, g
@@ -11,11 +12,10 @@ from flask_socketio import SocketIO
11
12
  from werkzeug.utils import secure_filename
12
13
 
13
14
  from ivoryos.utils import utils
14
- from ivoryos.utils.client_proxy import create_function, export_to_python
15
15
  from ivoryos.utils.global_config import GlobalConfig
16
16
  from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
17
17
  create_form_from_action, create_all_builtin_forms
18
- from ivoryos.utils.db_models import Script
18
+ from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
19
19
  from ivoryos.utils.script_runner import ScriptRunner
20
20
  # from ivoryos.utils.utils import load_workflows
21
21
 
@@ -25,32 +25,45 @@ design = Blueprint('design', __name__, template_folder='templates/design')
25
25
  global_config = GlobalConfig()
26
26
  runner = ScriptRunner()
27
27
 
28
-
29
- @socketio.on('abort_pending')
30
- def handle_abort_pending():
28
+ def abort_pending():
31
29
  runner.abort_pending()
32
30
  socketio.emit('log', {'message': "aborted pending iterations"})
33
31
 
34
-
35
- @socketio.on('abort_current')
36
- def handle_abort_current():
32
+ def abort_current():
37
33
  runner.stop_execution()
38
34
  socketio.emit('log', {'message': "stopped next task"})
39
35
 
40
-
41
- @socketio.on('pause')
42
- def handle_pause():
36
+ def pause():
43
37
  runner.retry = False
44
38
  msg = runner.toggle_pause()
45
39
  socketio.emit('log', {'message': msg})
40
+ return msg
46
41
 
47
- @socketio.on('retry')
48
- def handle_pause():
42
+ def retry():
49
43
  runner.retry = True
50
44
  msg = runner.toggle_pause()
51
45
  socketio.emit('log', {'message': msg})
52
46
 
53
47
 
48
+ # ---- Socket.IO Event Handlers ----
49
+
50
+ @socketio.on('abort_pending')
51
+ def handle_abort_pending():
52
+ abort_pending()
53
+
54
+ @socketio.on('abort_current')
55
+ def handle_abort_current():
56
+ abort_current()
57
+
58
+ @socketio.on('pause')
59
+ def handle_pause():
60
+ pause()
61
+
62
+ @socketio.on('retry')
63
+ def handle_retry():
64
+ retry()
65
+
66
+
54
67
  @socketio.on('connect')
55
68
  def handle_abort_action():
56
69
  # Fetch log messages from local file
@@ -61,8 +74,8 @@ def handle_abort_action():
61
74
  socketio.emit('log', {'message': message})
62
75
 
63
76
 
64
- @design.route("/experiment/build/", methods=['GET', 'POST'])
65
- @design.route("/experiment/build/<instrument>/", methods=['GET', 'POST'])
77
+ @design.route("/design/script/", methods=['GET', 'POST'])
78
+ @design.route("/design/script/<instrument>/", methods=['GET', 'POST'])
66
79
  @login_required
67
80
  def experiment_builder(instrument=None):
68
81
  """
@@ -73,7 +86,7 @@ def experiment_builder(instrument=None):
73
86
  This route allows users to build and edit experiment workflows. Users can interact with available instruments,
74
87
  define variables, and manage experiment scripts.
75
88
 
76
- .. http:get:: /experiment/build
89
+ .. http:get:: /design/script
77
90
 
78
91
  Load the experiment builder interface.
79
92
 
@@ -81,7 +94,7 @@ def experiment_builder(instrument=None):
81
94
  :type instrument: str
82
95
  :status 200: Experiment builder loaded successfully.
83
96
 
84
- .. http:post:: /experiment/build
97
+ .. http:post:: /design/script
85
98
 
86
99
  Submit form data to add or modify actions in the experiment script.
87
100
 
@@ -235,17 +248,17 @@ def experiment_builder(instrument=None):
235
248
  use_llm=enable_llm)
236
249
 
237
250
 
238
- @design.route("/generate_code", methods=['POST'])
251
+ @design.route("/design/generate_code", methods=['POST'])
239
252
  @login_required
240
253
  def generate_code():
241
254
  """
242
255
  .. :quickref: Text to Code; Generate code from user input and update the design canvas.
243
256
 
244
- .. http:post:: /generate_code
257
+ .. http:post:: /design/generate_code
245
258
 
246
259
  :form prompt: user's prompt
247
260
  :status 200: and then redirects to :http:get:`/experiment/build`
248
- :status 400: failed to initialize the AI agent redirects to :http:get:`/experiment/build`
261
+ :status 400: failed to initialize the AI agent redirects to :http:get:`/design/script`
249
262
 
250
263
  """
251
264
  agent = global_config.agent
@@ -283,17 +296,17 @@ def generate_code():
283
296
  return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True))
284
297
 
285
298
 
286
- @design.route("/experiment", methods=['GET', 'POST'])
299
+ @design.route("/design/campaign", methods=['GET', 'POST'])
287
300
  @login_required
288
301
  def experiment_run():
289
302
  """
290
303
  .. :quickref: Workflow Execution; Execute/iterate the workflow
291
304
 
292
- .. http:get:: /experiment
305
+ .. http:get:: /design/campaign
293
306
 
294
307
  Compile the workflow and load the experiment execution interface.
295
308
 
296
- .. http:post:: /experiment
309
+ .. http:post:: /design/campaign
297
310
 
298
311
  Start workflow execution
299
312
 
@@ -312,11 +325,18 @@ def experiment_run():
312
325
  config_preview = []
313
326
  config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
314
327
  try:
315
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
316
- except ValueError as e:
328
+ # todo
329
+ exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
330
+ # exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
331
+ # print(exec_string)
332
+ except Exception as e:
317
333
  flash(e.__str__())
318
- return redirect(url_for("design.experiment_builder"))
319
- # print(exec_string)
334
+ # handle api request
335
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
336
+ return jsonify({"error": e.__str__()})
337
+ else:
338
+ return redirect(url_for("design.experiment_builder"))
339
+
320
340
  config_file = request.args.get("filename")
321
341
  config = []
322
342
  if config_file:
@@ -331,6 +351,7 @@ def experiment_run():
331
351
  for key, func_str in exec_string.items():
332
352
  exec(func_str)
333
353
  line_collection = script.convert_to_lines(exec_string)
354
+
334
355
  except Exception:
335
356
  flash(f"Please check {key} syntax!!")
336
357
  return redirect(url_for("design.experiment_builder"))
@@ -356,25 +377,44 @@ def experiment_run():
356
377
  flash(f"This script is not compatible with current deck, import {script.deck}")
357
378
  if request.method == "POST":
358
379
  bo_args = None
359
- if "bo" in request.form:
360
- bo_args = request.form.to_dict()
361
- # ax_client = utils.ax_initiation(bo_args)
362
- if "online-config" in request.form:
363
- config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
364
- repeat = request.form.get('repeat', None)
380
+ compiled = False
381
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
382
+ payload_json = request.get_json()
383
+ compiled = True
384
+ if "kwargs" in payload_json:
385
+ config = payload_json["kwargs"]
386
+ elif "parameters" in payload_json:
387
+ bo_args = payload_json
388
+ repeat = payload_json.pop("repeat", None)
389
+ else:
390
+ if "bo" in request.form:
391
+ bo_args = request.form.to_dict()
392
+ if "online-config" in request.form:
393
+ config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
394
+ repeat = request.form.get('repeat', None)
365
395
 
366
396
  try:
367
397
  datapath = current_app.config["DATA_FOLDER"]
368
398
  run_name = script.validate_function_name(run_name)
369
399
  runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
370
400
  logger=g.logger, socketio=g.socketio, repeat_count=repeat,
371
- output_path=datapath, current_app=current_app._get_current_object()
401
+ output_path=datapath, compiled=compiled,
402
+ current_app=current_app._get_current_object()
372
403
  )
373
404
  if utils.check_config_duplicate(config):
374
405
  flash(f"WARNING: Duplicate in config entries.")
375
406
  except Exception as e:
376
- flash(e)
377
- return render_template('experiment_run.html', script=script.script_dict, filename=filename,
407
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
408
+ return jsonify({"error": e.__str__()})
409
+ else:
410
+ flash(e)
411
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
412
+ # wait to get a workflow ID
413
+ while not global_config.runner_status:
414
+ time.sleep(1)
415
+ return jsonify({"status": "task started", "task_id": global_config.runner_status.get("id")})
416
+ else:
417
+ return render_template('experiment_run.html', script=script.script_dict, filename=filename,
378
418
  dot_py=exec_string, line_collection=line_collection,
379
419
  return_list=return_list, config_list=config_list, config_file_list=config_file_list,
380
420
  config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
@@ -382,15 +422,15 @@ def experiment_run():
382
422
  history=deck_list, pause_status=runner.pause_status())
383
423
 
384
424
 
385
- @design.route("/toggle_script_type/<stype>")
425
+ @design.route("/design/script/toggle/<stype>")
386
426
  @login_required
387
427
  def toggle_script_type(stype=None):
388
428
  """
389
429
  .. :quickref: Workflow Design; toggle the experimental phase for design canvas.
390
430
 
391
- .. http:get:: /toggle_script_type
431
+ .. http:get:: /design/script/toggle/<stype>
392
432
 
393
- :status 200: and then redirects to :http:get:`/experiment/build`
433
+ :status 200: and then redirects to :http:get:`/design/script`
394
434
 
395
435
  """
396
436
  script = utils.get_script_file()
@@ -410,16 +450,16 @@ def update_list():
410
450
 
411
451
 
412
452
  # --------------------handle all the import/export and download/upload--------------------------
413
- @design.route("/clear")
453
+ @design.route("/design/clear")
414
454
  @login_required
415
455
  def clear():
416
456
  """
417
457
  .. :quickref: Workflow Design; clear the design canvas.
418
458
 
419
- .. http:get:: /clear
459
+ .. http:get:: /design/clear
420
460
 
421
461
  :form prompt: user's prompt
422
- :status 200: clear canvas and then redirects to :http:get:`/experiment/build`
462
+ :status 200: clear canvas and then redirects to :http:get:`/design/script`
423
463
  """
424
464
  deck = global_config.deck
425
465
  pseudo_name = session.get("pseudo_deck", "")
@@ -435,16 +475,16 @@ def clear():
435
475
  return redirect(url_for("design.experiment_builder"))
436
476
 
437
477
 
438
- @design.route("/import_pseudo", methods=['POST'])
478
+ @design.route("/design/import/pseudo", methods=['POST'])
439
479
  @login_required
440
480
  def import_pseudo():
441
481
  """
442
482
  .. :quickref: Workflow Design; Import pseudo deck from deck history
443
483
 
444
- .. http:post:: /import_pseudo
484
+ .. http:post:: /design/import/pseudo
445
485
 
446
486
  :form pkl_name: pseudo deck name
447
- :status 302: load pseudo deck and then redirects to :http:get:`/experiment/build`
487
+ :status 302: load pseudo deck and then redirects to :http:get:`/design/script`
448
488
  """
449
489
  pkl_name = request.form.get('pkl_name')
450
490
  script = utils.get_script_file()
@@ -458,16 +498,16 @@ def import_pseudo():
458
498
  return redirect(url_for("design.experiment_builder"))
459
499
 
460
500
 
461
- @design.route('/uploads', methods=['POST'])
501
+ @design.route('/design/uploads', methods=['POST'])
462
502
  @login_required
463
503
  def upload():
464
504
  """
465
505
  .. :quickref: Workflow Execution; upload a workflow config file (.CSV)
466
506
 
467
- .. http:post:: /uploads
507
+ .. http:post:: /design/uploads
468
508
 
469
509
  :form file: workflow CSV config file
470
- :status 302: save csv file and then redirects to :http:get:`/experiment`
510
+ :status 302: save csv file and then redirects to :http:get:`/design/campaign`
471
511
  """
472
512
  if request.method == "POST":
473
513
  f = request.files['file']
@@ -483,14 +523,20 @@ def upload():
483
523
  return redirect(url_for("design.experiment_run"))
484
524
 
485
525
 
486
- @design.route('/download_results/<filename>')
526
+ @design.route('/design/workflow/download/<filename>')
487
527
  @login_required
488
528
  def download_results(filename):
529
+ """
530
+ .. :quickref: Workflow Design; download a workflow data file
531
+
532
+ .. http:get:: /design/workflow/download/<filename>
533
+
534
+ """
489
535
  filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
490
536
  return send_file(os.path.abspath(filepath), as_attachment=True)
491
537
 
492
538
 
493
- @design.route('/load_json', methods=['POST'])
539
+ @design.route('/design/load_json', methods=['POST'])
494
540
  @login_required
495
541
  def load_json():
496
542
  """
@@ -499,7 +545,7 @@ def load_json():
499
545
  .. http:post:: /load_json
500
546
 
501
547
  :form file: workflow design JSON file
502
- :status 302: load pseudo deck and then redirects to :http:get:`/experiment/build`
548
+ :status 302: load pseudo deck and then redirects to :http:get:`/design/script`
503
549
  """
504
550
  if request.method == "POST":
505
551
  f = request.files['file']
@@ -513,9 +559,15 @@ def load_json():
513
559
  return redirect(url_for("design.experiment_builder"))
514
560
 
515
561
 
516
- @design.route('/download/<filetype>')
562
+ @design.route('/design/script/download/<filetype>')
517
563
  @login_required
518
564
  def download(filetype):
565
+ """
566
+ .. :quickref: Workflow Design Ext; download a workflow design file
567
+
568
+ .. http:get:: /design/script/download/<filetype>
569
+
570
+ """
519
571
  script = utils.get_script_file()
520
572
  run_name = script.name if script.name else "untitled"
521
573
  if filetype == "configure":
@@ -533,40 +585,28 @@ def download(filetype):
533
585
  outfile.write(json_object)
534
586
  elif filetype == "python":
535
587
  filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
536
- elif filetype == "proxy":
537
- snapshot = global_config.deck_snapshot.copy()
538
- class_definitions = {}
539
- # Iterate through each instrument in the snapshot
540
- for instrument_key, instrument_data in snapshot.items():
541
- # Iterate through each function associated with the current instrument
542
- for function_key, function_data in instrument_data.items():
543
- # Convert the function signature to a string representation
544
- function_data['signature'] = str(function_data['signature'])
545
- class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
546
- class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
547
- # Export the generated class definitions to a .py script
548
- export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
549
- filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
588
+ else:
589
+ return "Unsupported file type", 400
550
590
  return send_file(os.path.abspath(filepath), as_attachment=True)
551
591
 
552
592
 
553
- @design.route("/edit/<uuid>", methods=['GET', 'POST'])
593
+ @design.route("/design/step/edit/<uuid>", methods=['GET', 'POST'])
554
594
  @login_required
555
595
  def edit_action(uuid: str):
556
596
  """
557
597
  .. :quickref: Workflow Design; edit parameters of an action step on canvas
558
598
 
559
- .. http:get:: /edit
599
+ .. http:get:: /design/step/edit/<uuid>
560
600
 
561
601
  Load parameter form of an action step
562
602
 
563
- .. http:post:: /edit
603
+ .. http:post:: /design/step/edit/<uuid>
564
604
 
565
605
  :param uuid: The step's uuid
566
606
  :type uuid: str
567
607
 
568
608
  :form dynamic form: workflow step dynamic inputs
569
- :status 302: save changes and then redirects to :http:get:`/experiment/build`
609
+ :status 302: save changes and then redirects to :http:get:`/design/script`
570
610
  """
571
611
  script = utils.get_script_file()
572
612
  action = script.find_by_uuid(uuid)
@@ -589,18 +629,18 @@ def edit_action(uuid: str):
589
629
  return redirect(url_for('design.experiment_builder'))
590
630
 
591
631
 
592
- @design.route("/delete/<id>")
632
+ @design.route("/design/step/delete/<id>")
593
633
  @login_required
594
634
  def delete_action(id: int):
595
635
  """
596
636
  .. :quickref: Workflow Design; delete an action step on canvas
597
637
 
598
- .. http:get:: /delete
638
+ .. http:get:: /design/step/delete/<id>
599
639
 
600
640
  :param id: The step number id
601
641
  :type id: int
602
642
 
603
- :status 302: save changes and then redirects to :http:get:`/experiment/build`
643
+ :status 302: save changes and then redirects to :http:get:`/design/script`
604
644
  """
605
645
  back = request.referrer
606
646
  script = utils.get_script_file()
@@ -609,18 +649,18 @@ def delete_action(id: int):
609
649
  return redirect(back)
610
650
 
611
651
 
612
- @design.route("/duplicate/<id>")
652
+ @design.route("/design/step/duplicate/<id>")
613
653
  @login_required
614
654
  def duplicate_action(id: int):
615
655
  """
616
656
  .. :quickref: Workflow Design; duplicate an action step on canvas
617
657
 
618
- .. http:get:: /duplicate
658
+ .. http:get:: /design/step/duplicate/<id>
619
659
 
620
660
  :param id: The step number id
621
661
  :type id: int
622
662
 
623
- :status 302: save changes and then redirects to :http:get:`/experiment/build`
663
+ :status 302: save changes and then redirects to :http:get:`/design/script`
624
664
  """
625
665
  back = request.referrer
626
666
  script = utils.get_script_file()
@@ -629,6 +669,103 @@ def duplicate_action(id: int):
629
669
  return redirect(back)
630
670
 
631
671
 
632
- @design.route("/backend/status", methods=["GET"])
672
+ # ---- HTTP API Endpoints ----
673
+
674
+ @design.route("/api/runner/status", methods=["GET"])
633
675
  def runner_status():
634
- return jsonify(runner.get_status())
676
+ """
677
+ .. :quickref: Workflow Design; get the execution status
678
+
679
+ .. http:get:: /api/runner/status
680
+
681
+ :status 200: status
682
+ """
683
+ runner_busy = global_config.runner_lock.locked()
684
+ status = {"busy": runner_busy}
685
+ task_status = global_config.runner_status
686
+ current_step = {}
687
+ # print(task_status)
688
+ if task_status is not None:
689
+ task_type = task_status["type"]
690
+ task_id = task_status["id"]
691
+ if task_type == "task":
692
+ step = SingleStep.query.get(task_id)
693
+ current_step = step.as_dict()
694
+ if task_type == "workflow":
695
+ workflow = WorkflowRun.query.get(task_id)
696
+ if workflow is not None:
697
+ latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(WorkflowStep.start_time.desc()).first()
698
+ if latest_step is not None:
699
+ current_step = latest_step.as_dict()
700
+ status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
701
+ status["current_task"] = current_step
702
+ return jsonify(status), 200
703
+
704
+
705
+
706
+ @design.route("/api/runner/abort_pending", methods=["POST"])
707
+ def api_abort_pending():
708
+ """
709
+ .. :quickref: Workflow Design; abort pending action(s) during execution
710
+
711
+ .. http:get:: /api/runner/abort_pending
712
+
713
+ :status 200: {"status": "ok"}
714
+ """
715
+ abort_pending()
716
+ return jsonify({"status": "ok"}), 200
717
+
718
+ @design.route("/api/runner/abort_current", methods=["POST"])
719
+ def api_abort_current():
720
+ """
721
+ .. :quickref: Workflow Design; abort right after current action during execution
722
+
723
+ .. http:get:: /api/runner/abort_current
724
+
725
+ :status 200: {"status": "ok"}
726
+ """
727
+ abort_current()
728
+ return jsonify({"status": "ok"}), 200
729
+
730
+ @design.route("/api/runner/pause", methods=["POST"])
731
+ def api_pause():
732
+ """
733
+ .. :quickref: Workflow Design; pause during execution
734
+
735
+ .. http:get:: /api/runner/pause
736
+
737
+ :status 200: {"status": "ok"}
738
+ """
739
+ msg = pause()
740
+ return jsonify({"status": "ok", "pause_status": msg}), 200
741
+
742
+ @design.route("/api/runner/retry", methods=["POST"])
743
+ def api_retry():
744
+ """
745
+ .. :quickref: Workflow Design; retry when error occur during execution
746
+
747
+ .. http:get:: /api/runner/retry
748
+
749
+ :status 200: {"status": "ok"}
750
+ """
751
+ retry()
752
+ return jsonify({"status": "ok, retrying failed step"}), 200
753
+
754
+
755
+ @design.route("/api/design/submit", methods=["POST"])
756
+ def submit_script():
757
+ """
758
+ .. :quickref: Workflow Design; submit script
759
+
760
+ .. http:get:: /api/design/submit
761
+
762
+ :status 200: {"status": "ok"}
763
+ """
764
+ deck = global_config.deck
765
+ deck_name = os.path.splitext(os.path.basename(deck.__file__))[0] if deck.__name__ == "__main__" else deck.__name__
766
+ script = Script(author=session.get('user'), deck=deck_name)
767
+ script_collection = request.get_json()
768
+ script.python_script = script_collection
769
+ # todo check script format
770
+ utils.post_script_file(script)
771
+ return jsonify({"status": "ok"}), 200