ivoryos 1.2.2__py3-none-any.whl → 1.2.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.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (64) hide show
  1. ivoryos/__init__.py +18 -18
  2. ivoryos/routes/auth/templates/login.html +25 -0
  3. ivoryos/routes/auth/templates/signup.html +32 -0
  4. ivoryos/routes/control/templates/controllers.html +166 -0
  5. ivoryos/routes/control/templates/controllers_new.html +112 -0
  6. ivoryos/routes/data/templates/components/step_card.html +13 -0
  7. ivoryos/routes/data/templates/workflow_database.html +109 -0
  8. ivoryos/routes/data/templates/workflow_view.html +130 -0
  9. ivoryos/routes/design/design.py +1 -1
  10. ivoryos/routes/design/templates/components/action_form.html +53 -0
  11. ivoryos/routes/design/templates/components/actions_panel.html +25 -0
  12. ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  13. ivoryos/routes/design/templates/components/canvas.html +5 -0
  14. ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  15. ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  16. ivoryos/routes/design/templates/components/canvas_main.html +34 -0
  17. ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  18. ivoryos/routes/design/templates/components/edit_action_form.html +38 -0
  19. ivoryos/routes/design/templates/components/instruments_panel.html +66 -0
  20. ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
  21. ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
  22. ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
  23. ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
  24. ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
  25. ivoryos/routes/design/templates/components/modals.html +6 -0
  26. ivoryos/routes/design/templates/components/python_code_overlay.html +39 -0
  27. ivoryos/routes/design/templates/components/sidebar.html +15 -0
  28. ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
  29. ivoryos/routes/design/templates/experiment_builder.html +41 -0
  30. ivoryos/routes/execute/templates/components/error_modal.html +20 -0
  31. ivoryos/routes/execute/templates/components/logging_panel.html +31 -0
  32. ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
  33. ivoryos/routes/execute/templates/components/run_panel.html +9 -0
  34. ivoryos/routes/execute/templates/components/run_tabs.html +17 -0
  35. ivoryos/routes/execute/templates/components/tab_bayesian.html +398 -0
  36. ivoryos/routes/execute/templates/components/tab_configuration.html +98 -0
  37. ivoryos/routes/execute/templates/components/tab_repeat.html +14 -0
  38. ivoryos/routes/execute/templates/experiment_run.html +294 -0
  39. ivoryos/routes/library/templates/library.html +92 -0
  40. ivoryos/routes/main/templates/help.html +141 -0
  41. ivoryos/routes/main/templates/home.html +103 -0
  42. ivoryos/static/favicon.ico +0 -0
  43. ivoryos/static/gui_annotation/Slide1.png +0 -0
  44. ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  45. ivoryos/static/js/action_handlers.js +213 -0
  46. ivoryos/static/js/db_delete.js +23 -0
  47. ivoryos/static/js/overlay.js +12 -0
  48. ivoryos/static/js/script_metadata.js +39 -0
  49. ivoryos/static/js/socket_handler.js +125 -0
  50. ivoryos/static/js/sortable_card.js +24 -0
  51. ivoryos/static/js/sortable_design.js +138 -0
  52. ivoryos/static/js/ui_state.js +114 -0
  53. ivoryos/static/logo.webp +0 -0
  54. ivoryos/static/style.css +211 -0
  55. ivoryos/templates/base.html +157 -0
  56. ivoryos/utils/form.py +6 -1
  57. ivoryos/utils/script_runner.py +46 -38
  58. ivoryos/version.py +1 -1
  59. {ivoryos-1.2.2.dist-info → ivoryos-1.2.4.dist-info}/METADATA +80 -119
  60. ivoryos-1.2.4.dist-info/RECORD +100 -0
  61. ivoryos-1.2.2.dist-info/RECORD +0 -47
  62. {ivoryos-1.2.2.dist-info → ivoryos-1.2.4.dist-info}/WHEEL +0 -0
  63. {ivoryos-1.2.2.dist-info → ivoryos-1.2.4.dist-info}/licenses/LICENSE +0 -0
  64. {ivoryos-1.2.2.dist-info → ivoryos-1.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,211 @@
1
+ .login {
2
+ width: 500px;
3
+ padding: 8% 0 0;
4
+ margin: auto;
5
+ align-content: center;
6
+ }
7
+ .card{
8
+ display: flex;
9
+ justify-content: flex-end;
10
+ cursor: move;
11
+ border: none;
12
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
13
+
14
+ }
15
+
16
+ .card a {
17
+ text-decoration: none;
18
+ }
19
+ .card-title {
20
+ color: #007bff; /* Bootstrap primary color */
21
+ }
22
+ .grid-container {
23
+ display: grid;
24
+ grid-template-columns: repeat(auto-fit, minmax(350px, 400px));
25
+ gap: 25px;
26
+ padding: 10px;
27
+ }
28
+ .navbar-nav {
29
+ font-size: larger;
30
+ }
31
+ .navbar-nav li{
32
+ margin-right:10px;
33
+ margin-left:10px;
34
+ }
35
+
36
+ .canvas {
37
+ height: 70vh;
38
+ overflow: hidden;
39
+ overflow-y: scroll;
40
+ background: #e8e8cd;
41
+ background-size: 20px 20px;
42
+ background-image: radial-gradient(circle, cadetblue 1px, rgba(0, 0, 0, 0) 1px);
43
+ }
44
+
45
+ .canvas content{
46
+ margin-top: 10px;
47
+ }
48
+ body {
49
+ padding-top: 100px;
50
+ background-color: #f8f9fa; /* Light grey background */
51
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
52
+ }
53
+ /*.run-panel-row-height {*/
54
+ /* height: 80vh;*/
55
+ /*}*/
56
+ pre {
57
+ tab-size: 4;
58
+ }
59
+ .scroll-column{
60
+ height: 90vh;
61
+ overflow: hidden;
62
+ overflow-y: scroll;
63
+ }
64
+
65
+ label {
66
+ display:block
67
+ }
68
+
69
+ .scroll {
70
+ overflow: auto;
71
+ -webkit-overflow-scrolling: touch; /* enables momentum-scrolling on iOS */
72
+ position: absolute;
73
+ top: 0;
74
+ left: 0;
75
+ right: 0;
76
+ bottom: 0;
77
+ }
78
+
79
+
80
+ /*Remove the scrollbar from Chrome, Safari, Edge and IE*/
81
+ ::-webkit-scrollbar {
82
+ background: transparent;
83
+ }
84
+
85
+ * {
86
+ -ms-overflow-style: none !important;
87
+ }
88
+ .script-table{
89
+ background: white;
90
+ width: fit-content;
91
+ }
92
+ .script-table td {
93
+ border-bottom: none;
94
+ border-top: cadetblue;
95
+ }
96
+ .script-table th{
97
+ border-top: cadetblue;
98
+ border-bottom: none;
99
+ }
100
+
101
+ .bottom-button {
102
+ position: absolute;
103
+ bottom: 20px;
104
+ }
105
+
106
+ hr.vertical {
107
+ width: 5px;
108
+ height: 100%;
109
+ display: inline-block;
110
+ /* or height in PX */
111
+ }
112
+ .list-group a {
113
+ text-align: left;
114
+ }
115
+
116
+ .tray input[type="checkbox"]:checked + label{
117
+ background: radial-gradient(circle, white 40%, midnightblue, dodgerblue 43%);
118
+ }
119
+
120
+ .btn-vial{
121
+ height: 50px;
122
+ width: 50px;
123
+ border-radius: 50%;
124
+ background: lightgray;
125
+ margin-right: 10px;
126
+ margin-top: 10px;
127
+ /*text-align: center;*/
128
+
129
+ }
130
+
131
+ .tray {
132
+ width: 70px;
133
+ height: 70px;
134
+ display: inline-block;
135
+ background: darkgrey;
136
+ /*text-align:center;*/
137
+ vertical-align: middle;
138
+ }
139
+
140
+ .disabled-link {
141
+ pointer-events: none;
142
+ color: currentColor;
143
+ cursor: not-allowed;
144
+ opacity: 0.5;
145
+ text-decoration: none;
146
+ }
147
+
148
+
149
+ .controller-card a {
150
+ text-decoration: none;
151
+ }
152
+
153
+ #logging-panel {
154
+ flex-grow: 1;
155
+ height: 50vh;
156
+ overflow-y: auto;
157
+ background-color: #f5f5f5;
158
+ padding: 10px;
159
+ }
160
+
161
+ .dropdown:hover .dropdown-menu {
162
+ display: block;
163
+ }
164
+
165
+ #reorder {
166
+ overflow-y: scroll;
167
+ -webkit-overflow-scrolling: touch;
168
+ }
169
+
170
+ .accordion-item.design-control .accordion-button.collapsed {
171
+ background-color:#c1f2f1 !important;
172
+ }
173
+ .accordion-item.design-control .accordion-button {
174
+ background-color:#b3dad9 !important;
175
+ }
176
+ .accordion-item.design-control .accordion-button:hover {
177
+ background-color:#b3dad9 !important;
178
+ }
179
+
180
+ .accordion-item.text-to-code .accordion-button.collapsed {
181
+ background-color: #cdc1f2 !important;
182
+ }
183
+ .accordion-item.text-to-code .accordion-button {
184
+ background-color: #cdc1f2 !important;
185
+ }
186
+ .accordion-item.text-to-code .accordion-button:hover {
187
+ background-color: #b8afdc !important;
188
+ }
189
+ .overlay {
190
+ position: fixed;
191
+ top: 0;
192
+ left: 0;
193
+ width: 100%;
194
+ height: 100%;
195
+ background-color: rgba(0, 0, 0, 0.5);
196
+ display: none;
197
+ z-index: 1000;
198
+ text-align: center;
199
+ color: white;
200
+ font-size: 24px;
201
+ padding-top: 20%;
202
+ }
203
+ .drop-placeholder {
204
+ height: 2px !important; /* Keep it very thin */
205
+ min-height: 2px !important;
206
+ margin: 0 !important;
207
+ padding: 0 !important;
208
+ background: rgba(0, 0, 0, 0.2); /* Slight visibility */
209
+ border-radius: 2px;
210
+ list-style: none; /* Remove any default list styling */
211
+ }
@@ -0,0 +1,157 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{% block title %}{% endblock %}</title>
7
+ {#bootstrap#}
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css" integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
11
+ {#static#}
12
+ <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
13
+ <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
14
+ {#for python code displaying#}
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
16
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js" integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N" crossorigin="anonymous"></script>
17
+ <script>hljs.highlightAll();</script>
18
+ {#drag design#}
19
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
20
+ {#drag card#}
21
+ <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
22
+ </head>
23
+ <body>
24
+ <nav class="navbar navbar-expand-lg navbar-light bg-light fixed-top">
25
+ <div class= "container">
26
+ {# {{ module_config }}#}
27
+ <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
+ </a>
30
+ <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
+ <span class="navbar-toggler-icon"></span>
32
+ </button>
33
+
34
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
35
+ <ul class="navbar-nav mr-auto">
36
+ <li class="nav-item">
37
+ <a class="nav-link" href="{{ url_for('main.index') }}" aria-current="page">Home</a>
38
+ </li>
39
+ {% if enable_design %}
40
+ <li class="nav-item">
41
+ <a class="nav-link" href="{{ url_for('library.load_from_database') }}" aria-current="page">Library</a>
42
+ </li>
43
+ <li class="nav-item">
44
+ <a class="nav-link" href="{{ url_for('design.experiment_builder') }}">Design</a>
45
+ </li>
46
+ <li class="nav-item">
47
+ <a class="nav-link" href="{{ url_for('execute.experiment_run') }}">Compile/Run</a>
48
+ </li>
49
+ <li class="nav-item">
50
+ <a class="nav-link" href="{{ url_for('data.list_workflows') }}">Data</a>
51
+ </li>
52
+ {% endif %}
53
+
54
+ <li class="nav-item">
55
+ <a class="nav-link" href="{{ url_for('control.deck_controllers') }}">Devices</a></li>
56
+ </li>
57
+ {# <li class="nav-item">#}
58
+ {# <a class="nav-link" href="{{ url_for('control.controllers_home') }}">Temp Devices</a></li>#}
59
+ {# </li>#}
60
+ {# <li class="nav-item">#}
61
+ {# <a class="nav-link" href="{{ url_for('main.help_info') }}">About</a>#}
62
+ {# </li>#}
63
+ {% if plugins %}
64
+ {% for plugin in plugins %}
65
+ <li class="nav-item">
66
+ <a class="nav-link" href="{{ url_for(plugin+'.main') }}">{{ plugin.capitalize() }}</a></li>
67
+ </li>
68
+ {% endfor %}
69
+ {% endif %}
70
+ </ul>
71
+ <ul class="navbar-nav ms-auto">
72
+ {# {{ current_user }}#}
73
+ {% if current_user.is_authenticated %}
74
+ <div class="dropdown">
75
+ <li class="nav-item " aria-expanded="false"><i class="bi bi-person-circle"></i> {{ current_user.get_id() }}</li>
76
+ <ul class="dropdown-menu">
77
+ <li><a class="dropdown-item" href="{{ url_for("auth.logout") }}" role="button" aria-expanded="false">Logout</a></li>
78
+ </ul>
79
+
80
+ </div>
81
+ {% else %}
82
+ <li class="nav-item">
83
+ <a class="nav-link" href="{{ url_for("auth.login") }}">Login</a>
84
+ </li>
85
+ {% endif %}
86
+ {# <li class="nav-item">#}
87
+ {# <a class="nav-link"href="{{ url_for("signup") }}">Signup</a>#}
88
+ {# </li>#}
89
+ </ul>
90
+ </div>
91
+ </div>
92
+ </nav>
93
+
94
+ <div class= "container">
95
+ <div class="flash">
96
+ {% with messages = get_flashed_messages() %}
97
+ {% if messages %}
98
+ <div class="alert alert-warning">
99
+ Message:
100
+ {% for message in messages %}
101
+ <div >
102
+ {{ message|safe }}
103
+ </div>
104
+ {% endfor %}
105
+ </div>
106
+ {% endif %}
107
+ {% endwith %}
108
+ </div>
109
+ {% block body %}{% endblock %}
110
+ </div>
111
+
112
+ <div class="modal fade" id="importModal" tabindex="-1" aria-labelledby="importModal" aria-hidden="true" >
113
+ <div class="modal-dialog">
114
+ <div class="modal-content">
115
+ <div class="modal-header">
116
+ <h1 class="modal-title fs-5" id="importModal">Import deck by file path</h1>
117
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
118
+ </div>
119
+ <form method="POST" action="{{ url_for('control.temp.import_deck') }}" enctype="multipart/form-data">
120
+ <div class="modal-body">
121
+ <h5>from connection history</h5>
122
+ <div class="form-group">
123
+ <select class="form-select" name="filepath">
124
+ <option disabled selected value> -- select an option -- </option>
125
+ {% for connection in history %}
126
+ <option style="overflow-wrap: break-word;" name="filepath" id="filepath" value="{{connection}}">{{connection}}</option>
127
+ {% endfor %}
128
+ {# <option>clear history</option>#}
129
+ </select>
130
+ </div>
131
+ <h5>input manually</h5>
132
+ <div class="input-group mb-3">
133
+ <label class="input-group-text" for="filepath">File Path:</label>
134
+ <input type="text" class="form-control" name="filepath" id="filepath">
135
+ </div>
136
+ <div class="input-group mb-3">
137
+ <div class="form-check">
138
+ <input type="checkbox" class="form-check-input" id="update" name="update" value="update">
139
+ <label class="form-check-label" for="update">Update editor config</label>
140
+ </div>
141
+ </div>
142
+
143
+ <div class="modal-footer">
144
+ <div class="form-check">
145
+ <input type="checkbox" class="form-check-input" id="dismiss" name="dismiss" value="dismiss">
146
+ <label class="form-check-label" for="dismiss">Don't remind me</label>
147
+ </div>
148
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
149
+ <button type="submit" class="btn btn-primary"> Save </button>
150
+ </div>
151
+ </div>
152
+ </form>
153
+ </div>
154
+ </div>
155
+ </div>
156
+ </body>
157
+ </html>
ivoryos/utils/form.py CHANGED
@@ -1,5 +1,10 @@
1
1
  from enum import Enum
2
- from typing import get_origin, get_args, Union, Any
2
+ from typing import Union, Any
3
+ try:
4
+ from typing import get_origin, get_args
5
+ except ImportError:
6
+ # For Python versions = 3.7, use typing_extensions
7
+ from typing_extensions import get_origin, get_args
3
8
 
4
9
  from wtforms.fields.choices import SelectField
5
10
  from wtforms.fields.core import Field
@@ -188,44 +188,52 @@ class ScriptRunner:
188
188
  # _func_str = script.compile()
189
189
  # step_list_dict: dict = script.convert_to_lines(_func_str)
190
190
  self._emit_progress(socketio, 1)
191
-
192
- # Run "prep" section once
193
- script_dict = script.script_dict
194
- with current_app.app_context():
195
-
196
- run = WorkflowRun(name=script.name or "untitled", platform=script.deck or "deck",start_time=datetime.now())
197
- db.session.add(run)
198
- db.session.commit()
199
- run_id = run.id # Save the ID
200
- global_config.runner_status = {"id":run_id, "type": "workflow"}
201
- self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id)
202
- output_list = []
203
- _, arg_type = script.config("script")
204
- _, return_list = script.config_return()
205
-
206
- # Run "script" section multiple times
207
- if repeat_count:
208
- self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
209
- run_name, return_list, compiled, logger, socketio,
210
- history, output_path, run_id=run_id, optimizer=optimizer)
211
- elif config:
212
- self._run_config_section(config, arg_type, output_list, script, run_name, logger,
213
- socketio, run_id=run_id, compiled=compiled)
214
-
215
- # Run "cleanup" section once
216
- self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
217
- # Reset the running flag when done
191
+ filename = None
192
+ error_flag = False
193
+ # create a new run entry in the database
194
+ try:
195
+ with current_app.app_context():
196
+ run = WorkflowRun(name=script.name or "untitled", platform=script.deck or "deck",start_time=datetime.now())
197
+ db.session.add(run)
198
+ db.session.commit()
199
+ run_id = run.id # Save the ID
200
+ global_config.runner_status = {"id":run_id, "type": "workflow"}
201
+
202
+ # Run "prep" section once
203
+ self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id)
204
+ output_list = []
205
+ _, arg_type = script.config("script")
206
+ _, return_list = script.config_return()
207
+ # Run "script" section multiple times
208
+ if repeat_count:
209
+ self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
210
+ run_name, return_list, compiled, logger, socketio,
211
+ history, output_path, run_id=run_id, optimizer=optimizer)
212
+ elif config:
213
+ self._run_config_section(config, arg_type, output_list, script, run_name, logger,
214
+ socketio, run_id=run_id, compiled=compiled)
215
+ # Run "cleanup" section once
216
+ self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
217
+ # Reset the running flag when done
218
+
219
+ # Save results if necessary
220
+
221
+ if not script.python_script and output_list:
222
+ filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
223
+ self._emit_progress(socketio, 100)
224
+
225
+ except Exception as e:
226
+ logger.error(f"Error during script execution: {e.__str__()}")
227
+ error_flag = True
228
+ finally:
218
229
  self.lock.release()
219
- # Save results if necessary
220
- filename = None
221
- if not script.python_script and output_list:
222
- filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
223
- self._emit_progress(socketio, 100)
224
- with current_app.app_context():
225
- run = db.session.get(WorkflowRun, run_id) # SQLAlchemy 1.4+ recommended method
226
- run.end_time = datetime.now()
227
- run.data_path = filename
228
- db.session.commit()
230
+ with current_app.app_context():
231
+ run = db.session.get(WorkflowRun, run_id)
232
+ run.end_time = datetime.now()
233
+ run.output_file = filename
234
+ run.run_error = error_flag
235
+ db.session.commit()
236
+
229
237
 
230
238
  def _run_actions(self, script, section_name="", logger=None, socketio=None, run_id=None):
231
239
  _func_str = script.python_script or script.compile()
@@ -254,7 +262,7 @@ class ScriptRunner:
254
262
  logger.info(f'Stopping execution during {run_name}: {i + 1}/{len(config)}')
255
263
  break
256
264
  logger.info(f'Executing {i + 1} of {len(config)} with kwargs = {kwargs}')
257
- progress = (i + 1) * 100 / len(config)
265
+ progress = ((i + 1) * 100 / len(config)) - 0.1
258
266
  self._emit_progress(socketio, progress)
259
267
  # fname = f"{run_name}_script"
260
268
  # function = self.globals_dict[fname]
ivoryos/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.2.2"
1
+ __version__ = "1.2.4"