ivoryos 1.0.9__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 (107) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +17 -207
  3. ivoryos/app.py +154 -0
  4. ivoryos/config.py +1 -0
  5. ivoryos/optimizer/ax_optimizer.py +191 -0
  6. ivoryos/optimizer/base_optimizer.py +84 -0
  7. ivoryos/optimizer/baybe_optimizer.py +193 -0
  8. ivoryos/optimizer/nimo_optimizer.py +173 -0
  9. ivoryos/optimizer/registry.py +11 -0
  10. ivoryos/routes/auth/auth.py +43 -14
  11. ivoryos/routes/auth/templates/change_password.html +32 -0
  12. ivoryos/routes/control/control.py +101 -366
  13. ivoryos/routes/control/control_file.py +33 -0
  14. ivoryos/routes/control/control_new_device.py +152 -0
  15. ivoryos/routes/control/templates/controllers.html +193 -0
  16. ivoryos/routes/control/templates/controllers_new.html +112 -0
  17. ivoryos/routes/control/utils.py +40 -0
  18. ivoryos/routes/data/data.py +197 -0
  19. ivoryos/routes/data/templates/components/step_card.html +78 -0
  20. ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
  21. ivoryos/routes/data/templates/workflow_view.html +360 -0
  22. ivoryos/routes/design/__init__.py +4 -0
  23. ivoryos/routes/design/design.py +348 -657
  24. ivoryos/routes/design/design_file.py +68 -0
  25. ivoryos/routes/design/design_step.py +171 -0
  26. ivoryos/routes/design/templates/components/action_form.html +53 -0
  27. ivoryos/routes/design/templates/components/actions_panel.html +25 -0
  28. ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  29. ivoryos/routes/design/templates/components/canvas.html +5 -0
  30. ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  31. ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  32. ivoryos/routes/design/templates/components/canvas_main.html +39 -0
  33. ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  34. ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
  35. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  36. ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
  37. ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
  38. ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
  39. ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
  40. ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
  41. ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
  42. ivoryos/routes/design/templates/components/modals.html +6 -0
  43. ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  44. ivoryos/routes/design/templates/components/sidebar.html +15 -0
  45. ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
  46. ivoryos/routes/design/templates/experiment_builder.html +44 -0
  47. ivoryos/routes/execute/__init__.py +0 -0
  48. ivoryos/routes/execute/execute.py +377 -0
  49. ivoryos/routes/execute/execute_file.py +78 -0
  50. ivoryos/routes/execute/templates/components/error_modal.html +20 -0
  51. ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
  52. ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
  53. ivoryos/routes/execute/templates/components/run_panel.html +9 -0
  54. ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  55. ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  56. ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  57. ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
  58. ivoryos/routes/execute/templates/experiment_run.html +30 -0
  59. ivoryos/routes/library/__init__.py +0 -0
  60. ivoryos/routes/library/library.py +157 -0
  61. ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
  62. ivoryos/routes/main/main.py +31 -3
  63. ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
  64. ivoryos/server.py +180 -0
  65. ivoryos/socket_handlers.py +52 -0
  66. ivoryos/static/ivoryos_logo.png +0 -0
  67. ivoryos/static/js/action_handlers.js +384 -0
  68. ivoryos/static/js/db_delete.js +23 -0
  69. ivoryos/static/js/script_metadata.js +39 -0
  70. ivoryos/static/js/socket_handler.js +40 -5
  71. ivoryos/static/js/sortable_design.js +107 -56
  72. ivoryos/static/js/ui_state.js +114 -0
  73. ivoryos/templates/base.html +67 -8
  74. ivoryos/utils/bo_campaign.py +180 -3
  75. ivoryos/utils/client_proxy.py +267 -36
  76. ivoryos/utils/db_models.py +300 -65
  77. ivoryos/utils/decorators.py +34 -0
  78. ivoryos/utils/form.py +63 -29
  79. ivoryos/utils/global_config.py +34 -1
  80. ivoryos/utils/nest_script.py +314 -0
  81. ivoryos/utils/py_to_json.py +295 -0
  82. ivoryos/utils/script_runner.py +599 -165
  83. ivoryos/utils/serilize.py +201 -0
  84. ivoryos/utils/task_runner.py +71 -21
  85. ivoryos/utils/utils.py +50 -6
  86. ivoryos/version.py +1 -1
  87. ivoryos-1.4.4.dist-info/METADATA +263 -0
  88. ivoryos-1.4.4.dist-info/RECORD +119 -0
  89. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
  90. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
  91. tests/unit/test_type_conversion.py +42 -0
  92. tests/unit/test_util.py +3 -0
  93. ivoryos/routes/control/templates/control/controllers.html +0 -78
  94. ivoryos/routes/control/templates/control/controllers_home.html +0 -55
  95. ivoryos/routes/control/templates/control/controllers_new.html +0 -89
  96. ivoryos/routes/database/database.py +0 -306
  97. ivoryos/routes/database/templates/database/step_card.html +0 -7
  98. ivoryos/routes/database/templates/database/workflow_view.html +0 -130
  99. ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
  100. ivoryos/routes/design/templates/design/experiment_run.html +0 -558
  101. ivoryos-1.0.9.dist-info/METADATA +0 -218
  102. ivoryos-1.0.9.dist-info/RECORD +0 -61
  103. /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
  104. /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
  105. /ivoryos/routes/{database → data}/__init__.py +0 -0
  106. /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
  107. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,201 @@
1
+ import ast
2
+ import json
3
+ import inspect
4
+ import logging
5
+ from typing import get_type_hints, Union, Optional, get_origin, get_args
6
+ import sys
7
+
8
+ import flask
9
+
10
+ from example.abstract_sdl_example import abstract_sdl as deck
11
+
12
+
13
+
14
+ class ScriptAnalyzer:
15
+ def __init__(self):
16
+ self.primitive_types = {
17
+ 'int', 'float', 'str', 'bool', 'tuple', 'list'
18
+ }
19
+ self.type_mapping = {
20
+ int: 'int',
21
+ float: 'float',
22
+ str: 'str',
23
+ bool: 'bool',
24
+ tuple: 'tuple',
25
+ list: 'list'
26
+ }
27
+
28
+ def extract_type_from_hint(self, type_hint):
29
+ """Extract primitive types from type hints, handling Union and Optional"""
30
+ if type_hint is None:
31
+ return None
32
+
33
+ # Handle Union types (including Optional which is Union[T, None])
34
+ origin = get_origin(type_hint)
35
+ if origin is Union:
36
+ args = get_args(type_hint)
37
+ types = []
38
+ for arg in args:
39
+ if arg is type(None): # Skip None type
40
+ continue
41
+ if arg in self.type_mapping:
42
+ types.append(self.type_mapping[arg])
43
+ elif hasattr(arg, '__name__') and arg.__name__ in self.primitive_types:
44
+ types.append(arg.__name__)
45
+ else:
46
+ return None # Non-primitive type found
47
+ return types if types else None
48
+
49
+ # Handle direct primitive types
50
+ if type_hint in self.type_mapping:
51
+ return [self.type_mapping[type_hint]]
52
+ elif hasattr(type_hint, '__name__') and type_hint.__name__ in self.primitive_types:
53
+ return [type_hint.__name__]
54
+ else:
55
+ return None # Non-primitive type
56
+
57
+
58
+ def analyze_method(self, method):
59
+ """Analyze a single method and extract its signature"""
60
+ try:
61
+ sig = inspect.signature(method)
62
+ type_hints = get_type_hints(method)
63
+
64
+ parameters = []
65
+ type_hint_warning = False
66
+ user_input_params = []
67
+
68
+ for param_name, param in sig.parameters.items():
69
+ if param_name == 'self':
70
+ continue
71
+
72
+ param_info = {"name": param_name}
73
+
74
+ # Get type hint
75
+ if param_name in type_hints:
76
+ type_list = self.extract_type_from_hint(type_hints[param_name])
77
+ if type_list is None:
78
+ type_hint_warning = True
79
+ user_input_params.append(param_name)
80
+ param_info["type"] = ""
81
+ else:
82
+ param_info["type"] = type_list[0] if len(type_list) == 1 else type_list
83
+ else:
84
+ type_hint_warning = True
85
+ user_input_params.append(param_name)
86
+ param_info["type"] = ""
87
+
88
+ # Get default value
89
+ if param.default != inspect.Parameter.empty:
90
+ param_info["default"] = param.default
91
+
92
+ parameters.append(param_info)
93
+
94
+ # Get docstring
95
+ docstring = inspect.getdoc(method)
96
+
97
+ method_info = {
98
+ "docstring": docstring or "",
99
+ "parameters": parameters
100
+ }
101
+
102
+ return method_info, type_hint_warning
103
+
104
+ except Exception as e:
105
+ print(f"Error analyzing method {method.__name__}: {e}")
106
+ return None
107
+
108
+
109
+ def analyze_module(self, module, exclude_names=[]):
110
+ """Analyze module from sys.modules and extract class instances and methods"""
111
+ exclude_classes = (flask.Blueprint, logging.Logger)
112
+ # Get all variables in the module that are class instances
113
+ included = {}
114
+ excluded = {}
115
+ failed = {}
116
+ included_with_warnings = {}
117
+ for name, obj in vars(module).items():
118
+ if (
119
+ type(obj).__module__ == 'builtins'
120
+ or name[0].isupper()
121
+ or name.startswith("_")
122
+ or isinstance(obj, exclude_classes)
123
+ or name in exclude_names
124
+ or not hasattr(obj, '__class__')
125
+ ):
126
+ excluded[name] = type(obj).__name__
127
+ continue
128
+
129
+ cls = obj.__class__
130
+
131
+ try:
132
+ class_methods, type_hint_warning = self.analyze_class(cls)
133
+ if class_methods:
134
+ if type_hint_warning:
135
+ included_with_warnings[name] = class_methods
136
+ included[name] = class_methods
137
+ except Exception as e:
138
+ failed[name] = str(e)
139
+ continue
140
+ return included, included_with_warnings, failed, excluded
141
+
142
+ def save_to_json(self, data, output_path):
143
+ """Save analysis result to JSON file"""
144
+ with open(output_path, 'w') as f:
145
+ json.dump(data, f, indent=2)
146
+
147
+ def analyze_class(self, cls):
148
+ class_methods = {}
149
+ type_hint_flag = False
150
+ for method_name, method in inspect.getmembers(cls, predicate=callable):
151
+ if method_name.startswith("_") or method_name.isupper():
152
+ continue
153
+ method_info, type_hint_warning = self.analyze_method(method)
154
+ if type_hint_warning:
155
+ type_hint_flag = True
156
+ if method_info:
157
+ class_methods[method_name] = method_info
158
+ return class_methods, type_hint_flag
159
+
160
+ @staticmethod
161
+ def print_deck_snapshot(result, with_warnings, failed):
162
+ print("\nDeck Snapshot:")
163
+ print("Included Classes:")
164
+ for name, methods in result.items():
165
+ print(f" {name}:")
166
+ for method_name, method_info in methods.items():
167
+ print(f" {method_name}: {method_info}")
168
+
169
+ if with_warnings:
170
+ print("\nClasses with Type Hint Warnings:")
171
+ for name, methods in with_warnings.items():
172
+ print(f" {name}:")
173
+ for method_name, method_info in methods.items():
174
+ print(f" {method_name}: {method_info}")
175
+
176
+ if failed:
177
+ print("\nFailed Classes:")
178
+ for name, error in failed.items():
179
+ print(f" {name}: {error}")
180
+
181
+ if __name__ == "__main__":
182
+
183
+ _analyzer = ScriptAnalyzer()
184
+ # module = sys.modules[deck]
185
+ try:
186
+
187
+ result, with_warnings, failed, _ = _analyzer.analyze_module(deck)
188
+
189
+ output_path = f"analysis.json"
190
+ _analyzer.save_to_json(result, output_path)
191
+
192
+ print(f"\nAnalysis complete! Results saved to: {output_path}")
193
+
194
+
195
+
196
+
197
+ _analyzer.print_deck_snapshot(result, with_warnings, failed)
198
+
199
+ except Exception as e:
200
+ print(f"Error: {e}")
201
+
@@ -1,7 +1,10 @@
1
+ import inspect
2
+ import asyncio
1
3
  import threading
