ivoryos 1.2.4__tar.gz → 1.2.6__tar.gz

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.

Files changed (108) hide show
  1. {ivoryos-1.2.4 → ivoryos-1.2.6}/PKG-INFO +1 -1
  2. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/api/api.py +1 -1
  3. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/control/control.py +46 -31
  4. ivoryos-1.2.6/ivoryos/routes/control/control_file.py +33 -0
  5. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/action_handlers.js +1 -1
  6. ivoryos-1.2.6/ivoryos/utils/client_proxy.py +288 -0
  7. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/db_models.py +1 -1
  8. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/form.py +5 -0
  9. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/task_runner.py +3 -3
  10. ivoryos-1.2.6/ivoryos/version.py +1 -0
  11. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos.egg-info/PKG-INFO +1 -1
  12. ivoryos-1.2.4/ivoryos/routes/control/control_file.py +0 -36
  13. ivoryos-1.2.4/ivoryos/utils/client_proxy.py +0 -57
  14. ivoryos-1.2.4/ivoryos/version.py +0 -1
  15. {ivoryos-1.2.4 → ivoryos-1.2.6}/LICENSE +0 -0
  16. {ivoryos-1.2.4 → ivoryos-1.2.6}/README.md +0 -0
  17. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/__init__.py +0 -0
  18. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/config.py +0 -0
  19. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/optimizer/ax_optimizer.py +0 -0
  20. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/optimizer/base_optimizer.py +0 -0
  21. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/optimizer/baybe_optimizer.py +0 -0
  22. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/optimizer/registry.py +0 -0
  23. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/__init__.py +0 -0
  24. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/auth/__init__.py +0 -0
  25. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/auth/auth.py +0 -0
  26. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/auth/templates/login.html +0 -0
  27. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/auth/templates/signup.html +0 -0
  28. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/control/__init__.py +0 -0
  29. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/control/control_new_device.py +0 -0
  30. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/control/templates/controllers.html +0 -0
  31. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  32. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/control/utils.py +0 -0
  33. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/data/__init__.py +0 -0
  34. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/data/data.py +0 -0
  35. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/data/templates/components/step_card.html +0 -0
  36. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  37. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/data/templates/workflow_view.html +0 -0
  38. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/__init__.py +0 -0
  39. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/design.py +0 -0
  40. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/design_file.py +0 -0
  41. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/design_step.py +0 -0
  42. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  43. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  44. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  45. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  46. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  47. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  48. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/canvas_main.html +0 -0
  49. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  50. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/edit_action_form.html +0 -0
  51. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  52. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  53. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  54. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  55. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  56. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  57. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/modals.html +0 -0
  58. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
  59. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  60. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  61. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
  62. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/__init__.py +0 -0
  63. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/execute.py +0 -0
  64. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/execute_file.py +0 -0
  65. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  66. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  67. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  68. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  69. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  70. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
  71. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
  72. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  73. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  74. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/library/__init__.py +0 -0
  75. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/library/library.py +0 -0
  76. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/library/templates/library.html +0 -0
  77. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/main/__init__.py +0 -0
  78. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/main/main.py +0 -0
  79. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/main/templates/help.html +0 -0
  80. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/routes/main/templates/home.html +0 -0
  81. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/socket_handlers.py +0 -0
  82. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/favicon.ico +0 -0
  83. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  84. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  85. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/db_delete.js +0 -0
  86. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/overlay.js +0 -0
  87. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/script_metadata.js +0 -0
  88. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/socket_handler.js +0 -0
  89. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/sortable_card.js +0 -0
  90. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/sortable_design.js +0 -0
  91. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/js/ui_state.js +0 -0
  92. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/logo.webp +0 -0
  93. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/static/style.css +0 -0
  94. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/templates/base.html +0 -0
  95. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/__init__.py +0 -0
  96. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/bo_campaign.py +0 -0
  97. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/global_config.py +0 -0
  98. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/llm_agent.py +0 -0
  99. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/py_to_json.py +0 -0
  100. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/script_runner.py +0 -0
  101. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/serilize.py +0 -0
  102. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos/utils/utils.py +0 -0
  103. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos.egg-info/SOURCES.txt +0 -0
  104. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos.egg-info/dependency_links.txt +0 -0
  105. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos.egg-info/requires.txt +0 -0
  106. {ivoryos-1.2.4 → ivoryos-1.2.6}/ivoryos.egg-info/top_level.txt +0 -0
  107. {ivoryos-1.2.4 → ivoryos-1.2.6}/pyproject.toml +0 -0
  108. {ivoryos-1.2.4 → ivoryos-1.2.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -37,7 +37,7 @@ def backend_control(instrument: str=None):
37
37
  forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
38
38
 
39
39
  if request.method == 'POST':
40
- method_name = request.form.get("hidden_name", None)
40
+ method_name = request.json.get("hidden_name", None)
41
41
  form = forms.get(method_name, None)
42
42
  if form:
43
43
  kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
@@ -21,7 +21,7 @@ control.register_blueprint(control_temp)
21
21
  @control.route("/", strict_slashes=False, methods=["GET", "POST"])
22
22
  @control.route("/<string:instrument>", strict_slashes=False, methods=["GET", "POST"])
23
23
  @login_required
24
- def deck_controllers():
24
+ def deck_controllers(instrument: str = None):
25
25
  """
26
26
  .. :quickref: Direct Control; device (instruments) and methods
27
27
 
@@ -44,41 +44,56 @@ def deck_controllers():
44
44
  :status 200: render template with instruments and methods
45
45
 
46
46
  """
47
- deck_variables = global_config.deck_snapshot.keys()
48
- temp_variables = global_config.defined_variables.keys()
49
- instrument = request.args.get('instrument')
47
+ instrument = instrument or request.args.get("instrument")
50
48
  forms = None
51
49
  if instrument:
52
50
  inst_object = find_instrument_by_name(instrument)
53
- _forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
54
- order = get_session_by_instrument('card_order', instrument)
55
- hidden_functions = get_session_by_instrument('hidden_functions', instrument)
56
- functions = list(_forms.keys())
57
- for function in functions:
58
- if function not in hidden_functions and function not in order:
59
- order.append(function)
60
- post_session_by_instrument('card_order', instrument, order)
61
- forms = {name: _forms[name] for name in order if name in _forms}
62
- # Handle POST for method execution
63
- if request.method == 'POST':
64
- all_kwargs = request.form.copy()
65
- method_name = all_kwargs.pop("hidden_name", None)
66
- form = forms.get(method_name)
67
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'} if form else {}
68
- if form and form.validate_on_submit():
69
- kwargs.pop("hidden_name", None)
70
- output = runner.run_single_step(instrument, method_name, kwargs, wait=True, current_app=current_app._get_current_object())
71
- if output["success"]:
72
- flash(f"\nRun Success! Output value: {output.get('output', 'None')}.")
73
- else:
74
- flash(f"\nRun Error! {output.get('output', 'Unknown error occurred.')}", "error")
51
+ forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
52
+
53
+ if request.method == "POST":
54
+ if not forms:
55
+ return jsonify({"success": False, "error": "Instrument not found"}), 404
56
+
57
+ payload = request.get_json() if request.is_json else request.form.to_dict()
58
+ method_name = payload.pop("hidden_name", None)
59
+ form = forms.get(method_name)
60
+
61
+ if not form:
62
+ return jsonify({"success": False, "error": f"Method {method_name} not found"}), 404
63
+
64
+ # Extract kwargs
65
+ if request.is_json:
66
+ kwargs = {k: v for k, v in payload.items() if k not in ["csrf_token", "hidden_wait"]}
67
+ else:
68
+ kwargs = {field.name: field.data for field in form if field.name not in ["csrf_token", "hidden_name"]}
69
+
70
+ wait = str(payload.get("hidden_wait", "true")).lower() == "true"
71
+
72
+ output = runner.run_single_step(
73
+ component=instrument, method=method_name, kwargs=kwargs, wait=wait,
74
+ current_app=current_app._get_current_object()
75
+ )
76
+
77
+ if request.is_json:
78
+ return jsonify(output)
79
+ else:
80
+ if output.get("success"):
81
+ flash(f"Run Success! Output: {output.get('output', 'None')}")
75
82
  else:
76
- if form:
77
- flash(form.errors)
78
- else:
79
- flash("Invalid method selected.")
83
+ flash(f"Run Error! {output.get('output', 'Unknown error occurred.')}", "error")
84
+
85
+ # GET request → render web form or return snapshot for API
86
+ if request.is_json or request.accept_mimetypes["application/json"]:
87
+ snapshot = global_config.deck_snapshot.copy()
88
+ for instrument_key, instrument_data in snapshot.items():
89
+ for function_key, function_data in instrument_data.items():
90
+ function_data["signature"] = str(function_data["signature"])
91
+ return jsonify(snapshot)
92
+
93
+ deck_variables = global_config.deck_snapshot.keys()
94
+ temp_variables = global_config.defined_variables.keys()
80
95
  return render_template(
81
- 'controllers.html',
96
+ "controllers.html",
82
97
  defined_variables=deck_variables,
83
98
  temp_variables=temp_variables,
84
99
  instrument=instrument,
@@ -0,0 +1,33 @@
1
+ import os
2
+ from flask import Blueprint, request,current_app, send_file
3
+ from flask_login import login_required
4
+
5
+ from ivoryos.utils.client_proxy import ProxyGenerator
6
+ from ivoryos.utils.global_config import GlobalConfig
7
+
8
+ global_config = GlobalConfig()
9
+
10
+ control_file = Blueprint('file', __name__)
11
+
12
+
13
+
14
+ @control_file.route("/files/proxy", strict_slashes=False)
15
+ @login_required
16
+ def download_proxy():
17
+ """
18
+ .. :quickref: Direct Control Files; Download proxy Python interface
19
+
20
+ download proxy Python interface
21
+
22
+ .. http:get:: /files/proxy
23
+ """
24
+ generator = ProxyGenerator(request.url_root)
25
+ snapshot = global_config.deck_snapshot.copy()
26
+
27
+ filepath = generator.generate_from_flask_route(
28
+ snapshot,
29
+ request.url_root,
30
+ current_app.config["OUTPUT_FOLDER"]
31
+ )
32
+
33
+ return send_file(os.path.abspath(filepath), as_attachment=True)
@@ -15,7 +15,7 @@ function saveWorkflow(link) {
15
15
  .then(data => {
16
16
  if (data.success) {
17
17
  // flash a success message
18
- flash("Workflow saved successfully", "success");
18
+ // flash("Workflow saved successfully", "success");
19
19
  window.location.reload(); // or update the UI dynamically
20
20
  } else {
21
21
  alert("Failed to save workflow: " + data.error);
@@ -0,0 +1,288 @@
1
+ import os
2
+ import re
3
+ from typing import Dict, Set, Any, Optional
4
+
5
+
6
+ class ProxyGenerator:
7
+ """
8
+ A class to generate Python proxy interfaces for API clients.
9
+
10
+ This generator creates client classes that wrap API endpoints,
11
+ automatically handling request/response cycles and error handling.
12
+ """
13
+
14
+ # Common typing symbols to scan for in function signatures
15
+ TYPING_SYMBOLS = {
16
+ "Optional", "Union", "List", "Dict", "Tuple",
17
+ "Any", "Callable", "Iterable", "Sequence", "Set"
18
+ }
19
+
20
+ def __init__(self, base_url: str, api_path_template: str = "ivoryos/instruments/deck.{class_name}"):
21
+ """
22
+ Initialize the ProxyGenerator.
23
+
24
+ Args:
25
+ base_url: The base URL for the API
26
+ api_path_template: Template for API paths, with {class_name} placeholder
27
+ """
28
+ self.base_url = base_url.rstrip('/')
29
+ self.api_path_template = api_path_template
30
+ self.used_typing_symbols: Set[str] = set()
31
+
32
+ def extract_typing_from_signatures(self, functions: Dict[str, Dict[str, Any]]) -> Set[str]:
33
+ """
34
+ Scan function signatures for typing symbols and track usage.
35
+
36
+ Args:
37
+ functions: Dictionary of function definitions with signatures
38
+
39
+ Returns:
40
+ Set of typing symbols found in the signatures
41
+ """
42
+ for function_data in functions.values():
43
+ signature = function_data.get("signature", "")
44
+ for symbol in self.TYPING_SYMBOLS:
45
+ if re.search(rf"\b{symbol}\b", signature):
46
+ self.used_typing_symbols.add(symbol)
47
+ return self.used_typing_symbols
48
+
49
+ def create_class_definition(self, class_name: str, functions: Dict[str, Dict[str, Any]]) -> str:
50
+ """
51
+ Generate a class definition string for one API client class.
52
+
53
+ Args:
54
+ class_name: Name of the class to generate
55
+ functions: Dictionary of function definitions
56
+
57
+ Returns:
58
+ String containing the complete class definition
59
+ """
60
+ capitalized_name = class_name.capitalize()
61
+ api_url = f"{self.base_url}/{self.api_path_template.format(class_name=class_name)}"
62
+
63
+ class_template = f"class {capitalized_name}:\n"
64
+ class_template += f' """Auto-generated API client for {class_name} operations."""\n'
65
+ class_template += f' url = "{api_url}"\n\n'
66
+
67
+ # Add the __init__ with auth
68
+ class_template += self._generate_init()
69
+
70
+ # Add the _auth
71
+ class_template += self._generate_auth()
72
+
73
+ # Add the base _call method
74
+ class_template += self._generate_call_method()
75
+
76
+ # Add individual methods for each function
77
+ for function_name, details in functions.items():
78
+ method_def = self._generate_method(function_name, details)
79
+ class_template += method_def + "\n"
80
+
81
+ return class_template
82
+
83
+ def _generate_call_method(self) -> str:
84
+ """Generate the base _call method for API communication."""
85
+ return ''' def _call(self, payload):
86
+ """Make API call with error handling."""
87
+ res = session.post(self.url, json=payload, allow_redirects=False)
88
+ # Handle 302 redirect (likely auth issue)
89
+ if res.status_code == 302:
90
+ try:
91
+ self._auth()
92
+ res = session.post(self.url, json=payload, allow_redirects=False)
93
+ except Exception as e:
94
+ raise AuthenticationError(
95
+ "Authentication failed during re-attempt. "
96
+ "Please check your credentials or connection."
97
+ ) from e
98
+ res.raise_for_status()
99
+ data = res.json()
100
+ if not data.get('success'):
101
+ raise Exception(data.get('output', "Unknown API error."))
102
+ return data.get('output')
103
+
104
+ '''
105
+
106
+ def _generate_method(self, function_name: str, details: Dict[str, Any]) -> str:
107
+ """
108
+ Generate a single method definition.
109
+
110
+ Args:
111
+ function_name: Name of the method
112
+ details: Function details including signature and docstring
113
+
114
+ Returns:
115
+ String containing the method definition
116
+ """
117
+ signature = details.get("signature", "(self)")
118
+ docstring = details.get("docstring", "")
119
+
120
+ # Build method header
121
+ method = f" def {function_name}{signature}:\n"
122
+
123
+ if docstring:
124
+ method += f' """{docstring}"""\n'
125
+
126
+ # Build payload
127
+ method += f' payload = {{"hidden_name": "{function_name}"}}\n'
128
+
129
+ # Extract parameters from signature (excluding 'self')
130
+ params = self._extract_parameters(signature)
131
+
132
+ for param_name in params:
133
+ method += f' payload["{param_name}"] = {param_name}\n'
134
+
135
+ method += " return self._call(payload)\n"
136
+
137
+ return method
138
+
139
+ def _extract_parameters(self, signature: str) -> list:
140
+ """
141
+ Extract parameter names from a function signature.
142
+
143
+ Args:
144
+ signature: Function signature string like "(self, param1, param2: int = 5)"
145
+
146
+ Returns:
147
+ List of parameter names (excluding 'self')
148
+ """
149
+ # Remove parentheses and split by comma
150
+ param_str = signature.strip("()").strip()
151
+ if not param_str or param_str == "self":
152
+ return []
153
+
154
+ params = [param.strip() for param in param_str.split(",")]
155
+ result = []
156
+
157
+ for param in params:
158
+ if param and param != "self":
159
+ # Extract parameter name (before : or = if present)
160
+ param_name = param.split(":")[0].split("=")[0].strip()
161
+ if param_name:
162
+ result.append(param_name)
163
+
164
+ return result
165
+
166
+ def generate_proxy_file(self,
167
+ snapshot: Dict[str, Dict[str, Any]],
168
+ output_path: str,
169
+ filename: str = "generated_proxy.py") -> str:
170
+ """
171
+ Generate the complete proxy file from a snapshot of instruments.
172
+
173
+ Args:
174
+ snapshot: Dictionary containing instrument data with functions
175
+ output_path: Directory to write the output file
176
+ filename: Name of the output file
177
+
178
+ Returns:
179
+ Path to the generated file
180
+ """
181
+ class_definitions = {}
182
+ self.used_typing_symbols.clear()
183
+
184
+ # Process each instrument in the snapshot
185
+ for instrument_key, instrument_data in snapshot.items():
186
+ # Convert function signatures to strings if needed
187
+ for function_key, function_data in instrument_data.items():
188
+ if 'signature' in function_data:
189
+ function_data['signature'] = str(function_data['signature'])
190
+
191
+ # Extract class name from instrument path
192
+ class_name = instrument_key.split('.')[-1]
193
+
194
+ # Generate class definition
195
+ class_definitions[class_name] = self.create_class_definition(
196
+ class_name, instrument_data
197
+ )
198
+
199
+ # Track typing symbols used
200
+ self.extract_typing_from_signatures(instrument_data)
201
+
202
+ # Write the complete file
203
+ filepath = self._write_proxy_file(class_definitions, output_path, filename)
204
+ return filepath
205
+
206
+ def _write_proxy_file(self,
207
+ class_definitions: Dict[str, str],
208
+ output_path: str,
209
+ filename: str) -> str:
210
+ """
211
+ Write the generated classes to a Python file.
212
+
213
+ Args:
214
+ class_definitions: Dictionary of class names to class definition strings
215
+ output_path: Directory to write the file
216
+ filename: Name of the file
217
+
218
+ Returns:
219
+ Full path to the written file
220
+ """
221
+ filepath = os.path.join(output_path, filename)
222
+
223
+ with open(filepath, "w") as f:
224
+ # Write imports
225
+ f.write("import requests\n")
226
+ if self.used_typing_symbols:
227
+ f.write(f"from typing import {', '.join(sorted(self.used_typing_symbols))}\n")
228
+ f.write("\n")
229
+
230
+ # Write session setup
231
+ f.write("session = requests.Session()\n\n")
232
+
233
+ # Write class definitions
234
+ for class_name, class_def in class_definitions.items():
235
+ f.write(class_def)
236
+ f.write("\n")
237
+
238
+ # Create default instances
239
+ f.write("# Default instances for convenience\n")
240
+ for class_name in class_definitions.keys():
241
+ instance_name = class_name.lower()
242
+ f.write(f"{instance_name} = {class_name.capitalize()}()\n")
243
+
244
+ return filepath
245
+
246
+ def generate_from_flask_route(self,
247
+ snapshot: Dict[str, Dict[str, Any]],
248
+ request_url_root: str,
249
+ output_folder: str) -> str:
250
+ """
251
+ Convenience method that matches the original Flask route behavior.
252
+
253
+ Args:
254
+ snapshot: The deck snapshot from global_config
255
+ request_url_root: The URL root from Flask request
256
+ output_folder: Output folder path from app config
257
+
258
+ Returns:
259
+ Path to the generated file
260
+ """
261
+ # Set the base URL from the request
262
+ self.base_url = request_url_root.rstrip('/')
263
+
264
+ # Generate the proxy file
265
+ return self.generate_proxy_file(snapshot, output_folder)
266
+
267
+ def _generate_init(self):
268
+ return ''' def __init__(self, username=None, password=None):
269
+ """Initialize the client with authentication."""
270
+ self.username = username
271
+ self.password = password
272
+ self._auth()
273
+
274
+ '''
275
+
276
+
277
+ def _generate_auth(self):
278
+ return f""" def _auth(self):
279
+ username = self.username or 'admin'
280
+ password = self.password or 'admin'
281
+ res = session.post(
282
+ '{self.base_url}/ivoryos/auth/login',
283
+ data={{"username": username, "password": password}}
284
+ )
285
+ if res.status_code != 200:
286
+ raise Exception("Authentication failed")
287
+
288
+ """
@@ -474,7 +474,7 @@ class Script(db.Model):
474
474
  """
475
475
  configure, config_type = self.config(stype)
476
476
 
477
- configure = [param + f":{param_type}" if not param_type == "any" else "" for param, param_type in
477
+ configure = [param + f":{param_type}" if not param_type == "any" else param for param, param_type in
478
478
  config_type.items()]
479
479
 
480
480
  script_type = f"_{stype}" if stype != "script" else ""
@@ -299,6 +299,11 @@ def create_form_for_method(method, autofill, script=None, design=True):
299
299
  if optional:
300
300
  field_kwargs["filters"] = [lambda x: x if x != '' else None]
301
301
 
302
+ if annotation is bool:
303
+ # Boolean fields should not use InputRequired
304
+ field_kwargs["validators"] = [] # or [Optional()]
305
+ else:
306
+ field_kwargs["validators"] = [InputRequired()] if param.default is param.empty else [Optional()]
302
307
 
303
308
  render_kwargs = {"placeholder": placeholder_text}
304
309
 
@@ -64,7 +64,7 @@ class TaskRunner:
64
64
 
65
65
  # with self.lock:
66
66
  with current_app.app_context():
67
- step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=False, start_time=datetime.now())
67
+ step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=None, start_time=datetime.now())
68
68
  db.session.add(step)
69
69
  db.session.commit()
70
70
  global_config.runner_status = {"id":step.id, "type": "task"}
@@ -74,10 +74,10 @@ class TaskRunner:
74
74
  step.end_time = datetime.now()
75
75
  success = True
76
76
  except Exception as e:
77
- step.run_error = e.__str__()
77
+ step.run_error = str(e)
78
78
  step.end_time = datetime.now()
79
79
  success = False
80
- output = e.__str__()
80
+ output = str(e)
81
81
  finally:
82
82
  db.session.commit()
83
83
  self.lock.release()
@@ -0,0 +1 @@
1
+ __version__ = "1.2.6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
6
  License: MIT
@@ -1,36 +0,0 @@
1
- import os
2
- from flask import Blueprint, request,current_app, send_file
3
- from flask_login import login_required
4
-
5
- from ivoryos.utils.client_proxy import export_to_python, create_function
6
- from ivoryos.utils.global_config import GlobalConfig
7
-
8
- global_config = GlobalConfig()
9
-
10
- control_file = Blueprint('file', __name__)
11
-
12
-
13
- @control_file.route("/files/proxy", strict_slashes=False)
14
- @login_required
15
- def download_proxy():
16
- """
17
- .. :quickref: Direct Control Files; download proxy interface
18
-
19
- download proxy Python interface
20
-
21
- .. http:get:: /files/proxy
22
- """
23
- snapshot = global_config.deck_snapshot.copy()
24
- class_definitions = {}
25
- # Iterate through each instrument in the snapshot
26
- for instrument_key, instrument_data in snapshot.items():
27
- # Iterate through each function associated with the current instrument
28
- for function_key, function_data in instrument_data.items():
29
- # Convert the function signature to a string representation
30
- function_data['signature'] = str(function_data['signature'])
31
- class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
32
- class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
33
- # Export the generated class definitions to a .py script
34
- export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
35
- filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
36
- return send_file(os.path.abspath(filepath), as_attachment=True)
@@ -1,57 +0,0 @@
1
- # import argparse
2
- import os
3
-
4
- # import requests
5
-
6
- # session = requests.Session()
7
-
8
-
9
- # Function to create class and methods dynamically
10
- def create_function(url, class_name, functions):
11
- class_template = f'class {class_name.capitalize()}:\n url = "{url}ivoryos/api/control/deck.{class_name}"\n'
12
-
13
- for function_name, details in functions.items():
14
- signature = details['signature']
15
- docstring = details.get('docstring', '')
16
-
17
- # Creating the function definition
18
- method = f' def {function_name}{signature}:\n'
19
- if docstring:
20
- method += f' """{docstring}"""\n'
21
-
22
- # Generating the session.post code for sending data
23
- method += ' return session.post(self.url, data={'
24
- method += f'"hidden_name": "{function_name}"'
25
-
26
- # Extracting the parameters from the signature string for the data payload
27
- param_str = signature[6:-1] # Remove the "(self" and final ")"
28
- params = [param.strip() for param in param_str.split(',')] if param_str else []
29
-
30
- for param in params:
31
- param_name = param.split(':')[0].strip() # Split on ':' and get parameter name
32
- method += f', "{param_name}": {param_name}'
33
-
34
- method += '}).json()\n'
35
- class_template += method + '\n'
36
-
37
- return class_template
38
-
39
- # Function to export the generated classes to a Python script
40
- def export_to_python(class_definitions, path):
41
- with open(os.path.join(path, "generated_proxy.py"), 'w') as f:
42
- # Writing the imports at the top of the script
43
- f.write('import requests\n\n')
44
- f.write('session = requests.Session()\n\n')
45
-
46
- # Writing each class definition to the file
47
- for class_name, class_def in class_definitions.items():
48
- f.write(class_def)
49
- f.write('\n')
50
-
51
- # Creating instances of the dynamically generated classes
52
- for class_name in class_definitions.keys():
53
- instance_name = class_name.lower() # Using lowercase for instance names
54
- f.write(f'{instance_name} = {class_name.capitalize()}()\n')
55
-
56
-
57
-
@@ -1 +0,0 @@
1
- __version__ = "1.2.4"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes