Completed
Push — master ( 2d2260...b25832 )
by Olivier
46s
created

QtHandler.emit()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
rs 10
cc 1
1
#! /usr/bin/env python3
2
3
import sys
4
import os
5
import inspect
6
import logging
7
8
from PyQt5.QtCore import QTimer, QSettings, QModelIndex, Qt, QCoreApplication
9
from PyQt5.QtGui import QIcon, QFont
10
from PyQt5.QtWidgets import QMainWindow, QApplication, QFileDialog, QMessageBox, QStyledItemDelegate, QMenu
11
12
from opcua import ua
13
from opcua import Server
14
from opcua import copy_node
15
from opcua.common.ua_utils import get_node_children
16
from opcua.common.xmlexporter import XmlExporter
17
from opcua.common.instantiate import instantiate
18
19
from uawidgets import resources
20
from uawidgets.attrs_widget import AttrsWidget
21
from uawidgets.tree_widget import TreeWidget
22
from uawidgets.refs_widget import RefsWidget
23
from uawidgets.new_node_dialogs import NewNodeBaseDialog, NewUaObjectDialog, NewUaVariableDialog, NewUaMethodDialog
24
from uamodeler.uamodeler_ui import Ui_UaModeler
25
from uamodeler.namespace_widget import NamespaceWidget
26
from uamodeler.refnodesets_widget import RefNodeSetsWidget
27
28
29
logger = logging.getLogger(__name__)
30
31
32
class QtHandler(logging.Handler):
33
34
    def __init__(self, modeler):
35
        logging.Handler.__init__(self)
36
        self.modeler = modeler
37
38
    def emit(self, record):
39
        print("HANDLER emit", record)
40
        msg = self.format(record)
41
        self.modeler.ui.logTextEdit.append(msg)
42
43
44
class BoldDelegate(QStyledItemDelegate):
45
46
    def __init__(self, parent, model, added_node_list):
47
        QStyledItemDelegate.__init__(self, parent)
48
        self.added_node_list = added_node_list
49
        self.model = model
50
51
    def paint(self, painter, option, idx):
52
        new_idx = idx.sibling(idx.row(), 0)
53
        item = self.model.itemFromIndex(new_idx)
54
        if item and item.data() in self.added_node_list:
55
            option.font.setWeight(QFont.Bold)
56
        QStyledItemDelegate.paint(self, painter, option, idx)
57
58
59
def trycatchslot(func):
60
    def wrapper(self, *args):
61
        # filter out excess args as qt signals do
62
        sig = inspect.signature(func)
63
        args = args[:(len(sig.parameters)-1)]
64
        result = None
65
        try:
66
            result = func(self, *args)
67
        except Exception as ex:
68
            logger.exception(ex)
69
            self.show_error(ex)
70
        return result
71
    return wrapper
72
73
74
class UaModeler(QMainWindow):
75
76
    def __init__(self):
77
        QMainWindow.__init__(self)
78
        self.ui = Ui_UaModeler()
79
        self.ui.setupUi(self)
80
        self.setWindowIcon(QIcon(":/network.svg"))
81
82
        # we only show statusbar in case of errors
83
        self.ui.statusBar.hide()
84
85
        # setup QSettings for application and get a settings object
86
        QCoreApplication.setOrganizationName("FreeOpcUa")
87
        QCoreApplication.setApplicationName("OpcUaModeler")
88
        self.settings = QSettings()
89
        self._last_dir = self.settings.value("last_dir", ".")
90
91
        self._restore_state()
92
93
        self.server = None
94
        self._new_nodes = []  # the added nodes we will save
95
        self._current_path = None
96
        self._modified = False
97
        self._copy_clipboard = None
98
99
        self.tree_ui = TreeWidget(self.ui.treeView)
100
        self.tree_ui.error.connect(self.show_error)
101
        delegate = BoldDelegate(self, self.tree_ui.model, self._new_nodes)
102
        self.ui.treeView.setItemDelegate(delegate)
103
        self.ui.treeView.selectionModel().currentChanged.connect(self._update_actions_state)
104
105
        self.refs_ui = RefsWidget(self.ui.refView)
106
        self.refs_ui.error.connect(self.show_error)