2
4
  import time
3
5
  from datetime import datetime
4
6
 
7
+ from ivoryos.utils.decorators import BUILDING_BLOCKS
5
8
  from ivoryos.utils.db_models import db, SingleStep
6
9
  from ivoryos.utils.global_config import GlobalConfig
7
10
 
@@ -18,8 +21,7 @@ class TaskRunner:
18
21
  self.globals_dict = globals_dict
19
22
  self.lock = global_config.runner_lock
20
23
 
21
-
22
- def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
24
+ async def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
23
25
  global deck
24
26
  if deck is None:
25
27
  deck = global_config.deck
@@ -28,18 +30,18 @@ class TaskRunner:
28
30
  if not self.lock.acquire(blocking=False):
29
31
  current_status = global_config.runner_status
30
32
  current_status["status"] = "busy"
33
+ current_status["output"] = "busy"
31
34
  return current_status
32
35
 
33
-
34
36
  if wait:
35
- output = self._run_single_step(component, method, kwargs, current_app)
37
+ output = await self._run_single_step(component, method, kwargs, current_app)
36
38
  else:
37
- print("running with thread")
38
- thread = threading.Thread(
39
- target=self._run_single_step, args=(component, method, kwargs, current_app)
40
- )
41
- thread.start()
42
- time.sleep(0.1)
39
+ # Create background task properly
40
+ async def background_runner():
41
+ await self._run_single_step(component, method, kwargs, current_app)
42
+
43
+ asyncio.create_task(background_runner())
44
+ await asyncio.sleep(0.1) # Change time.sleep to await asyncio.sleep
43
45
  output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
