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
@@ -1,5 +1,5 @@
1
1
  document.addEventListener("DOMContentLoaded", function() {
2
- var socket = io.connect('http://' + document.domain + ':' + location.port);
2
+ var socket = io();
3
3
  socket.on('connect', function() {
4
4
  console.log('Connected');
5
5
  });
@@ -37,13 +37,43 @@ document.addEventListener("DOMContentLoaded", function() {
37
37
  console.error("Error received:", errorData);
38
38
  var progressBar = document.getElementById('progress-bar-inner');
39
39
 
40
- progressBar.classList.remove('bg-success');
41
- progressBar.classList.add('bg-danger'); // Red color for error
42
- // Show error modal
40
+ progressBar.classList.remove('bg-success', 'bg-warning');
41
+ progressBar.classList.add('bg-danger');
42
+
43
43
  var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
44
- document.getElementById('error-message').innerText = "An error occurred: " + errorData.message;
44
+ document.getElementById('errorModalLabel').innerText = "Error Detected";
45
+ document.getElementById('error-message').innerText =
46
+ "An error occurred: " + errorData.message;
47
+
48
+ // Show all buttons again
49
+ document.getElementById('retry-btn').style.display = "inline-block";
50
+ document.getElementById('continue-btn').style.display = "inline-block";
51
+ document.getElementById('stop-btn').style.display = "inline-block";
52
+
45
53
  errorModal.show();
54
+ });
55
+
56
+
57
+ socket.on('human_intervention', function(data) {
58
+ console.warn("Human intervention required:", data);
59
+ var progressBar = document.getElementById('progress-bar-inner');
46
60
 
61
+ // Set progress bar to yellow
62
+ progressBar.classList.remove('bg-success', 'bg-danger');
63
+ progressBar.classList.add('bg-warning');
64
+
65
+ // Reuse error modal but update content
66
+ var errorModal = new bootstrap.Modal(document.getElementById('error-modal'));
67
+ document.getElementById('errorModalLabel').innerText = "Human Intervention Required";
68
+ document.getElementById('error-message').innerText =
69
+ "Workflow paused: " + (data.message || "Please check and manually resume.");
70
+
71
+ // Optionally: hide retry button, since it may not apply
72
+ document.getElementById('retry-btn').style.display = "none";
73
+ document.getElementById('continue-btn').style.display = "inline-block";
74
+ document.getElementById('stop-btn').style.display = "inline-block";
75
+
76
+ errorModal.show();
47
77
  });
48
78
 
49
79
  // Handle Pause/Resume Button
@@ -71,6 +101,11 @@ document.addEventListener("DOMContentLoaded", function() {
71
101
  document.getElementById('continue-btn').addEventListener('click', function() {
72
102
  socket.emit('pause'); // Resume execution
73
103
  console.log("Execution resumed.");
104
+
105
+ // Reset progress bar color to running (blue)
106
+ var progressBar = document.getElementById('progress-bar-inner');
107
+ progressBar.classList.remove('bg-danger', 'bg-warning');
108
+ progressBar.classList.add('bg-primary');
74
109
  });
75
110
 
76
111
  document.getElementById('retry-btn').addEventListener('click', function() {
@@ -106,6 +106,7 @@ function initializeCanvas() {
106
106
  document.activeElement?.blur();
107
107
  triggerModal(formHtml, actionName, actionId, state.dropTargetId);
108
108
  });
109
+ initializeCodeOverlay();
109
110
  }
110
111
 
111
112
  function insertDropPlaceholder($target) {
@@ -115,20 +116,37 @@ function insertDropPlaceholder($target) {
115
116
 
116
117
  // Add this function to sortable_design.js
117
118
  function initializeDragHandlers() {
118
- $(".accordion-item").off("dragstart").on("dragstart", function (event) {
119
- let formHtml = $(this).find(".accordion-body form").prop('outerHTML');
120
-
121
- if (!formHtml) {
122
- console.error("Form not found in accordion-body");
123
- return false;
124
- }
119
+ const $cards = $(".accordion-item.design-control");
125
120
 
126
- event.originalEvent.dataTransfer.setData("form", formHtml);
127
- event.originalEvent.dataTransfer.setData("action", $(this).find(".draggable-action").data("action"));
128
- event.originalEvent.dataTransfer.setData("id", $(this).find(".draggable-action").attr("id"));
121
+ // Toggle draggable based on mouse/touch position
122
+ $cards.off("mousedown touchstart").on("mousedown touchstart", function (event) {
123
+ this.setAttribute("draggable", $(event.target).closest(".input-group").length ? "false" : "true");
124
+ });
129
125
 
130
- $(this).addClass("dragging");
126
+ // Handle the actual drag
127
+ $cards.off("dragstart dragend").on({
128
+ dragstart: function (event) {
129
+ if (this.getAttribute("draggable") !== "true") {
130
+ event.preventDefault();
131
+ return false;
132
+ }
133
+
134
+ const formHtml = $(this).find(".accordion-body form").prop("outerHTML");
135
+ if (!formHtml) return false;
136
+
137
+ event.originalEvent.dataTransfer.setData("form", formHtml);
138
+ event.originalEvent.dataTransfer.setData("action", $(this).find(".draggable-action").data("action"));
139
+ event.originalEvent.dataTransfer.setData("id", $(this).find(".draggable-action").attr("id"));
140
+
141
+ $(this).addClass("dragging");
142
+ },
143
+ dragend: function () {
144
+ $(this).removeClass("dragging").attr("draggable", "false");
145
+ }
131
146
  });
147
+
148
+ // Prevent form inputs from being draggable
149
+ $(".accordion-item input, .accordion-item select").attr("draggable", "false");
132
150
  }
133
151
 
134
152
  // Make sure it's called in the document ready function
@@ -24,9 +24,25 @@
24
24
  <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
25
25
  <div class= "container">
26
26
  {# {{ module_config }}#}
27
+
27
28
  <a class="navbar-brand" href="{{ url_for('main.index') }}">
28
- <img src="{{url_for('static', filename='logo.webp')}}" alt="Logo" height="60" class="d-inline-block align-text-bottom">
29
+ {% if current_user.is_authenticated %}
30
+ {% if current_user.settings.logo_mode == 'replace' and current_user.settings.logo_filename %}
31
+ <img src="{{ url_for('static', filename='user_logos/' ~ current_user.settings.logo_filename) }}" alt="User Logo" height="50">
32
+ {% else %}
33
+
34
+ {% if current_user.settings.logo_mode == 'add' and current_user.settings.logo_filename %}
35
+ <img src="{{ url_for('static', filename='user_logos/' ~ current_user.settings.logo_filename) }}" alt="User Logo" height="50" class="ms-2">
36
+ {% endif %}
37
+ <img src="{{ url_for('static', filename='ivoryos_logo.png') }}" alt="IvoryOS" height="50">
38
+ {% endif %}
39
+ {% else %}
40
+ <img src="{{ url_for('static', filename='ivoryos_logo.png') }}" alt="IvoryOS" height="50">
41
+ {% endif %}
29
42
  </a>
43
+
44
+
45
+
30
46
  <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
31
47
  <span class="navbar-toggler-icon"></span>
32
48
  </button>
@@ -69,11 +85,12 @@
69
85
  {% endif %}
70
86
  </ul>
71
87
  <ul class="navbar-nav ms-auto">
72
- {# {{ current_user }}#}
73
88
  {% if current_user.is_authenticated %}
74
89
  <div class="dropdown">
75
90
  <li class="nav-item " aria-expanded="false"><i class="bi bi-person-circle"></i> {{ current_user.get_id() }}</li>
76
91
  <ul class="dropdown-menu">
92
+ <li><a class="dropdown-item" href="{{ url_for("auth.change_password") }}" role="button" aria-expanded="false">Change Password</a></li>
93
+ <li><a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#logoModal" role="button" aria-expanded="false">Customize Logo</a></li>
77
94
  <li><a class="dropdown-item" href="{{ url_for("auth.logout") }}" role="button" aria-expanded="false">Logout</a></li>
78
95
  </ul>
79
96
 
@@ -153,5 +170,47 @@
153
170
  </div>
154
171
  </div>
155
172
  </div>
173
+
174
+ <!-- Logo Customization Modal -->
175
+ <div class="modal fade" id="logoModal" tabindex="-1" aria-labelledby="logoModalLabel" aria-hidden="true">
176
+ <div class="modal-dialog modal-dialog-centered">
177
+ <div class="modal-content shadow-lg">
178
+ <div class="modal-header">
179
+ <h5 class="modal-title" id="logoModalLabel">Customize Navbar Logo</h5>
180
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
181
+ </div>
182
+ <form method="POST"
183
+ action="{{ url_for('main.customize_logo') }}"
184
+ enctype="multipart/form-data">
185
+ <div class="modal-body">
186
+ <div class="mb-3">
187
+ <label class="form-label fw-semibold">Upload your logo</label>
188
+ <input type="file" name="logo" accept="image/*" class="form-control" required>
189
+ </div>
190
+ <div class="mb-2">
191
+ <label class="form-label fw-semibold">Display mode</label>
192
+ <div class="form-check">
193
+ <input class="form-check-input" type="radio" name="mode" id="add" value="add" checked>
194
+ <label class="form-check-label" for="add">
195
+ Add next to IvoryOS logo
196
+ </label>
197
+ </div>
198
+ <div class="form-check">
199
+ <input class="form-check-input" type="radio" name="mode" id="replace" value="replace">
200
+ <label class="form-check-label" for="replace">
201
+ Replace IvoryOS logo
202
+ </label>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ <div class="modal-footer">
207
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
208
+ <button type="submit" class="btn btn-primary">Apply</button>
209
+ </div>
210
+ </form>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
156
215
  </body>
157
216
  </html>
@@ -134,7 +134,7 @@ def parse_optimization_form(form_data: Dict[str, str]):
134
134
  Parse dynamic form data into structured optimization configuration.
135
135
 
136
136
  Expected form field patterns:
137
- - Objectives: {name}_min, {name}_weight
137
+ - Objectives: {name}_obj_min, {name}_weight
138
138
  - Parameters: {name}_type, {name}_min, {name}_max, {name}_choices, {name}_value_type
139
139
  - Config: step{n}_model, step{n}_num_samples
140
140
  """
@@ -149,25 +149,25 @@ def parse_optimization_form(form_data: Dict[str, str]):
149
149
 
150
150
  # Parse objectives
151
151
  for field_name, value in form_data.items():
152
- if field_name.endswith('_min') and value:
152
+ if field_name.endswith('_obj_min') and value:
153
153
  # Extract objective name
154
- obj_name = field_name.replace('_min', '')
154
+ obj_name = field_name.replace('_obj_min', '')
155
155
  if obj_name in processed_objectives:
156
156
  continue
157
157
 
158
158
  # Check if corresponding weight exists
159
159
  weight_field = f"{obj_name}_weight"
160
- if weight_field in form_data and form_data[weight_field]:
161
- objectives.append({
162
- "name": obj_name,
163
- "minimize": value == "minimize",
164
- "weight": float(form_data[weight_field])
165
- })
166
- else:
167
- objectives.append({
160
+ early_stop_field = f"{obj_name}_obj_threshold"
161
+
162
+ config = {
168
163
  "name": obj_name,
169
164
  "minimize": value == "minimize",
170
- })
165
+ }
166
+ if weight_field in form_data and form_data[weight_field]:
167
+ config["weight"] = float(form_data[weight_field])
168
+ if early_stop_field in form_data and form_data[early_stop_field]:
169
+ config["early_stop"] = float(form_data[early_stop_field])
170
+ objectives.append(config)
171
171
  processed_objectives.add(obj_name)
172
172
 
173
173
  # Parse parameters
@@ -192,11 +192,11 @@ def parse_optimization_form(form_data: Dict[str, str]):
192
192
  if value == "range":
193
193
  min_field = f"{param_name}_min"
194
194
  max_field = f"{param_name}_max"
195
-
195
+ step_field = f"{param_name}_step"
196
196
  if min_field in form_data and max_field in form_data:
197
197
  min_val = form_data[min_field]
198
198
  max_val = form_data[max_field]
199
-
199
+ step_val = form_data[step_field] if step_field in form_data else None
200
200
  if min_val and max_val:
201
201
  # Convert based on value_type
202
202
  if value_type == "int":
@@ -204,12 +204,13 @@ def parse_optimization_form(form_data: Dict[str, str]):
204
204
  elif value_type == "float":
205
205
  bounds = [float(min_val), float(max_val)]
206
206
  else: # string
207
- bounds = [str(min_val), str(max_val)]
208
-
207
+ bounds = [float(min_val), float(max_val)]
208
+ if step_val:
209
+ bounds.append(float(step_val))
209
210
  parameter["bounds"] = bounds
210
211
 
211
212
  elif value == "choice":
212
- choices_field = f"{param_name}_choices"
213
+ choices_field = f"{param_name}_value"
213
214
  if choices_field in form_data and form_data[choices_field]:
214
215
  # Split choices by comma and clean whitespace
215
216
  choices = [choice.strip() for choice in form_data[choices_field].split(',')]
@@ -1,57 +1,288 @@
1
- # import argparse
2
1
  import os
2
+ import re
3
+ from typing import Dict, Set, Any, Optional
3
4
 
4
- # import requests
5
5
 
6
- # session = requests.Session()
6
+ class ProxyGenerator:
7
+ """
8
+ A class to generate Python proxy interfaces for API clients.
7
9
 
10
+ This generator creates client classes that wrap API endpoints,
11
+ automatically handling request/response cycles and error handling.
12
+ """
8
13
 
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'
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
+ }
12
19
 
13
- for function_name, details in functions.items():
14
- signature = details['signature']
15
- docstring = details.get('docstring', '')
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"
16
122
 
17
- # Creating the function definition
18
- method = f' def {function_name}{signature}:\n'
19
123
  if docstring:
20
124
  method += f' """{docstring}"""\n'
21
125
 
22
- # Generating the session.post code for sending data
23
- method += ' return session.post(self.url, data={'
24
- method += f'"hidden_name": "{function_name}"'
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"
25
136
 
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 []
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 = []
29
156
 
30
157
  for param in params:
31
- param_name = param.split(':')[0].strip() # Split on ':' and get parameter name
32
- method += f', "{param_name}": {param_name}'
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
33
245
 
34
- method += '}).json()\n'
35
- class_template += method + '\n'
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.
36
252
 
37
- return class_template
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
38
257
 
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')
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('/')
45
263
 
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')
264
+ # Generate the proxy file
265
+ return self.generate_proxy_file(snapshot, output_folder)
50
266
 
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')
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()
55
273
 
274
+ '''
56
275
 
57
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
+ """