107
        self.attrs_ui = AttrsWidget(self.ui.attrView, show_timestamps=False)
108
        self.attrs_ui.error.connect(self.show_error)
109
        self.attrs_ui.attr_written.connect(self._attr_written)
110
        self.idx_ui = NamespaceWidget(self.ui.namespaceView)
111
        self.nodesets_ui = RefNodeSetsWidget(self.ui.refNodeSetsView)
112
        self.nodesets_ui.error.connect(self.show_error)
113
        self.nodesets_ui.nodeset_added.connect(self.nodesets_change)
114
        self.nodesets_ui.nodeset_removed.connect(self.nodesets_change)
115
116
        self.ui.treeView.activated.connect(self.show_refs)
117
        self.ui.treeView.clicked.connect(self.show_refs)
118
        self.ui.treeView.activated.connect(self.show_attrs)
119
        self.ui.treeView.clicked.connect(self.show_attrs)
120
121
        # fix icon stuff
122
        self.ui.actionNew.setIcon(QIcon(":/new.svg"))
123
        self.ui.actionOpen.setIcon(QIcon(":/open.svg"))
124
        self.ui.actionCopy.setIcon(QIcon(":/copy.svg"))
125
        self.ui.actionPaste.setIcon(QIcon(":/paste.svg"))
126
        self.ui.actionDelete.setIcon(QIcon(":/delete.svg"))
127
        self.ui.actionSave.setIcon(QIcon(":/save.svg"))
128
        self.ui.actionAddFolder.setIcon(QIcon(":/folder.svg"))
129
        self.ui.actionAddObject.setIcon(QIcon(":/object.svg"))
130
        self.ui.actionAddMethod.setIcon(QIcon(":/method.svg"))
131
        self.ui.actionAddObjectType.setIcon(QIcon(":/object_type.svg"))
132
        self.ui.actionAddProperty.setIcon(QIcon(":/property.svg"))
133
        self.ui.actionAddVariable.setIcon(QIcon(":/variable.svg"))
134
        self.ui.actionAddVariableType.setIcon(QIcon(":/variable_type.svg"))
135
        self.ui.actionAddDataType.setIcon(QIcon(":/data_type.svg"))
136
        self.ui.actionAddReferenceType.setIcon(QIcon(":/reference_type.svg"))
137
138
        self.setup_context_menu_tree()
139
140
        # actions
141
        self.ui.actionNew.triggered.connect(self._new)
142
        self.ui.actionOpen.triggered.connect(self._open)
143
        self.ui.actionCopy.triggered.connect(self._copy)
144
        self.ui.actionPaste.triggered.connect(self._paste)
145
        self.ui.actionDelete.triggered.connect(self._delete)
146
        self.ui.actionImport.triggered.connect(self._import_slot)
147
        self.ui.actionSave.triggered.connect(self._save_slot)
148
        self.ui.actionSaveAs.triggered.connect(self._save_as)
149
        self.ui.actionCloseModel.triggered.connect(self._close_model_slot)
150
        self.ui.actionAddObjectType.triggered.connect(self._add_object_type)
151
        self.ui.actionAddObject.triggered.connect(self._add_object)
152
        self.ui.actionAddFolder.triggered.connect(self._add_folder)
153
        self.ui.actionAddMethod.triggered.connect(self._add_method)
154
        self.ui.actionAddDataType.triggered.connect(self._add_data_type)
155
        self.ui.actionAddVariable.triggered.connect(self._add_variable)
156
        self.ui.actionAddVariableType.triggered.connect(self._add_variable_type)
157
        self.ui.actionAddProperty.triggered.connect(self._add_property)
158
159
        self._disable_actions()
160
161
    def _update_actions_state(self, current, previous):
162
        node = self.tree_ui.get_current_node(current)
163
        if not node or node == self.server.nodes.root:
164
            self._disable_add_actions()
165
            return
166
        path = node.get_path()
167
        nodeclass = node.get_node_class()
168
169
        if self.server.nodes.base_object_type in path:
170
            self.ui.actionAddObjectType.setEnabled(True)
171
        else:
172
            self.ui.actionAddObjectType.setEnabled(False)
173
174
        if self.server.nodes.base_variable_type in path:
