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

ivoryos/routes/api/api.py CHANGED
@@ -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,
@@ -2,7 +2,7 @@ import os
2
2
  from flask import Blueprint, request,current_app, send_file
3
3
  from flask_login import login_required
4
4
 
5
- from ivoryos.utils.client_proxy import export_to_python, create_function
5
+ from ivoryos.utils.client_proxy import ProxyGenerator
6
6
  from ivoryos.utils.global_config import GlobalConfig
7
7
 
8
8
  global_config = GlobalConfig()
@@ -10,27 +10,24 @@ global_config = GlobalConfig()
10
10
  control_file = Blueprint('file', __name__)
11
11
 
12
12
 
13
+
13
14
  @control_file.route("/files/proxy", strict_slashes=False)
14
15
  @login_required
15
16
  def download_proxy():
16
17
  """
17
- .. :quickref: Direct Control Files; download proxy interface
18
+ .. :quickref: Direct Control Files; Download proxy Python interface
18
19
 
19
20
  download proxy Python interface
20
21
 
21
22
  .. http:get:: /files/proxy
22
23
  """
24
+ generator = ProxyGenerator(request.url_root)
23
25
  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)
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);
@@ -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
+ """
@@ -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 ""
ivoryos/utils/form.py CHANGED
@@ -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()
ivoryos/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.4"
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,20 +1,20 @@
1
1
  ivoryos/__init__.py,sha256=BAA7OPl3h_QdSc4na-7OWTKhaOq1LtHOrJHX3gGITrc,9863
2
2
  ivoryos/config.py,sha256=y3RxNjiIola9tK7jg-mHM8EzLMwiLwOzoisXkDvj0gA,2174
3
3
  ivoryos/socket_handlers.py,sha256=VWVWiIdm4jYAutwGu6R0t1nK5MuMyOCL0xAnFn06jWQ,1302
4
- ivoryos/version.py,sha256=XBKH8E1LmDxv06U39yqMBbXZapOERFgICEDYZs_kRso,22
4
+ ivoryos/version.py,sha256=vMQK58X8_YZGKzRm0ThvPAKFtpfyejGmUnDrY9RQ13w,22
5
5
  ivoryos/optimizer/ax_optimizer.py,sha256=PoSu8hrDFFpqyhRBnaSMswIUsDfEX6sPWt8NEZ_sobs,7112
6
6
  ivoryos/optimizer/base_optimizer.py,sha256=JTbUharZKn0t8_BDbAFuwZIbT1VOnX1Xuog1pJuU8hY,1992
7
7
  ivoryos/optimizer/baybe_optimizer.py,sha256=EdrrRiYO-IOx610cPXiQhH4qG8knUP0uiZ0YoyaGIU8,7954
8
8
  ivoryos/optimizer/registry.py,sha256=lr0cqdI2iEjw227ZPRpVkvsdYdddjeJJRzawDv77cEc,219
9
9
  ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- ivoryos/routes/api/api.py,sha256=X_aZMB_nCxW41pqZpJOiEEwGmlqLqJUArruevuy41v0,2284
10
+ ivoryos/routes/api/api.py,sha256=1Hq4FOBtSEXqjataoPUdAWHvezw07xqhEI1fJdoSn5U,2284
11
11
  ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  ivoryos/routes/auth/auth.py,sha256=CqoP9cM8BuXVGHGujX7-0sNAOdWILU9amyBrObOD6Ss,3283
13
13
  ivoryos/routes/auth/templates/login.html,sha256=WSRrKbdM_oobqSXFRTo-j9UlOgp6sYzS9tm7TqqPULI,1207
14
14
  ivoryos/routes/auth/templates/signup.html,sha256=b5LTXtpfTSkSS7X8u1ldwQbbgEFTk6UNMAediA5BwBY,1465
15
15
  ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- ivoryos/routes/control/control.py,sha256=Vmy3GRZz8EbKmV9qslR8gsaYaYzb5HQTpukp42HfMow,5236
17
- ivoryos/routes/control/control_file.py,sha256=NIAzwhswvpl3u0mansy1ku-rPDybS5hVbmbnymOikWk,1548
16
+ ivoryos/routes/control/control.py,sha256=d08xFo-7Fm7hdFPBxmcf_48UsxO4ctcApm0Wxo_oPQk,5530
17
+ ivoryos/routes/control/control_file.py,sha256=3fQ9R8EcdqKs_hABn2EqRAB1xC2DHAT_q_pwsMIDDQI,864
18
18
  ivoryos/routes/control/control_new_device.py,sha256=mfJKg5JAOagIpUKbp2b5nRwvd2V3bzT3M0zIhIsEaFM,5456
19
19
  ivoryos/routes/control/utils.py,sha256=at11wA5HPAZN4BfMaymj1GKEvRTrqi4Wg6cTqUZJDjU,1155
20
20
  ivoryos/routes/control/templates/controllers.html,sha256=tgtTuns8S2Pf6XKeojinQZ1bz112ieRGLPF5-1cElfE,8030
@@ -72,7 +72,7 @@ ivoryos/static/logo.webp,sha256=lXgfQR-4mHTH83k7VV9iB54-oC2ipe6uZvbwdOnLETc,1497
72
72
  ivoryos/static/style.css,sha256=zQVx35A5g6JMJ-K84-6fSKtzXGjp_p5ZVG6KLHPM2IE,4021
73
73
  ivoryos/static/gui_annotation/Slide1.png,sha256=Lm4gdOkUF5HIUFaB94tl6koQVkzpitKj43GXV_XYMMc,121727
74
74
  ivoryos/static/gui_annotation/Slide2.PNG,sha256=z3wQ9oVgg4JTWVLQGKK_KhtepRHUYP1e05XUWGT2A0I,118761
75
- ivoryos/static/js/action_handlers.js,sha256=VEDox3gQvg0YXJ6WW6IthOsFqZKmvUGJ8pmQdfzHQFw,5122
75
+ ivoryos/static/js/action_handlers.js,sha256=UJHKFhYRNQRBo0AHLCIxhWxt8OSgYeyLynzPIPNhbeY,5125
76
76
  ivoryos/static/js/db_delete.js,sha256=l67fqUaN_FVDaL7v91Hd7LyRbxnqXx9nyjF34-7aewY,561
77
77
  ivoryos/static/js/overlay.js,sha256=dPxop19es0E0ZUSY3d_4exIk7CJuQEnlW5uTt5fZfzI,483
78
78
  ivoryos/static/js/script_metadata.js,sha256=m8VYZ8OGT2oTx1kXMXq60bKQI9WCbJNkzcFDzLvRuGc,1188
@@ -83,18 +83,18 @@ ivoryos/static/js/ui_state.js,sha256=XYsOcfGlduqLlqHySvPrRrR50CiAsml51duqneigsRY
83
83
  ivoryos/templates/base.html,sha256=cl5w6E8yskbUzdiJFal6fZjnPuFNKEzc7BrrbRd6bMI,8581
84
84
  ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
85
85
  ivoryos/utils/bo_campaign.py,sha256=Fil-zT7JexL_p9XqyWByjAk42XB1R9XUKN8CdV5bi6c,9714
86
- ivoryos/utils/client_proxy.py,sha256=0OT2xTMkqh_2ybgCxMV_71ZVUThWwrsnAhTIBY5vDR8,2095
87
- ivoryos/utils/db_models.py,sha256=baE4LJcSGUj10Tj6imfINXi4JQX_4oLv_kb9bd0rp-M,27920
88
- ivoryos/utils/form.py,sha256=8GSEhfY3cE-q9QxGSW_u-9dFNO72N6QOSGwfYyPTfM0,21912
86
+ ivoryos/utils/client_proxy.py,sha256=74G3HAuq50iEHkSvlMZFmQaukm613FbRgOdzO_T3dMg,10191
87
+ ivoryos/utils/db_models.py,sha256=EN0gNzYgCxKLxgceKEixWi17EKMObz0hLdDnpZ-ur5o,27923
88
+ ivoryos/utils/form.py,sha256=A6juCWGtSbaTClgVc8rqufniPRWT7LjeDQs4r0gQs50,22207
89
89
  ivoryos/utils/global_config.py,sha256=zNO9GYhGn7El3msWoxJIm3S4Mzb3VMh2i5ZEsVtvb2Q,2463
90
90
  ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
91
91
  ivoryos/utils/py_to_json.py,sha256=fyqjaxDHPh-sahgT6IHSn34ktwf6y51_x1qvhbNlH-U,7314
92
92
  ivoryos/utils/script_runner.py,sha256=MdLMSAeaVXxnbcQfzHJximeJ6W7uOCxqTQhFSpsEieg,17065
93
93
  ivoryos/utils/serilize.py,sha256=lkBhkz8r2bLmz2_xOb0c4ptSSOqjIu6krj5YYK4Nvj8,6784
94
- ivoryos/utils/task_runner.py,sha256=cDIcmDaqYh0vXoYaL_kO877pluAo2tyfsHl9OgZqJJE,3029
94
+ ivoryos/utils/task_runner.py,sha256=qgHheE2rnhgRmWUeUQHgdS-Kl-yv-9uA-eM6lD9d0b4,3018
95
95
  ivoryos/utils/utils.py,sha256=-WiU0_brszB9yDsiQepf_7SzNgPTSpul2RSKDOY3pqo,13921
96
- ivoryos-1.2.4.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
97
- ivoryos-1.2.4.dist-info/METADATA,sha256=V4lFZwkePsSRrYNShj0i2KM6u8_Z5F2omuHbYeAW5FU,7351
98
- ivoryos-1.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
99
- ivoryos-1.2.4.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
100
- ivoryos-1.2.4.dist-info/RECORD,,
96
+ ivoryos-1.2.6.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
97
+ ivoryos-1.2.6.dist-info/METADATA,sha256=Jf99DEqSEM-hGlQyIxilU021FEsX8W3Ztj_s6IHN1q0,7351
98
+ ivoryos-1.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
99
+ ivoryos-1.2.6.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
100
+ ivoryos-1.2.6.dist-info/RECORD,,