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/reloader.py ADDED
@@ -0,0 +1,150 @@
1
+ from .autotest import config
2
+
3
+ empty_app = {
4
+ "blocks": [],
5
+ "header": "No screens",
6
+ "icon": None,
7
+ "menu": [["You need to put at least 1 file in the 'screens' folder.",'exclamation']],
8
+ "name": "",
9
+ "order": 0,
10
+ "toolbar": [],
11
+ "type": "screen"
12
+ }
13
+
14
+ if config.hot_reload:
15
+ import os, sys, traceback
16
+ from watchdog.observers import Observer
17
+ from watchdog.events import PatternMatchingEventHandler
18
+ from .users import User
19
+ from .utils import divpath, Redesign, app_dir
20
+ from .autotest import check_screen
21
+ import re, collections
22
+
23
+ #for removing message duplicates
24
+ file_content = collections.defaultdict(str)
25
+
26
+ busy = False
27
+ cwd = os.getcwd()
28
+
29
+ def free():
30
+ global busy
31
+ if request_file:
32
+ reload(request_file)
33
+ else:
34
+ busy = False
35
+
36
+ def reload(sname):
37
+ user = User.last_user
38
+ if user:
39
+ file = open(f'screens{divpath}{sname}', "r")
40
+ content = file.read()
41
+ if file_content[sname] == content:
42
+ return
43
+ file_content[sname] = content
44
+
45
+ global busy, request_file
46
+ busy = True
47
+ request_file = None
48
+
49
+ try:
50
+ module = user.load_screen(sname)
51
+ errors = check_screen(module)
52
+ if errors:
53
+ print('\n'.join(errors))
54
+ busy = False
55
+ return
56
+ print('Reloaded.')
57
+ except:
58
+ traceback.print_exc()
59
+ busy = False
60
+ return
61
+
62
+ for i, s in enumerate(user.screens):
63
+ if s.__file__ == module.__file__:
64
+ same = user.screen_module.__file__ == module.__file__
65
+ user.screens[i] = module
66
+ if same:
67
+ user.set_screen(module.screen.name)
68
+ break
69
+ else:
70
+ user.screens.append(module)
71
+ if len(user.screens) == 1:
72
+ user.set_screen(module.name)
73
+
74
+ user.update_menu()
75
+ user.set_clean()
76
+ user.sync_send(Redesign)
77
+
78
+ free()
79
+ return module
80
+
81
+ class ScreenEventHandler(PatternMatchingEventHandler):
82
+ def on_modified(self, event):
83
+ if not event.is_directory and User.last_user:
84
+ short_path = event.src_path[len(cwd) + 1:]
85
+ arr = short_path.split(divpath)
86
+ name = arr[-1]
87
+ dir = arr[0] if len(arr) > 1 else ''
88
+
89
+ if name.endswith('.py'):
90
+ user = User.last_user
91
+
92
+ if user.screen_module and dir not in ['screens','blocks']:
93
+ #analyze if dependency exist
94
+ file = open(user.screen_module.__file__, "r")
95
+ arr[-1] = arr[-1][:-3]
96
+ module_name = '.'.join(arr)
97
+ module_pattern = '\.'.join(arr)
98
+
99
+ if re.search(f"((import|from)[ \t]*{module_pattern}[ \t\n]*)",file.read()):
100
+ if module_name in sys.modules:
101
+ del sys.modules[module_name]
102
+ short_path = user.screen_module.__file__
103
+ if short_path.startswith(app_dir):
104
+ short_path = short_path[len(app_dir) + 1:]
105
+ dir, name = short_path.split(divpath)
106
+
107
+ if dir in ['screens','blocks']:
108
+ if busy:
109
+ global request_file
110
+ request_file = short_path
111
+ else:
112
+ fresh_module = reload(name) if dir == 'screens' else None
113
+ module = user.screen_module
114
+ if module:
115
+ current = module.__file__
116
+ if not fresh_module or current != fresh_module.__file__:
117
+ reload(current.split(divpath)[-1])
118
+
119
+ def on_deleted(self, event):
120
+ if not event.is_directory and User.last_user:
121
+ user = User.last_user
122
+ arr = event.src_path.split(divpath)
123
+ name = arr[-1]
124
+ dir = arr[-2]
125
+ if name.endswith('.py') and dir == 'screens':
126
+ delfile = f'{dir}{divpath}{name}'
127
+ for i, s in enumerate(user.screens):
128
+ if s.__file__ == event.src_path:
129
+ user.screens.remove(s)
130
+ if user.screen_module is s:
131
+ if user.screens:
132
+ fname = user.screens[0].__file__.split(divpath)[-1]
133
+ module = reload(fname)
134
+ user.set_screen(module.name)
135
+ user.update_menu()
136
+ user.sync_send(Redesign)
137
+ else:
138
+ user.sync_send(empty_app)
139
+ else:
140
+ reload(user.screen_module.__file__.split(divpath)[-1])
141
+ user.update_menu()
142
+ user.sync_send(Redesign)
143
+ break
144
+
145
+ event_handler = ScreenEventHandler()
146
+ observer = Observer()
147
+ path = os.getcwd()
148
+ observer.schedule(event_handler, path, recursive = True)
149
+ observer.start()
150
+
unisi/server.py ADDED
@@ -0,0 +1,118 @@
1
+ from aiohttp import web, WSMsgType
2
+ from .users import *
3
+ from pathlib import Path
4
+ from .reloader import empty_app
5
+ from .autotest import recorder, jsonString, run_tests
6
+ from .common import *
7
+ from config import port, upload_dir
8
+ import traceback
9
+
10
+ async def post_handler(request):
11
+ reader = await request.multipart()
12
+ field = await reader.next()
13
+ filename = upload_path(field.filename)
14
+ size = 0
15
+ with open(filename, 'wb') as f:
16
+ while True:
17
+ chunk = await field.read_chunk()
18
+ if not chunk:
19
+ break
20
+ size += len(chunk)
21
+ f.write(chunk)
22
+
23
+ return web.Response(text=filename)
24
+
25
+ async def static_serve(request):
26
+ rpath = request.path
27
+ file_path = Path(f"{webpath}{rpath}" )
28
+ if request.path == '/':
29
+ file_path /= 'index.html'
30
+
31
+ if not file_path.exists():
32
+ file_path = None
33
+ #unmask win path
34
+ if rpath.startswith('/') and rpath[2] == ':':
35
+ rpath = rpath[1:]
36
+ dirs = getattr(config, public_dirs, [])
37
+ for dir in dirs:
38
+ if rpath.startswith(dir):
39
+ if os.path.exists(rpath):
40
+ file_path = Path(rpath)
41
+ break
42
+
43
+ return web.FileResponse(file_path) if file_path else web.HTTPNotFound()
44
+
45
+ def broadcast(message, message_user):
46
+ screen = message_user.screen_module
47
+ for user in User.reflections:
48
+ if user is not message_user and screen is user.screen_module:
49
+ user.sync_send(message)
50
+
51
+ async def websocket_handler(request):
52
+ ws = web.WebSocketResponse()
53
+ await ws.prepare(request)
54
+ user, ok = make_user()
55
+ user.transport = ws._writer.transport if divpath != '/' else None
56
+
57
+ async def send(res):
58
+ if type(res) != str:
59
+ res = jsonString(user.prepare_result(res))
60
+ await ws.send_str(res)
61
+
62
+ user.send = send
63
+ user.session = request.remote
64
+ await send(user.screen if ok else empty_app)
65
+ try:
66
+ async for msg in ws:
67
+ if msg.type == WSMsgType.TEXT:
68
+ if msg.data == 'close':
69
+ await ws.close()
70
+ else:
71
+ raw_message = json.loads(msg.data)
72
+ if isinstance(raw_message, list):
73
+ for raw_submessage in raw_message:
74
+ message = ReceivedMessage(raw_submessage)
75
+ result = user.result4message(message)
76
+ else:
77
+ message = None
78
+ result = Error('Empty command batch!')
79
+ else:
80
+ message = ReceivedMessage(raw_message)
81
+ result = user.result4message(message)
82
+ await send(result)
83
+ if message:
84
+ if recorder.record_file:
85
+ recorder.accept(message, result)
86
+ if config.mirror and not is_screen_switch(message):
87
+ if result:
88
+ broadcast(result, user)
89
+ msg_object = user.find_element(message)
90
+ if not isinstance(result, Message) or not result.contains(msg_object):
91
+ broadcast(jsonString(user.prepare_result(msg_object)), user)
92
+ elif msg.type == WSMsgType.ERROR:
93
+ user.log('ws connection closed with exception %s' % ws.exception())
94
+ except:
95
+ user.log(traceback.format_exc())
96
+
97
+ if User.reflections:
98
+ User.reflections.remove(user)
99
+ return ws
100
+
101
+ def start(appname = None, user_type = User, http_handlers = []):
102
+ if appname is not None:
103
+ config.appname = appname
104
+
105
+ User.UserType = user_type
106
+
107
+ if config.autotest:
108
+ run_tests()
109
+
110
+ http_handlers.insert(0, web.get('/ws', websocket_handler))
111
+ http_handlers += [web.static(f'/{config.upload_dir}', upload_dir),
112
+ web.get('/{tail:.*}', static_serve), web.post('/', post_handler)]
113
+
114
+ print(f'Start {appname} web server..')
115
+ app = web.Application()
116
+ app.add_routes(http_handlers)
117
+ web.run_app(app, port=port)
118
+
unisi/tables.py ADDED
@@ -0,0 +1,116 @@
1
+ from .guielements import Gui
2
+
3
+ def accept_cell_value(table, val):
4
+ value, position = val
5
+ if not isinstance(value, bool):
6
+ try:
7
+ value = float(value)
8
+ except ValueError:
9
+ pass
10
+ table.rows[position[0]][position[1]] = value
11
+
12
+ def delete_table_row(table, value):
13
+ if table.rows:
14
+ keyed = len(table.headers) < len(table.rows[0])
15
+ table.value = value
16
+ if isinstance(value, list):
17
+ if keyed:
18
+ table.rows = [row for row in table.rows if row[-1] not in value]
19
+ else:
20
+ value.sort(reverse=True)
21
+ for v in value:
22
+ del table.rows[v]
23
+ table.value = []
24
+ else:
25
+ if keyed:
26
+ table.rows = [row for row in table.rows if row[-1] != value]
27
+ else:
28
+ del table.rows[value]
29
+ table.value = None
30
+
31
+ def append_table_row(table, value):
32
+ ''' append has to return new row or error string, val is search string in the table'''
33
+ new_id_row, search = value #new_id_row == rows count
34
+ new_row = [''] * len(table.headers)
35
+ if search:
36
+ new_row[0] = search
37
+ table.rows.append(new_row)
38
+ return new_row
39
+
40
+ class Table(Gui):
41
+ def __init__(self, *args, panda = None, **kwargs):
42
+ if panda is not None:
43
+ self.mutate(PandaTable(*args, panda=panda, **kwargs))
44
+ else:
45
+ super().__init__(*args, **kwargs)
46
+ if not hasattr(self,'headers'):
47
+ self.headers = []
48
+ if not hasattr(self,'type'):
49
+ self.type = 'table'
50
+ if not hasattr(self,'value'):
51
+ self.value = None
52
+ if not hasattr(self,'rows'):
53
+ self.rows = []
54
+ if not hasattr(self,'dense'):
55
+ self.dense = True
56
+
57
+ if getattr(self,'edit', True):
58
+ if not hasattr(self,'delete'):
59
+ self.delete = delete_table_row
60
+ if not hasattr(self,'append'):
61
+ self.append = append_table_row
62
+ if not hasattr(self,'modify'):
63
+ self.modify = accept_cell_value
64
+
65
+ def selected_list(self):
66
+ return [self.value] if self.value != None else [] if type(self.value) == int else self.value
67
+
68
+ def clean(self):
69
+ self.rows = []
70
+ self.value = [] if isinstance(self.value,(tuple, list)) else None
71
+ return self
72
+
73
+ def delete_panda_row(table, row_num):
74
+ df = table.__panda__
75
+ if row_num < 0 or row_num >= len(df):
76
+ raise ValueError("Row number is out of range")
77
+ pt = table.__panda__
78
+ pt.drop(index = row_num, inplace=True)
79
+ pt.reset_index(inplace=True)
80
+ delete_table_row(table, row_num)
81
+
82
+ def accept_panda_cell(table, value_pos):
83
+ value, position = value_pos
84
+ row_num, col_num = position
85
+ table.__panda__.iloc[row_num,col_num] = value
86
+ accept_cell_value(table, value_pos)
87
+
88
+ def append_panda_row(table, row_num):
89
+ df = table.__panda__
90
+ new_row = append_table_row(table, row_num)
91
+ df.loc[len(df), df.columns] = new_row
92
+ return new_row
93
+
94
+ class PandaTable(Table):
95
+ """ panda = opened panda table"""
96
+ def __init__(self, *args, panda = None, fix_headers = True, **kwargs):
97
+ super().__init__(*args, **kwargs)
98
+ if panda is None:
99
+ raise Exception('PandaTable has to get panda = pandaTable as an argument.')
100
+ self.headers = panda.columns.tolist()
101
+ if fix_headers:
102
+ self.headers = [header.replace('_',' ') for header in self.headers]
103
+ self.rows = panda.values.tolist()
104
+ self.__panda__ = panda
105
+
106
+ if getattr(self,'edit', True):
107
+ if not hasattr(self,'delete'):
108
+ self.delete = delete_panda_row
109
+ if not hasattr(self,'append'):
110
+ self.append = append_panda_row
111
+ if not hasattr(self,'modify'):
112
+ self.modify = accept_panda_cell
113
+ @property
114
+ def panda(self):
115
+ return getattr(self,'__panda__',None)
116
+
unisi/users.py ADDED
@@ -0,0 +1,271 @@
1
+ import importlib
2
+ from .utils import *
3
+ from .guielements import *
4
+ from .common import *
5
+ from .containers import Dialog, Screen
6
+ import sys
7
+ import asyncio
8
+ from threading import Thread
9
+ import logging
10
+
11
+ class User:
12
+ def __init__(self):
13
+ self.screens = []
14
+ self.active_dialog = None
15
+ self.screen_module = None
16
+ self.session = None
17
+ self.__handlers__ = {}
18
+ self.last_message = None
19
+ User.last_user = self
20
+
21
+ async def send_windows(self, obj):
22
+ await self.send(obj)
23
+ self.transport._write_fut = None
24
+ self.transport._loop._ready.pop()
25
+
26
+ def sync_send(self, obj):
27
+ asyncio.run_coroutine_threadsafe(self.send_windows(obj)
28
+ if self.transport else self.send(obj), self.extra_loop)
29
+
30
+ def progress(self, str, *updates):
31
+ """open or update progress window if str != null else close it """
32
+ if not self.testing:
33
+ self.sync_send(TypeMessage('progress', str, *updates, user = self))
34
+
35
+ def load_screen(self, file):
36
+ screen_vars = {
37
+ 'icon' : None,
38
+ 'prepare' : None,
39
+ 'blocks' : [],
40
+ 'header' : config.appname,
41
+ 'toolbar' : [],
42
+ 'order' : 0,
43
+ 'reload': config.hot_reload
44
+ }
45
+ name = file[:-3]
46
+ path = f'{screens_dir}{divpath}{file}'
47
+ spec = importlib.util.spec_from_file_location(name,path)
48
+ module = importlib.util.module_from_spec(spec)
49
+
50
+ module.user = self
51
+
52
+ spec.loader.exec_module(module)
53
+ screen = Screen(getattr(module, 'name', ''))
54
+ #set system vars
55
+ for var in screen_vars:
56
+ setattr(screen, var, getattr(module,var,screen_vars[var]))
57
+
58
+ if screen.toolbar:
59
+ screen.toolbar += User.toolbar
60
+ else:
61
+ screen.toolbar = User.toolbar
62
+
63
+ module.screen = screen
64
+ return module
65
+
66
+ def set_clean(self):
67
+ #remove user modules from sys
68
+ if os.path.exists(blocks_dir):
69
+ for file in os.listdir(blocks_dir):
70
+ if file.endswith(".py") and file != '__init__.py':
71
+ name = f'{blocks_dir}.{file[0:-3]}'
72
+ if name in sys.modules:
73
+ sys.modules[name].user = self
74
+ del sys.modules[name]
75
+ def load(self):
76
+ if os.path.exists(screens_dir):
77
+ for file in os.listdir(screens_dir):
78
+ if file.endswith(".py") and file != '__init__.py':
79
+ module = self.load_screen(file)
80
+ self.screens.append(module)
81
+
82
+ if self.screens:
83
+ self.screens.sort(key=lambda s: s.screen.order)
84
+ main = self.screens[0]
85
+ if 'prepare' in dir(main):
86
+ main.prepare()
87
+ self.screen_module = main
88
+ self.update_menu()
89
+ self.set_clean()
90
+ return True
91
+
92
+ def update_menu(self):
93
+ menu = [[getattr(s, 'name', ''),getattr(s,'icon', None)] for s in self.screens]
94
+ for s in self.screens:
95
+ s.screen.menu = menu
96
+
97
+ @property
98
+ def testing(self):
99
+ return self.session == 'autotest'
100
+
101
+ @property
102
+ def screen(self):
103
+ return self.screen_module.screen
104
+
105
+ def set_screen(self,name):
106
+ return self.process(ArgObject(block = 'root', element = None, value = name))
107
+
108
+ def result4message(self, message):
109
+ result = None
110
+ dialog = self.active_dialog
111
+ if dialog:
112
+ if message.element is None: #button pressed
113
+ self.active_dialog = None
114
+ result = dialog.changed(dialog, message.value)
115
+ else:
116
+ el = self.find_element(message)
117
+ if el:
118
+ result = self.process_element(el, message)
119
+ else:
120
+ result = self.process(message)
121
+ if result and isinstance(result, Dialog):
122
+ self.active_dialog = result
123
+ return result
124
+
125
+ @property
126
+ def blocks(self):
127
+ return [self.active_dialog] if self.active_dialog and \
128
+ self.active_dialog.value else self.screen.blocks
129
+
130
+ def find_element(self, message):
131
+ blname = message.block
132
+ elname = message.element
133
+ if blname == 'toolbar':
134
+ for e in self.screen.toolbar:
135
+ if e.name == elname:
136
+ return e
137
+ else:
138
+ for bl in flatten(self.blocks):
139
+ if bl.name == blname:
140
+ for c in bl.value:
141
+ if isinstance(c, list):
142
+ for sub in c:
143
+ if sub.name == elname:
144
+ return sub
145
+ elif c.name == elname:
146
+ return c
147
+
148
+ def find_path(self, elem):
149
+ for bl in flatten(self.blocks):
150
+ if bl == elem:
151
+ return [bl.name]
152
+ for c in bl.value:
153
+ if isinstance(c, list):
154
+ for sub in c:
155
+ if sub == elem:
156
+ return [bl.name, sub.name]
157
+ elif c == elem:
158
+ return [bl.name, c.name]
159
+ for e in self.screen.toolbar:
160
+ if e == elem:
161
+ return ['toolbar', e.name]
162
+
163
+ def prepare_result(self, raw):
164
+ if raw == UpdateScreen:
165
+ raw = self.screen
166
+ raw.reload = False
167
+ elif raw == Redesign:
168
+ raw = self.screen
169
+ raw.reload = True
170
+ else:
171
+ if isinstance(raw, Message):
172
+ raw.fill_paths4(self)
173
+ elif isinstance(raw,Gui):
174
+ raw = Message(raw, user = self)
175
+ elif isinstance(raw, (list, tuple)):
176
+ raw = Message(*raw, user = self)
177
+ return raw
178
+
179
+ def process(self, message):
180
+ self.last_message = message
181
+ screen_change_message = message.screen and self.screen.name != message.screen
182
+ if is_screen_switch(message) or screen_change_message:
183
+ for s in self.screens:
184
+ if s.name == message.value:
185
+ self.screen_module = s
186
+ if screen_change_message:
187
+ break
188
+ if getattr(s.screen,'prepare', False):
189
+ s.screen.prepare()
190
+ return True
191
+ else:
192
+ error = f'Unknown screen name: {message.value}'
193
+ self.log(error)
194
+ return Error(error)
195
+
196
+ elem = self.find_element(message)
197
+ if elem:
198
+ return self.process_element(elem, message)
199
+
200
+ error = f'Element {message.block}>>{message.element} does not exists!'
201
+ self.log(error)
202
+ return Error(error)
203
+
204
+ def process_element(self, elem, message):
205
+ event = message.event
206
+ query = event in ['complete', 'append']
207
+
208
+ handler = self.__handlers__.get((elem, event), None)
209
+ if handler:
210
+ result = handler(elem, message.value)
211
+ return result
212
+
213
+ handler = getattr(elem, event, False)
214
+ if handler:
215
+ result = handler(elem, message.value)
216
+ if query:
217
+ result = Answer(event, message, result)
218
+ return result
219
+ elif event == 'changed':
220
+ elem.value = message.value
221
+ else:
222
+ self.log(f'{elem} does not contain method for {event} event type!')
223
+ return Error(f'Invalid {event} event type for {message.block}>>{message.element} is received!')
224
+
225
+ def reflect(self):
226
+ user = User.UserType()
227
+ user.screens = self.screens
228
+ if self.screens:
229
+ user.screen_module = self.screens[0]
230
+ user.__handlers__ = self.__handlers__
231
+ return user
232
+
233
+ def log(self, str, type = 'error'):
234
+ scr = self.screen.name if self.screens else 'omitted'
235
+ str = f"session: {self.session}, screen: {scr}, message: {self.last_message} \n {str}"
236
+ if type == 'error':
237
+ logging.error(str)
238
+ else:
239
+ logging.warning(str)
240
+
241
+ def make_user():
242
+ if config.mirror and User.last_user:
243
+ user = User.last_user.reflect()
244
+ ok = user.screens
245
+ else:
246
+ user = User.UserType()
247
+ ok = user.load()
248
+ if config.mirror:
249
+ User.reflections.append(user)
250
+ return user, ok
251
+
252
+ #loop and thread is for progress window and sync interactions
253
+ loop = asyncio.new_event_loop()
254
+
255
+ def f(loop):
256
+ asyncio.set_event_loop(loop)
257
+ loop.run_forever()
258
+
259
+ async_thread = Thread(target=f, args=(loop,))
260
+ async_thread.start()
261
+
262
+ def handle(elem, event):
263
+ def h(fn):
264
+ User.last_user.__handlers__[elem, event] = fn
265
+ return h
266
+
267
+ User.extra_loop = loop
268
+ User.UserType = User
269
+ User.last_user = None
270
+ User.toolbar = []
271
+ User.reflections = []