175
            self.ui.actionAddVariableType.setEnabled(True)
176
        else:
177
            self.ui.actionAddVariableType.setEnabled(False)
178
179
        if self.server.nodes.base_data_type in path:
180
            self.ui.actionAddDataType.setEnabled(True)
181
            return  # not other nodes should be added here
182
        else:
183
            self.ui.actionAddDataType.setEnabled(False)
184
185
        if nodeclass != ua.NodeClass.Variable:
186
            self.ui.actionAddFolder.setEnabled(True)
187
            self.ui.actionAddObject.setEnabled(True)
188
            self.ui.actionAddVariable.setEnabled(True)
189
            self.ui.actionAddProperty.setEnabled(True)
190
            self.ui.actionAddMethod.setEnabled(True)
191
        else:
192
            self.ui.actionAddFolder.setEnabled(False)
193
            self.ui.actionAddObject.setEnabled(False)
194
            self.ui.actionAddVariable.setEnabled(False)
195
            self.ui.actionAddProperty.setEnabled(False)
196
            self.ui.actionAddMethod.setEnabled(False)
197
198
    def setup_context_menu_tree(self):
199
        self.ui.treeView.setContextMenuPolicy(Qt.CustomContextMenu)
200
        self.ui.treeView.customContextMenuRequested.connect(self._show_context_menu_tree)
201
        self._contextMenu = QMenu()
202
203
        # tree view menu
204
        self._contextMenu.addAction(self.ui.actionCopy)
205
        self._contextMenu.addAction(self.ui.actionPaste)
206
        self._contextMenu.addAction(self.ui.actionDelete)
207
        self._contextMenu.addSeparator()
208
        self._contextMenu.addAction(self.ui.actionAddFolder)
209
        self._contextMenu.addAction(self.ui.actionAddObject)
210
        self._contextMenu.addAction(self.ui.actionAddVariable)
211
        self._contextMenu.addAction(self.ui.actionAddProperty)
212
        self._contextMenu.addAction(self.ui.actionAddMethod)
213
        self._contextMenu.addAction(self.ui.actionAddObjectType)
214
        self._contextMenu.addAction(self.ui.actionAddVariableType)
215
        self._contextMenu.addAction(self.ui.actionAddDataType)
216
217
    def _show_context_menu_tree(self, position):
218
        node = self.tree_ui.get_current_node()
219
        if node:
220
            self._contextMenu.exec_(self.ui.treeView.viewport().mapToGlobal(position))
221
222
    @trycatchslot
223
    def _delete(self):
224
        node = self.tree_ui.get_current_node()
225
        if node:
226
            nodes = get_node_children(node)
227
            for n in nodes:
228
                n.delete()
229
                if n in self._new_nodes:
230
                    self._new_nodes.remove(n)
231
            self.tree_ui.remove_current_item()
232
233
    @trycatchslot
234
    def _copy(self):
235
        node = self.tree_ui.get_current_node()
236
        if node:
237
            self._copy_clipboard = node
238
239
    @trycatchslot
240
    def _paste(self):
241
        if self._copy_clipboard:
242
            parent = self.tree_ui.get_current_node()
243
            try:
244
                added_nodes = copy_node(parent, self._copy_clipboard)
245
            except Exception as ex:
246
                self.show_error(ex)
247
                raise
248
            self._new_nodes.extend(added_nodes)
249
            self.tree_ui.reload_current()
250
            self.show_refs()
251
            self._modified = True
252
253
    def _attr_written(self, attr, dv):
254
        self._modified = True
255
        if attr == ua.AttributeIds.BrowseName:
256
            self.tree_ui.update_browse_name_current_item(dv.Value.Value)
257
        elif attr == ua.AttributeIds.DisplayName:
258
            self.tree_ui.update_display_name_current_item(dv.Value.Value)
259
      
260
    def _restore_state(self):
261
        self.resize(int(self.settings.value("main_window_width", 800)),
262
                    int(self.settings.value("main_window_height", 600)))
263
        #self.restoreState(self.settings.value("main_window_state", b"", type="QByteArray"))
264
        self.restoreState(self.settings.value("main_window_state", bytearray()))