44
46
 
45
47
  return output
@@ -48,34 +50,82 @@ class TaskRunner:
48
50
  if component.startswith("deck."):
49
51
  component = component.split(".")[1]
50
52
  instrument = getattr(deck, component)
53
+ function_executable = getattr(instrument, method)
54
+ elif component.startswith("blocks."):
55
+ component = component.split(".")[1]
56
+ function_executable = BUILDING_BLOCKS[component][method]["func"]
51
57
  else:
52
58
  temp_connections = global_config.defined_variables
53
59
  instrument = temp_connections.get(component)
54
- function_executable = getattr(instrument, method)
60
+ function_executable = getattr(instrument, method)
55
61
  return function_executable
56
62
 
57
- def _run_single_step(self, component, method, kwargs, current_app=None):
63
+ async def _run_single_step(self, component, method, kwargs, current_app=None):
58
64
  try:
59
65
  function_executable = self._get_executable(component, deck, method)
60
- method_name = f"{function_executable.__self__.__class__.__name__}.{function_executable.__name__}"
66
+ method_name = f"{component}.{method}"
61
67
  except Exception as e:
62
68
  self.lock.release()
63
- return {"status": "error", "msg": e.__str__()}
69
+ return {"status": "error", "msg": str(e)}
64
70
 
65
- # with self.lock:
71
+ # Flask context is NOT async → just use normal "with"
66
72
  with current_app.app_context():
67
- step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=False, start_time=datetime.now())
73
+ step = SingleStep(
74
+ method_name=method_name,
75
+ kwargs=kwargs,
76
+ run_error=None,
77
+ start_time=datetime.now()
78
+ )
68
79
  db.session.add(step)
69
- db.session.commit()
70
- global_config.runner_status = {"id":step.id, "type": "task"}
80
+ db.session.flush()
81
+ global_config.runner_status = {"id": step.id, "type": "task"}
82
+
71
83
  try:
72
- output = function_executable(**kwargs)
84
+ kwargs = self._convert_kwargs_type(kwargs, function_executable)
85
+
86
+ if inspect.iscoroutinefunction(function_executable):
87
+ output = await function_executable(**kwargs)
88
+ else:
89
+ output = function_executable(**kwargs)
90
+
73
91
  step.output = output
74
92
  step.end_time = datetime.now()
93
+ success = True
75
94
  except Exception as e:
76
- step.run_error = e.__str__()
95
+ step.run_error = str(e)
77
96
  step.end_time = datetime.now()
97
+ success = False
98
+ output = str(e)
78
99
  finally:
79
100
  db.session.commit()
80
101
  self.lock.release()
81
- return output
102
+
103
+ return dict(success=success, output=output)
104
+
105
+ @staticmethod
106
+ def _convert_kwargs_type(kwargs, function_executable):
107
+ def convert_guess(str_value):
108
+ str_value = str_value.strip()
109
+ if str_value.isdigit() or (str_value.startswith('-') and str_value[1:].isdigit()):
110
+ return int(str_value)
111
+ try:
112
+ return float(str_value)
113
+ except ValueError:
114
+ return str_value
115
+
116
+ sig = inspect.signature(function_executable)
117
+ converted = {}
118
+
119
+ for name, value in kwargs.items():
120
+ if name in sig.parameters:
121
+ param = sig.parameters[name]
122
+ if param.annotation != inspect.Parameter.empty:
123
+ # convert using type hint
124
+ try:
125
+ converted[name] = param.annotation(value)
126
+ except Exception:
127
+ converted[name] = value
128
+ else:
129
+ # no type hint → guess
130
+ converted[name] = convert_guess(value)
131
+ return converted
ivoryos/utils/utils.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import ast
2
2
  import importlib
3
3
  import inspect
4
+ import json
4
5
  import logging
5
6
  import os
6
7
  import pickle
@@ -11,10 +12,11 @@ from collections import Counter
11
12
 
12
13
  import flask
13
14
  from flask import session
15
+ from flask_login import current_user
14
16
  from flask_socketio import SocketIO
15
17
 
