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 ADDED
@@ -0,0 +1,7 @@
1
+ from .utils import *
2
+ from .guielements import *
3
+ from .users import User, handle
4
+ from .server import start
5
+ from .tables import *
6
+ from .containers import *
7
+ from .proxy import *
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
+