265
        self.ui.splitterLeft.restoreState(self.settings.value("splitter_left", bytearray()))
266
        self.ui.splitterRight.restoreState(self.settings.value("splitter_right", bytearray()))
267
        self.ui.splitterCenter.restoreState(self.settings.value("splitter_center", bytearray()))
268
269
    def _disable_model_actions(self):
270
        self.ui.actionImport.setEnabled(False)
271
        self.ui.actionSave.setEnabled(False)
272
        self.ui.actionSaveAs.setEnabled(False)
273
274
    def _disable_actions(self):
275
        self._disable_add_actions()
276
        self._disable_model_actions()
277
278
    def _disable_add_actions(self):
279
        self.ui.actionAddObject.setEnabled(False)
280
        self.ui.actionAddFolder.setEnabled(False)
281
        self.ui.actionAddVariable.setEnabled(False)
282
        self.ui.actionAddProperty.setEnabled(False)
283
        self.ui.actionAddDataType.setEnabled(False)
284
        self.ui.actionAddVariableType.setEnabled(False)
285
        self.ui.actionAddObjectType.setEnabled(False)
286
        self.ui.actionAddMethod.setEnabled(False)
287
288
    def _enable_model_actions(self):
289
        self.ui.actionImport.setEnabled(True)
290
        self.ui.actionSave.setEnabled(True)
291
        self.ui.actionSaveAs.setEnabled(True)
292
        #self.ui.actionAddObject.setEnabled(True)
293
        #self.ui.actionAddFolder.setEnabled(True)
294
        #self.ui.actionAddVariable.setEnabled(True)
295
        #self.ui.actionAddProperty.setEnabled(True)
296
        #self.ui.actionAddDataType.setEnabled(True)
297
        #self.ui.actionAddVariableType.setEnabled(True)
298
        #self.ui.actionAddObjectType.setEnabled(True)
299
300
    @trycatchslot
301
    def _close_model_slot(self):
302
        self._close_model()
303
304
    def _close_model(self):
305
        if not self.really_exit():
306
            return False
307
        self._disable_actions()
308
        self.tree_ui.clear()
309
        self.refs_ui.clear()
310
        self.attrs_ui.clear()
311
        self.idx_ui.clear()
312
        self.nodesets_ui.clear()
313
        self._current_path = None
314
        self._modified = False
315
        self._update_title()
316
        if self.server is not None:
317
            self.server.stop()
318
        self.server = None
319
        return True
320
321
    def _update_title(self):
322
        self.setWindowTitle("FreeOpcUa Modeler " + str(self._current_path))
323
324
    @trycatchslot
325
    def _new(self):
326
        if not self._close_model():
327
            return
328
        self._create_new_model()
329
330
    def _create_new_model(self):
331
        self.server = Server()
332
        endpoint = "opc.tcp://0.0.0.0:48400/freeopcua/uamodeler/"
333
        print("Starting server on ", endpoint)
334
        self.server.set_endpoint(endpoint)
335
        self.server.set_server_name("OpcUa Modeler Server")
336
        # now remove freeopcua namespace, not necessary when modeling and
337
        # ensures correct idx for exported nodesets
338
        ns_node = self.server.get_node(ua.NodeId(ua.ObjectIds.Server_NamespaceArray))
339
        nss = ns_node.get_value()
340
        ns_node.set_value(nss[1:])
341
342
        del(self._new_nodes[:])  # empty list while keeping reference
343
344
        self.server.start()
345
        self.tree_ui.set_root_node(self.server.get_root_node())
346
        self.idx_ui.set_node(self.server.get_node(ua.ObjectIds.Server_NamespaceArray))
347
        self.nodesets_ui.set_server(self.server)
348
        self._modified = False
349
        self._enable_model_actions()
350
        self._current_path = "NoName"
351
        self._update_title()
352
        return True
353
354
    @trycatchslot
355
    def _import_slot(self):
356
        self._import()
357
358
    def _import(self, path=None):
359
        if not path:
360
            path, ok = self._get_xml()
361
            if not ok:
362
                return None
363
        self._last_dir = os.path.dirname(path)
364
        try:
365
            new_nodes = self.server.import_xml(path)
366
            self._new_nodes.extend([self.server.get_node(node) for node in new_nodes])