16
18
  from ivoryos.utils.db_models import Script
17
-
19
+ from ivoryos.utils.decorators import BUILDING_BLOCKS
18
20
 
19
21
  def get_script_file():
20
22
  """Get script from Flask session and returns the script"""
@@ -24,7 +26,7 @@ def get_script_file():
24
26
  s.__dict__.update(**session_script)
25
27
  return s
26
28
  else:
27
- return Script(author=session.get('user'))
29
+ return Script(author=current_user.get_id(),)
28
30
 
29
31
 
30
32
  def post_script_file(script, is_dict=False):
@@ -104,7 +106,8 @@ def _inspect_class(class_object=None, debug=False):
104
106
  try:
105
107
  annotation = inspect.signature(method)
106
108
  docstring = inspect.getdoc(method)
107
- functions[function] = dict(signature=annotation, docstring=docstring)
109
+ coroutine = inspect.iscoroutinefunction(method)
110
+ functions[function] = dict(signature=annotation, docstring=docstring, coroutine=coroutine,)
108
111
 
109
112
  except Exception:
110
113
  pass
@@ -140,6 +143,7 @@ def _get_type_from_parameters(arg, parameters):
140
143
  def _convert_by_str(args, arg_types):
141
144
  """
142
145
  Converts a value to type through eval(f'{type}("{args}")')
146
+ v1.3.4 TODO try str lastly, otherwise it's always converted to str
143
147
  """
144
148
  if type(arg_types) is not list:
145
149
  arg_types = [arg_types]
@@ -150,6 +154,7 @@ def _convert_by_str(args, arg_types):
150
154
  return args
151
155
  except Exception:
152
156
  raise TypeError(f"Input type error: cannot convert '{args}' to {arg_type}.")
157
+ return args
153
158
 
154
159
 
155
160
  def _convert_by_class(args, arg_types):
@@ -347,8 +352,8 @@ def create_deck_snapshot(deck, save: bool = False, output_path: str = '', exclud
347
352
  for name, class_type in items.items():
348
353
  print(f" {name}: {class_type}")
349
354
 
350
- print_section("✅ INCLUDED", deck_summary["included"])
351
- print_section("❌ FAILED", deck_summary["failed"])
355
+ print_section("✅ INCLUDED MODULES", deck_summary["included"])
356
+ print_section("❌ FAILED MODULES", deck_summary["failed"])
352
357
  print("\n")
353
358
 
354
359
  print_deck_snapshot(deck_summary)
@@ -363,6 +368,28 @@ def create_deck_snapshot(deck, save: bool = False, output_path: str = '', exclud
363
368
  return deck_snapshot
364
369
 
365
370
 
371
+ def create_block_snapshot(save: bool = False, output_path: str = ''):
372
+ block_snapshot = {}
373
+ included = {}
374
+ failed = {}
375
+ for category, data in BUILDING_BLOCKS.items():
376
+ key = f"blocks.{category}"
377
+ block_snapshot[key] = {}
378
+
379
+ for func_name, meta in data.items():
380
+ func = meta["func"]
381
+ block_snapshot[key][func_name] = {
382
+ "signature": meta["signature"],
383
+ "docstring": meta["docstring"],
384
+ "coroutine": meta["coroutine"],
385
+ "path": f"{func.__module__}.{func.__qualname__}"
386
+ }
387
+ if block_snapshot:
388
+ print(f"\n=== ✅ BUILDING_BLOCKS ({len(block_snapshot)}) ===")
389
+ for category, blocks in block_snapshot.items():
390
+ print(f" {category}: ", ",".join(blocks.keys()))
391
+ return block_snapshot
392
+
366
393
  def load_deck(pkl_name: str):
367
394
  """
368
395
  Loads a pickled deck snapshot from disk on offline mode
@@ -419,4 +446,21 @@ def get_local_ip():
419
446
  ip = '127.0.0.1'
420
447
  finally:
421
448
  s.close()
422
- return ip
449
+ return ip
450
+
451
+
452
+ def safe_dump(obj):
453
+ try:
454
+ json.dumps(obj)
455
+ return obj
456
+ except (TypeError, OverflowError):
457
+ return repr(obj) # store readable representation
458
+
459
+
460
+ def create_module_snapshot(module):
461
+ classes = inspect.getmembers(module, inspect.isclass)
462
+ api_variables = {}
463
+ for i in classes:
464
+ # globals()[i[0]] = i[1]
465
+ api_variables[i[0]] = i[1]
466
+ return api_variables
ivoryos/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.0.9"
1
+ __version__ = "1.4.4"