unisi 0.1.0__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.
- unisi/__init__.py +7 -0
- unisi/autotest.py +210 -0
- unisi/common.py +28 -0
- unisi/containers.py +97 -0
- unisi/guielements.py +160 -0
- unisi/proxy.py +159 -0
- unisi/reloader.py +150 -0
- unisi/server.py +118 -0
- unisi/tables.py +116 -0
- unisi/users.py +271 -0
- unisi/utils.py +108 -0
- unisi/web/css/169.3abcdd94.css +1 -0
- unisi/web/css/app.31d6cfe0.css +0 -0
- unisi/web/css/vendor.9ed7638d.css +6 -0
- unisi/web/favicon.ico +0 -0
- unisi/web/fonts/KFOkCnqEu92Fr1MmgVxIIzQ.68bb21d0.woff +0 -0
- unisi/web/fonts/KFOlCnqEu92Fr1MmEU9fBBc-.48af7707.woff +0 -0
- unisi/web/fonts/KFOlCnqEu92Fr1MmSU5fBBc-.c2f7ab22.woff +0 -0
- unisi/web/fonts/KFOlCnqEu92Fr1MmWUlfBBc-.77ecb942.woff +0 -0
- unisi/web/fonts/KFOlCnqEu92Fr1MmYUtfBBc-.f5677eb2.woff +0 -0
- unisi/web/fonts/KFOmCnqEu92Fr1Mu4mxM.f1e2a767.woff +0 -0
- unisi/web/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNa.4d73cb90.woff +0 -0
- unisi/web/fonts/flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.c5371cfb.woff2 +0 -0
- unisi/web/icons/favicon-128x128.png +0 -0
- unisi/web/icons/favicon-16x16.png +0 -0
- unisi/web/icons/favicon-32x32.png +0 -0
- unisi/web/icons/favicon-96x96.png +0 -0
- unisi/web/index.html +1 -0
- unisi/web/js/169.f952e294.js +1 -0
- unisi/web/js/193.283445be.js +1 -0
- unisi/web/js/430.591e9a73.js +1 -0
- unisi/web/js/app.ba94719f.js +1 -0
- unisi/web/js/vendor.d6797c01.js +45 -0
- unisi-0.1.0.dist-info/METADATA +422 -0
- unisi-0.1.0.dist-info/RECORD +37 -0
- unisi-0.1.0.dist-info/WHEEL +4 -0
- unisi-0.1.0.dist-info/licenses/LICENSE +201 -0
unisi/__init__.py
ADDED
unisi/autotest.py
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
import config, os, logging
|
2
|
+
from .utils import *
|
3
|
+
from .guielements import *
|
4
|
+
from .containers import Block, Dialog
|
5
|
+
from .users import User
|
6
|
+
from .common import *
|
7
|
+
from jsoncomparison import Compare, NO_DIFF
|
8
|
+
|
9
|
+
#setting config variables
|
10
|
+
testdir = 'autotest'
|
11
|
+
if not hasattr(config, testdir):
|
12
|
+
config.autotest = False
|
13
|
+
if not hasattr(config, 'port'):
|
14
|
+
config.port = 8000
|
15
|
+
if not hasattr(config, 'pretty_print'):
|
16
|
+
config.pretty_print = False
|
17
|
+
if not hasattr(config, 'upload_dir'):
|
18
|
+
config.upload_dir = 'web'
|
19
|
+
if not hasattr(config, 'logfile'):
|
20
|
+
config.logfile = None
|
21
|
+
if not hasattr(config, 'hot_reload'):
|
22
|
+
config.hot_reload = False
|
23
|
+
if not hasattr(config, 'appname'):
|
24
|
+
config.appname = 'Unisi app'
|
25
|
+
if not hasattr(config, 'mirror'):
|
26
|
+
config.mirror = False
|
27
|
+
|
28
|
+
if not os.path.exists(config.upload_dir):
|
29
|
+
os.makedirs(config.upload_dir)
|
30
|
+
|
31
|
+
#start logging
|
32
|
+
format = "%(asctime)s - %(levelname)s - %(message)s"
|
33
|
+
logfile = config.logfile
|
34
|
+
handlers = [logging.FileHandler(logfile), logging.StreamHandler()] if logfile else []
|
35
|
+
logging.basicConfig(level = logging.WARNING, format = format, handlers = handlers)
|
36
|
+
|
37
|
+
comparator = Compare().check
|
38
|
+
|
39
|
+
def jsonString(obj):
|
40
|
+
pretty = config.pretty_print
|
41
|
+
return toJson(obj, 2 if pretty else 0, pretty)
|
42
|
+
|
43
|
+
class Recorder:
|
44
|
+
def __init__(self):
|
45
|
+
self.start(None)
|
46
|
+
|
47
|
+
def accept(self, msg, response):
|
48
|
+
if self.ignored_1message:
|
49
|
+
self.record_buffer.append(f"{jsonString(msg)},\
|
50
|
+
\n{'null' if response is None else jsonString(response)}\n")
|
51
|
+
else: #start for setting screen
|
52
|
+
self.record_buffer.append(jsonString(ArgObject(block = 'root',
|
53
|
+
element = None, value = User.last_user.screen_module.name)))
|
54
|
+
self.ignored_1message = True
|
55
|
+
|
56
|
+
def stop_recording(self, _, x):
|
57
|
+
button.spinner = None
|
58
|
+
button.changed = button_clicked
|
59
|
+
button.tooltip = 'Create autotest'
|
60
|
+
full = len(self.record_buffer) > 1
|
61
|
+
if full:
|
62
|
+
with open(self.record_file, mode='w') as file:
|
63
|
+
content = ',\n'.join(self.record_buffer)
|
64
|
+
file.write(f"[\n{content}]")
|
65
|
+
test_name = self.record_file
|
66
|
+
self.record_file = None
|
67
|
+
|
68
|
+
return Info(f'Test {test_name} is created.', button) if full else\
|
69
|
+
Warning('Nothing to save!',button)
|
70
|
+
|
71
|
+
def start(self,fname):
|
72
|
+
self.record_file = fname
|
73
|
+
self.ignored_1message = False
|
74
|
+
self.record_buffer = []
|
75
|
+
|
76
|
+
recorder = Recorder()
|
77
|
+
|
78
|
+
def obj2pyjson(obj):
|
79
|
+
return json.loads(jsonpickle.encode(obj,unpicklable=False))
|
80
|
+
|
81
|
+
def test(filename, user):
|
82
|
+
filepath = f'{testdir}{divpath}{filename}'
|
83
|
+
file = open(filepath, "r")
|
84
|
+
data = json.loads(file.read())
|
85
|
+
error = False
|
86
|
+
for message in data:
|
87
|
+
if message is not None and message.get('block'):
|
88
|
+
result = user.result4message(ReceivedMessage(message))
|
89
|
+
response = user.prepare_result(result)
|
90
|
+
user_message = message
|
91
|
+
else:
|
92
|
+
diff = comparator(message, obj2pyjson(response))
|
93
|
+
if diff != NO_DIFF:
|
94
|
+
print(f"\nTest {filename} is failed on message {user_message}:")
|
95
|
+
err = diff.get('_message')
|
96
|
+
if err:
|
97
|
+
print(f" {err}")
|
98
|
+
else:
|
99
|
+
for value in diff.values():
|
100
|
+
err = value['_message']
|
101
|
+
print(f" {err}")
|
102
|
+
error = True
|
103
|
+
return not error
|
104
|
+
|
105
|
+
test_name = Edit('Name test file', '', focus = True)
|
106
|
+
rewrite = Switch('Overwrite existing', False, type = 'check')
|
107
|
+
|
108
|
+
def button_clicked(_,__):
|
109
|
+
test_name.value = ''
|
110
|
+
test_name.complete = smart_complete(os.listdir(testdir))
|
111
|
+
return Dialog('Create autotest..', ask_create_test, test_name, rewrite)
|
112
|
+
|
113
|
+
def create_test(fname):
|
114
|
+
fname = f'{testdir}{divpath}{fname}'
|
115
|
+
if os.path.exists(fname) and not rewrite.value:
|
116
|
+
return Warning(f'Test file {fname} already exists!')
|
117
|
+
|
118
|
+
button.spinner = True
|
119
|
+
button.tooltip = 'Stop test recording'
|
120
|
+
button.changed = recorder.stop_recording
|
121
|
+
recorder.start(fname)
|
122
|
+
|
123
|
+
return Info('Test is recording.. press the same button to stop',button)
|
124
|
+
|
125
|
+
def ask_create_test(_, bname):
|
126
|
+
if bname == 'Ok':
|
127
|
+
return create_test(test_name.value) if test_name.value else\
|
128
|
+
Warning('Test file name is not defined!')
|
129
|
+
|
130
|
+
button = Button('_Add test', button_clicked, right = True,
|
131
|
+
icon='format_list_bulleted_add', tooltip='Create autotest')
|
132
|
+
|
133
|
+
def check_block(self):
|
134
|
+
errors = []
|
135
|
+
child_names = set()
|
136
|
+
|
137
|
+
if not hasattr(self, 'name') or not self.name:
|
138
|
+
errors.append(f"The block with {[str(type(gui)).split('.')[-1] for gui in flatten(self.value)]} does not contain name!")
|
139
|
+
self.name = 'Unknown'
|
140
|
+
if not isinstance(self.name, str):
|
141
|
+
errors.append(f"The block with name {self.name} is not a string!")
|
142
|
+
for child in flatten(self.value):
|
143
|
+
if not isinstance(child, Gui) or not child:
|
144
|
+
errors.append(f'The block {self.name} contains invalid element {child} instead of Gui+ object!')
|
145
|
+
elif isinstance(child, Block):
|
146
|
+
errors.append(f'The block {self.name} contains block {child.name}. Blocks cannot contain blocks!')
|
147
|
+
elif child.name in child_names and child.type != 'line':
|
148
|
+
errors.append(f'The block {self.name} contains a duplicated element name "{child.name}"!')
|
149
|
+
elif child.type == 'chart' and not hasattr(child, 'view'):
|
150
|
+
errors.append(f'The block {self.name} contains a chart type "{child.name}", but not "view" option!')
|
151
|
+
else:
|
152
|
+
child_names.add(child.name)
|
153
|
+
return errors
|
154
|
+
|
155
|
+
def check_screen(module):
|
156
|
+
self = module.screen
|
157
|
+
errors = []
|
158
|
+
block_names = set()
|
159
|
+
if not hasattr(self, 'name') or not self.name:
|
160
|
+
errors.append(f"Screen file {module.__file__} does not contain name!")
|
161
|
+
self.name = 'Unknown'
|
162
|
+
elif not isinstance(self.name, str):
|
163
|
+
errors.append(f"The name in screen file {module.__file__} {self.name} is not a string!")
|
164
|
+
if not isinstance(self.blocks, list):
|
165
|
+
errors.append(f"Screen file {module.__file__} does not contain 'blocks' list!")
|
166
|
+
else:
|
167
|
+
for bl in flatten(self.blocks):
|
168
|
+
if not isinstance(bl, Block):
|
169
|
+
errors.append(f'The screen contains invalid element {bl} instead of Block object!')
|
170
|
+
elif bl.name in block_names:
|
171
|
+
errors.append(f'The screen contains a duplicated block name {bl.name}!')
|
172
|
+
else:
|
173
|
+
block_names.add(bl.name)
|
174
|
+
errors += check_block(bl)
|
175
|
+
if errors:
|
176
|
+
errors.insert(0, f"\nErrors in screen {self.name}, file name {module.__file__}:")
|
177
|
+
return errors
|
178
|
+
|
179
|
+
def run_tests():
|
180
|
+
if not os.path.exists(testdir):
|
181
|
+
os.makedirs(testdir)
|
182
|
+
user = User.UserType()
|
183
|
+
user.load()
|
184
|
+
user.session = 'autotest'
|
185
|
+
errors = []
|
186
|
+
for module in user.screens:
|
187
|
+
errors += check_screen(module)
|
188
|
+
if errors:
|
189
|
+
errors.insert(0, f'\n!!----Unisi detected errors in screens:')
|
190
|
+
print('\n'.join(errors), '\n')
|
191
|
+
elif user.screens:
|
192
|
+
print(f'\n----The screen definitions are correct.-----\n')
|
193
|
+
|
194
|
+
files = config.autotest
|
195
|
+
ok = True
|
196
|
+
process = False
|
197
|
+
if os.path.exists(testdir):
|
198
|
+
for file in os.listdir(testdir):
|
199
|
+
if not os.path.isdir(file) and (files == '*' or file in files):
|
200
|
+
process = True
|
201
|
+
if not test(file,user):
|
202
|
+
ok = False
|
203
|
+
if process and ok:
|
204
|
+
print('\n-----Autotests successfully passed.-----\n')
|
205
|
+
User.last_user = None
|
206
|
+
User.toolbar.append(button)
|
207
|
+
|
208
|
+
|
209
|
+
|
210
|
+
|
unisi/common.py
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
import json, jsonpickle
|
2
|
+
|
3
|
+
def flatten(*arr):
|
4
|
+
for a in arr:
|
5
|
+
if isinstance(a, list):
|
6
|
+
yield from flatten(*a)
|
7
|
+
else:
|
8
|
+
yield a
|
9
|
+
|
10
|
+
class ArgObject:
|
11
|
+
def __init__(self, **kwargs):
|
12
|
+
for key, value in kwargs.items():
|
13
|
+
setattr(self, key, value)
|
14
|
+
|
15
|
+
class ReceivedMessage:
|
16
|
+
def __init__(self, data):
|
17
|
+
self.screen = data.get('screen')
|
18
|
+
self.block = data.get('block')
|
19
|
+
self.element = data.get('element')
|
20
|
+
self.event = data.get('event')
|
21
|
+
self.value = data.get('value')
|
22
|
+
|
23
|
+
def toJson(obj, indent = 0, pretty = False):
|
24
|
+
js = jsonpickle.encode(obj,unpicklable=False)
|
25
|
+
return json.dumps(json.loads(js), indent=indent, sort_keys=pretty) if pretty else js
|
26
|
+
|
27
|
+
|
28
|
+
|
unisi/containers.py
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
from .guielements import Gui, Range, Edit, Switch, Select
|
2
|
+
|
3
|
+
class ContentScaler(Range):
|
4
|
+
def __init__(self, *args, **kwargs):
|
5
|
+
name = args[0] if args else 'Scale content'
|
6
|
+
super().__init__(name, *args, **kwargs)
|
7
|
+
if 'options' not in kwargs:
|
8
|
+
self.options = [0.25, 3.0, 0.25]
|
9
|
+
self.changed = self.scaler
|
10
|
+
|
11
|
+
def scaler(self, _, val):
|
12
|
+
prev = self.value
|
13
|
+
elements = self.elements()
|
14
|
+
self.value = val
|
15
|
+
if elements:
|
16
|
+
prev /= val
|
17
|
+
for element in elements:
|
18
|
+
element.width /= prev
|
19
|
+
element.height /= prev
|
20
|
+
return elements
|
21
|
+
|
22
|
+
class Block(Gui):
|
23
|
+
def __init__(self, name, *elems, **options):
|
24
|
+
self.name = name
|
25
|
+
self.type = 'block'
|
26
|
+
self.value = list(elems)
|
27
|
+
self.add(options)
|
28
|
+
if hasattr(self,'scaler'):
|
29
|
+
scaler = ContentScaler(elements = lambda: self.scroll_list)
|
30
|
+
self.scaler = scaler
|
31
|
+
if not self.value:
|
32
|
+
self.value = [[scaler]]
|
33
|
+
elif isinstance(self.value[0], list):
|
34
|
+
self.value[0].append(scaler)
|
35
|
+
else:
|
36
|
+
self.value[0] = [self.value, scaler]
|
37
|
+
@property
|
38
|
+
def scroll_list(self):
|
39
|
+
return self.value[1] if len(self.value) > 1 and isinstance(self.value[1], (list, tuple)) else []
|
40
|
+
|
41
|
+
@scroll_list.setter
|
42
|
+
def scroll_list(self, lst):
|
43
|
+
self.value = [self.value[0] if self.value else [], lst]
|
44
|
+
if hasattr(self,'scaler'):
|
45
|
+
sval = self.scaler.value
|
46
|
+
if sval != 1:
|
47
|
+
self.scaler.value = 1
|
48
|
+
self.scaler.changed(self.scaler, sval)
|
49
|
+
|
50
|
+
class ParamBlock(Block):
|
51
|
+
def __init__(self, name, *args, row = 3, **params):
|
52
|
+
if not args:
|
53
|
+
args = [[]]
|
54
|
+
super().__init__(name, *args)
|
55
|
+
self.name2elem = {}
|
56
|
+
cnt = 0
|
57
|
+
|
58
|
+
for param, val in params.items():
|
59
|
+
pretty_name = param.replace('_',' ')
|
60
|
+
pretty_name = pretty_name[0].upper() + pretty_name[1:]
|
61
|
+
t = type(val)
|
62
|
+
if t == str:
|
63
|
+
el = Edit(pretty_name, val)
|
64
|
+
elif t == bool:
|
65
|
+
el = Switch(pretty_name, val)
|
66
|
+
elif t == list or t == tuple:
|
67
|
+
el = Select(pretty_name, val[0], options = val, type = 'select')
|
68
|
+
else:
|
69
|
+
el = Edit(pretty_name, val, type = 'number')
|
70
|
+
self.name2elem[param] = el
|
71
|
+
|
72
|
+
if cnt % row == 0:
|
73
|
+
block = []
|
74
|
+
self.value.append(block)
|
75
|
+
cnt += 1
|
76
|
+
block.append(el)
|
77
|
+
@property
|
78
|
+
def params(self):
|
79
|
+
return {name: el.value for name, el in self.name2elem.items()}
|
80
|
+
|
81
|
+
class Dialog:
|
82
|
+
def __init__(self, question, callback, *content, commands = ['Ok','Cancel'],
|
83
|
+
icon = 'not_listed_location'):
|
84
|
+
self.type = 'dialog'
|
85
|
+
self.name = question
|
86
|
+
self.changed = callback
|
87
|
+
self.commands = commands
|
88
|
+
self.icon = icon
|
89
|
+
self.value = [[], *content] if content else []
|
90
|
+
|
91
|
+
class Screen:
|
92
|
+
def __init__(self, name, **kwargs):
|
93
|
+
self.name = name
|
94
|
+
self.type = 'screen'
|
95
|
+
for key, value in kwargs.items():
|
96
|
+
setattr(self, key, value)
|
97
|
+
|
unisi/guielements.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
class Gui:
|
2
|
+
def __init__(self, name, *args, **kwargs):
|
3
|
+
self.name = name
|
4
|
+
la = len(args)
|
5
|
+
if la:
|
6
|
+
self.value = args[0]
|
7
|
+
if la > 1:
|
8
|
+
self.changed = args[1]
|
9
|
+
self.add(kwargs)
|
10
|
+
|
11
|
+
def add(self, kwargs):
|
12
|
+
for key, value in kwargs.items():
|
13
|
+
setattr(self, key, value)
|
14
|
+
|
15
|
+
def mutate(self, obj):
|
16
|
+
self.__dict__ = obj.__dict__
|
17
|
+
|
18
|
+
def accept(self, value):
|
19
|
+
if hasattr(self, 'changed'):
|
20
|
+
self.changed(self, value)
|
21
|
+
else:
|
22
|
+
self.value = value
|
23
|
+
|
24
|
+
Line = Gui("Line", type = 'line')
|
25
|
+
|
26
|
+
def smart_complete(lst, min_input_length = 0, max_output_length = 20):
|
27
|
+
di = {it: it.lower() for it in lst}
|
28
|
+
def complete(gui, ustr):
|
29
|
+
if len(ustr) < min_input_length:
|
30
|
+
return []
|
31
|
+
ustr = ustr.lower()
|
32
|
+
arr = [(itlow.find(ustr), it, itlow) for it, itlow in di.items() if itlow.find(ustr) != -1]
|
33
|
+
arr.sort(key=lambda e: (e[0], e[2]))
|
34
|
+
if len(arr) > max_output_length:
|
35
|
+
arr = arr[: max_output_length]
|
36
|
+
return [e[1] for e in arr]
|
37
|
+
return complete
|
38
|
+
|
39
|
+
class Edit(Gui):
|
40
|
+
def __init__(self, name, *args, **kwargs):
|
41
|
+
super().__init__(name, *args, **kwargs)
|
42
|
+
has_value = hasattr(self,'value')
|
43
|
+
if 'type' not in kwargs:
|
44
|
+
if has_value:
|
45
|
+
type_value = type(self.value)
|
46
|
+
if type_value == int or type_value == float:
|
47
|
+
self.type = 'number'
|
48
|
+
return
|
49
|
+
self.type = 'autoedit' if 'complete' in kwargs else 'edit'
|
50
|
+
if not has_value:
|
51
|
+
self.value = '' if self.type != 'number' else 0
|
52
|
+
|
53
|
+
class Text(Gui):
|
54
|
+
def __init__(self, name, *args, **kwargs):
|
55
|
+
super().__init__(name, *args, **kwargs)
|
56
|
+
self.value = self.name
|
57
|
+
self.type = 'text'
|
58
|
+
|
59
|
+
class Range(Gui):
|
60
|
+
def __init__(self, name, *args, **kwargs):
|
61
|
+
super().__init__(name, *args, **kwargs)
|
62
|
+
if not hasattr(self, 'value'):
|
63
|
+
self.value = 1.0
|
64
|
+
self.type = 'range'
|
65
|
+
if 'options' not in kwargs:
|
66
|
+
self.options = [self.value - 10, self.value + 10, 1]
|
67
|
+
|
68
|
+
class Button(Gui):
|
69
|
+
def __init__(self, name, handler = None, **kwargs):
|
70
|
+
self.name = name
|
71
|
+
self.add(kwargs)
|
72
|
+
if not hasattr(self, 'type'):
|
73
|
+
self.type = 'command'
|
74
|
+
if handler:
|
75
|
+
self.changed = handler
|
76
|
+
|
77
|
+
def CameraButton(name, handler = None, **kwargs):
|
78
|
+
kwargs['type'] = 'camera'
|
79
|
+
return Button(name, handler, **kwargs)
|
80
|
+
|
81
|
+
def UploadButton(name, handler = None,**kwargs):
|
82
|
+
kwargs['type'] = 'image_uploader'
|
83
|
+
if 'width' not in kwargs:
|
84
|
+
kwargs['width'] = 250.0
|
85
|
+
return Button(name, handler, **kwargs)
|
86
|
+
|
87
|
+
class Image(Gui):
|
88
|
+
'''has to contain file parameter as name'''
|
89
|
+
def __init__(self,name, *args, **kwargs):
|
90
|
+
super().__init__(name, *args, **kwargs)
|
91
|
+
self.type='image'
|
92
|
+
if not hasattr(self,'width'):
|
93
|
+
self.width = 500.0
|
94
|
+
if not hasattr(self,'url'):
|
95
|
+
self.url = self.name
|
96
|
+
#mask full win path from Chrome detector
|
97
|
+
if self.url[1] == ':':
|
98
|
+
self.url = f'/{self.url}'
|
99
|
+
|
100
|
+
class Video(Gui):
|
101
|
+
'''has to contain src parameter'''
|
102
|
+
def __init__(self,name, *args, **kwargs):
|
103
|
+
super().__init__(name, *args, **kwargs)
|
104
|
+
self.type = 'video'
|
105
|
+
if not hasattr(self,'width'):
|
106
|
+
self.width = 300.0
|
107
|
+
if not hasattr(self,'url'):
|
108
|
+
self.url = self.name
|
109
|
+
if not hasattr(self,'ratio'):
|
110
|
+
self.ratio = None
|
111
|
+
|
112
|
+
graph_default_value = {'nodes' : [], 'edges' : []}
|
113
|
+
|
114
|
+
class Graph(Gui):
|
115
|
+
'''has to contain nodes, edges, see Readme'''
|
116
|
+
def __init__(self, name, *args, **kwargs):
|
117
|
+
super().__init__(name, *args, **kwargs)
|
118
|
+
self.type='graph'
|
119
|
+
if not hasattr(self,'value'):
|
120
|
+
self.value = graph_default_value
|
121
|
+
if not hasattr(self,'minwidth'):
|
122
|
+
self.minwidth = 600.0
|
123
|
+
if not hasattr(self, 'nodes'):
|
124
|
+
self.nodes = []
|
125
|
+
if not hasattr(self, 'edges'):
|
126
|
+
self.edges = []
|
127
|
+
|
128
|
+
class Switch(Gui):
|
129
|
+
def __init__(self,name, *args, **kwargs):
|
130
|
+
super().__init__(name, *args, **kwargs)
|
131
|
+
if not hasattr(self,'value'):
|
132
|
+
self.value = False
|
133
|
+
if not hasattr(self,'type'):
|
134
|
+
self.type = 'switch'
|
135
|
+
|
136
|
+
class Select(Gui):
|
137
|
+
def __init__(self,name, *args, **kwargs):
|
138
|
+
super().__init__(name, *args, **kwargs)
|
139
|
+
if not hasattr(self,'options'):
|
140
|
+
self.options = []
|
141
|
+
if not hasattr(self,'value'):
|
142
|
+
self.value = None
|
143
|
+
if not hasattr(self, 'type'):
|
144
|
+
self.type = 'select' if len(self.options) > 3 else 'radio'
|
145
|
+
|
146
|
+
class Tree(Gui):
|
147
|
+
def __init__(self,name, *args, **kwargs):
|
148
|
+
super().__init__(name, *args, **kwargs)
|
149
|
+
self.type = 'tree'
|
150
|
+
if not hasattr(self,'options'):
|
151
|
+
self.options = {}
|
152
|
+
if not hasattr(self,'value'):
|
153
|
+
self.value = None
|
154
|
+
|
155
|
+
class TextArea(Gui):
|
156
|
+
def __init__(self,name, *args, **kwargs):
|
157
|
+
super().__init__(name, *args, **kwargs)
|
158
|
+
self.type = 'textarea'
|
159
|
+
|
160
|
+
|
unisi/proxy.py
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
from websocket import create_connection
|
2
|
+
from enum import Enum
|
3
|
+
|
4
|
+
from .common import *
|
5
|
+
|
6
|
+
class Event(Enum):
|
7
|
+
none = 0
|
8
|
+
update = 1
|
9
|
+
invalid = 2
|
10
|
+
message = 4
|
11
|
+
update_message = 6
|
12
|
+
progress = 12
|
13
|
+
update_progress = 13
|
14
|
+
unknown = 16
|
15
|
+
unknown_update = 17
|
16
|
+
dialog = 32
|
17
|
+
screen = 65
|
18
|
+
complete = 128
|
19
|
+
append = 256
|
20
|
+
|
21
|
+
ws_header = 'ws://'
|
22
|
+
wss_header = 'wss://'
|
23
|
+
ws_path = 'ws'
|
24
|
+
|
25
|
+
message_types = ['error','warning','info']
|
26
|
+
|
27
|
+
class Proxy:
|
28
|
+
"""UNISI proxy"""
|
29
|
+
def __init__(self, addr_port, timeout = 3, ssl = False):
|
30
|
+
if not addr_port.startswith(ws_header) and not addr_port.startswith(wss_header):
|
31
|
+
addr_port = f'{wss_header if ssl else ws_header}{addr_port}'
|
32
|
+
addr_port = f'{addr_port}{"" if addr_port.endswith("/") else "/"}{ws_path}'
|
33
|
+
|
34
|
+
self.conn = create_connection(addr_port, timeout = timeout)
|
35
|
+
self.screen = None
|
36
|
+
self.screens = {}
|
37
|
+
self.dialog = None
|
38
|
+
self.event = None
|
39
|
+
self.request(None)
|
40
|
+
|
41
|
+
def close(self):
|
42
|
+
self.conn.close()
|
43
|
+
|
44
|
+
@property
|
45
|
+
def screen_menu(self):
|
46
|
+
return [name_icon[0] for name_icon in self.screen['menu']] if self.screen else []
|
47
|
+
|
48
|
+
@property
|
49
|
+
def commands(self, names = False):
|
50
|
+
"""return command objects or its names"""
|
51
|
+
celems = self.elements(types=['command'])
|
52
|
+
return [el['name'] for el in celems] if names else celems
|
53
|
+
|
54
|
+
def elements(self, block = None, types = None):
|
55
|
+
"""get elements with filtering types and blocks"""
|
56
|
+
if block:
|
57
|
+
return [el for el in flatten(block['value']) if not types or el['type'] in types]
|
58
|
+
answer = []
|
59
|
+
for block in self.screen['name2block'].values():
|
60
|
+
answer.extend([el for el in flatten(block['value']) if not types or el['type'] in types])
|
61
|
+
return answer
|
62
|
+
|
63
|
+
def block_of(self, element):
|
64
|
+
for block in self.screen['name2block'].values():
|
65
|
+
for el in flatten(block['value']):
|
66
|
+
if el == element:
|
67
|
+
return block
|
68
|
+
|
69
|
+
def message(self, element, value, event = 'changed'):
|
70
|
+
if event != 'changed' and event not in element:
|
71
|
+
return None
|
72
|
+
return ArgObject(block = self.block_of(element), element = element['name'],
|
73
|
+
event = event, value = value)
|
74
|
+
|
75
|
+
def interact(self, message, progress_callback = None):
|
76
|
+
"""progress_callback is def (proxy:Proxy)"""
|
77
|
+
while self.request(message) & Event.progress:
|
78
|
+
if progress_callback:
|
79
|
+
progress_callback(self)
|
80
|
+
message = None
|
81
|
+
return self.event
|
82
|
+
|
83
|
+
def request(self, message):
|
84
|
+
"""send message or message list, get responce, return the responce type"""
|
85
|
+
if message:
|
86
|
+
self.conn.send(toJson(message))
|
87
|
+
responce = self.conn.recv()
|
88
|
+
message = json.loads(responce)
|
89
|
+
return self.process(message)
|
90
|
+
|
91
|
+
def set_screen(self, name):
|
92
|
+
screen = self.screens.get(name)
|
93
|
+
if not screen:
|
94
|
+
if name in self.screen_menu:
|
95
|
+
mtype = self.request(ArgObject(block = 'root', element = None, value = name))
|
96
|
+
return mtype == Event.screen
|
97
|
+
else:
|
98
|
+
return False
|
99
|
+
return True
|
100
|
+
|
101
|
+
@property
|
102
|
+
def dialog_commands(self):
|
103
|
+
return self.dialog['commands'] if self.dialog else []
|
104
|
+
|
105
|
+
def dialog_responce(self, command: str | None):
|
106
|
+
if not self.dialog:
|
107
|
+
self.event = Event.invalid
|
108
|
+
return self.event
|
109
|
+
return self.interact(ArgObject(block = self.dialog['name'], value = command))
|
110
|
+
|
111
|
+
def process(self, message):
|
112
|
+
self.message = message
|
113
|
+
if not message:
|
114
|
+
self.event = Event.none
|
115
|
+
self.mtype = None
|
116
|
+
else:
|
117
|
+
mtype = message.get('type')
|
118
|
+
self.mtype = mtype
|
119
|
+
if mtype == 'screen':
|
120
|
+
self.screen = message
|
121
|
+
self.screens[self.screen['name']] = message
|
122
|
+
name2block = {block['name']: block for block in flatten(message['blocks'])}
|
123
|
+
name2block['toolbar'] = {'name': 'toolbar', 'value': message['toolbar']}
|
124
|
+
message['name2block'] = name2block
|
125
|
+
self.event = Event.screen
|
126
|
+
elif mtype == 'dialog':
|
127
|
+
self.dialog = message
|
128
|
+
self.event = Event.dialog
|
129
|
+
elif mtype == 'complete':
|
130
|
+
return Event.complete
|
131
|
+
elif mtype == 'append':
|
132
|
+
self.event = Event.append
|
133
|
+
elif mtype == 'update':
|
134
|
+
self.update(message)
|
135
|
+
self.event = Event.update
|
136
|
+
else:
|
137
|
+
updates = message.get('updates')
|
138
|
+
if updates:
|
139
|
+
self.update(message)
|
140
|
+
if type in message_types:
|
141
|
+
self.event = Event.update_message if updates else Event.message
|
142
|
+
if type == 'progress':
|
143
|
+
self.event = Event.update_progress if updates else Event.progress
|
144
|
+
else:
|
145
|
+
self.event = Event.unknown_update if updates else Event.unknown
|
146
|
+
return self.event
|
147
|
+
|
148
|
+
def update(self, message):
|
149
|
+
"""update screen from the message"""
|
150
|
+
updates = message.updates
|
151
|
+
for update in updates:
|
152
|
+
path = update['path']
|
153
|
+
name2block = self.screen['name2block']
|
154
|
+
if len(path) == 1: #block
|
155
|
+
name2block[block] = update['data']
|
156
|
+
else:
|
157
|
+
block, element = path
|
158
|
+
name2block[block][element] = update['data']
|
159
|
+
|