367
            self._modified = True
368
        except Exception as ex:
369
            self.show_error(ex)
370
            raise
371
        # we maybe should only reload the imported nodes
372
        self.tree_ui.reload()
373
        self.idx_ui.reload()
374
        return path
375
376
    def _get_xml(self):
377
        return QFileDialog.getOpenFileName(self, caption="Open OPC UA XML", filter="XML Files (*.xml *.XML)", directory=self._last_dir)
378
379
    @trycatchslot
380
    def _open(self):
381
        if not self._close_model():
382
            return
383
        path, ok = self._get_xml()
384
        if not ok:
385
            return
386
        self._create_new_model()
387
        try:
388
            path = self._import(path)
389
        except:
390
            self._close_model()
391
            raise
392
        self._modified = False
393
        self._current_path = path
394
        self._update_title()
395
396
    @trycatchslot
397
    def _save_as(self):
398
        path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
399
        if ok:
400
            self._current_path = path
401
            self._update_title()
402
            self._save()
403
404
    @trycatchslot
405
    def _save_slot(self):
406
        self._save()
407
408
    def _save(self):
409
        if not self._current_path or self._current_path == "NoName":
410
            path, ok = QFileDialog.getSaveFileName(self, caption="Save OPC UA XML", filter="XML Files (*.xml *.XML)")
411
            self._current_path = path
412
            if not ok:
413
                return
414
        print("Saving to", self._current_path)
415
        print("Exporting  {} nodes: {}".format(len(self._new_nodes), self._new_nodes))
416
        print("and namespaces: ", self.server.get_namespace_array()[1:])
417
        exp = XmlExporter(self.server)
418
        uris = self.server.get_namespace_array()[1:]
419
        exp.build_etree(self._new_nodes, uris=uris)
420
        try:
421
            exp.write_xml(self._current_path)
422
        except Exception as ex:
423
            self.show_error(ex)
424
            raise
425
        self._modified = False
426
        self._update_title()
427
        self.show_msg(self._current_path + " saved")
428
429
    def really_exit(self):
430
        if self._modified:
431
            reply = QMessageBox.question(
432
                self,
433
                "OPC UA Modeler",
434
                "Model is modified, do you really want to close model?",
435
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
436
            )
437
            if reply != QMessageBox.Yes:
438
                return False
439
440
        return True
441
442
    def _after_add(self, new_nodes):
443
        if isinstance(new_nodes, (list, tuple)):
444
            self._new_nodes.extend(new_nodes)
445
        else:
446
            self._new_nodes.append(new_nodes)
447
        self.tree_ui.reload_current()
448
        self.show_refs()
449
        self._modified = True
450
451
    @trycatchslot
452
    def _add_method(self):
453
        parent = self.tree_ui.get_current_node()
454
        args, ok = NewUaMethodDialog.getArgs(self, "Add Method", self.server)
455
        if ok:
456
            new_nodes = []
457
            new_node = parent.add_method(*args)
458
            new_nodes.append(new_node)
459
            new_nodes.extend(new_node.get_children())
460
            self._after_add(new_nodes)
461
462
    @trycatchslot
463
    def _add_object_type(self):
464
        parent = self.tree_ui.get_current_node()
465
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Object Type", self.server)
466
        if ok:
467
            new_node = parent.add_object_type(*args)
468
            self._after_add(new_node)
469
470
    @trycatchslot
471
    def _add_folder(self):
472
        parent = self.tree_ui.get_current_node()
473
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Folder", self.server)
474
        if ok:
475
            new_node = parent.add_folder(*args)
476
            self._after_add(new_node)
477
478
    @trycatchslot
479
    def _add_object(self):
480
        parent = self.tree_ui.get_current_node()
481
        args, ok = NewUaObjectDialog.getArgs(self, "Add Object", self.server, base_node_type=self.server.get_node(ua.ObjectIds.BaseObjectType))
482
        if ok:
483
            nodeid, bname, otype = args
484
            new_nodes = instantiate(parent, otype, bname=bname, nodeid=nodeid)
485
            self._after_add(new_nodes)
486
487
    @trycatchslot
488
    def _add_data_type(self):
489
        parent = self.tree_ui.get_current_node()
490
        args, ok = NewNodeBaseDialog.getArgs(self, "Add Data Type", self.server)
491
        if ok:
492
            new_node = parent.add_data_type(*args)
493
            self._after_add(new_node)
494
    
495
    @trycatchslot
496
    def _add_variable(self):
497
        parent = self.tree_ui.get_current_node()
498
        dtype = self.settings.value("last_datatype", None)
499
        args, ok = NewUaVariableDialog.getArgs(self, "Add Variable", self.server, default_value=9.99, dtype=dtype)
500
        if ok:
501
            self.settings.setValue("last_datatype", args[4])
502
            new_node = parent.add_variable(*args)
503
            self._after_add(new_node)
504
505
    @trycatchslot
506
    def _add_property(self):
507
        parent = self.tree_ui.get_current_node()
508
        dtype = self.settings.value("last_datatype", None)
509
        args, ok = NewUaVariableDialog.getArgs(self, "Add Property", self.server, default_value=9.99, dtype=dtype)
510
        if ok:
511
            self.settings.setValue("last_datatype", args[4])
512
            new_node = parent.add_property(*args)
513
            self._after_add(new_node)
514
515
    @trycatchslot
516
    def _add_variable_type(self):
517
        parent = self.tree_ui.get_current_node()
518
        args, ok = NewUaObjectDialog.getArgs(self, "Add Variable Type", self.server, base_node_type=self.server.get_node(ua.ObjectIds.BaseVariableType))
519
        if ok:
520
            nodeid, bname, datatype = args
521
            new_node = parent.add_variable_type(nodeid, bname, datatype.nodeid)
522
            self._after_add(new_node)
523
524
    @trycatchslot
525
    def show_refs(self, idx=None):
526
        node = self.get_current_node(idx)
527
        self.refs_ui.show_refs(node)
528
529
    @trycatchslot
530
    def show_attrs(self, idx=None):
531
        if not isinstance(idx, QModelIndex):
532
            idx = None
533
        node = self.get_current_node(idx)
534
        self.attrs_ui.show_attrs(node)
535
536
    def show_error(self, msg):
537
        logger.warning("showing error: %s", msg)
538
        self.ui.statusBar.show()
539
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : red; color : black; }")
540
        self.ui.statusBar.showMessage(str(msg))
541
        QTimer.singleShot(2500, self.ui.statusBar.hide)
542
543
    def show_msg(self, msg):
544
        self.ui.statusBar.show()
545
        self.ui.statusBar.setStyleSheet("QStatusBar { background-color : green; color : black; }")
546
        self.ui.statusBar.showMessage(str(msg))
547
        QTimer.singleShot(1500, self.ui.statusBar.hide)
548
549
    def get_current_node(self, idx=None):
550
        return self.tree_ui.get_current_node(idx)
551
    
552
    def nodesets_change(self, data):
553
        self.idx_ui.reload()
554
        self.tree_ui.reload()
555
        self.refs_ui.clear()
556
        self.attrs_ui.clear()
557
558
    def closeEvent(self, event):
559
        if not self._close_model():
560
            event.ignore()
561
            return
562
        self.attrs_ui.save_state()
563
        self.refs_ui.save_state()
564
        self.settings.setValue("last_dir", self._last_dir)
565
        self.settings.setValue("main_window_width", self.size().width())
566
        self.settings.setValue("main_window_height", self.size().height())
567
        self.settings.setValue("main_window_state", self.saveState())
568
        self.settings.setValue("splitter_left", self.ui.splitterLeft.saveState())
569
        self.settings.setValue("splitter_right", self.ui.splitterRight.saveState())
570
        self.settings.setValue("splitter_center", self.ui.splitterCenter.saveState())
571
        if self.server:
572
            self.server.stop()
573
        event.accept()
574
575
576
def main():
577
    app = QApplication(sys.argv)
578
    modeler = UaModeler()
579
    handler = QtHandler(modeler)
580
    handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
581
    logging.getLogger().addHandler(handler)
582
    logging.getLogger().setLevel(logging.INFO)
583
    modeler.show()
584
    sys.exit(app.exec_())
585
586
587
if __name__ == "__main__":
588
    main()
589