Coverage for /home/ubuntu/hidebound/python/hidebound/server/components.py: 100%
138 statements
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-05 23:50 +0000
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-05 23:50 +0000
1from typing import Any, Dict, List, Optional, Union # noqa F401
2from collections import OrderedDict # noqa F401
3import flask # noqa F401
5import re
7from dash import dash_table, dcc, html
8from pandas import DataFrame
9import dash
10import dash_cytoscape as cyto
11import rolling_pin.blob_etl as blob_etl
12# ------------------------------------------------------------------------------
15COLOR_SCHEME = dict(
16 dark1='#040404',
17 dark2='#141414',
18 bg='#181818',
19 grey1='#242424',
20 grey2='#444444',
21 light1='#A4A4A4',
22 light2='#F4F4F4',
23 dialog1='#444459',
24 dialog2='#5D5D7A',
25 red1='#F77E70',
26 red2='#DE958E',
27 orange1='#EB9E58',
28 orange2='#EBB483',
29 yellow1='#E8EA7E',
30 yellow2='#E9EABE',
31 green1='#8BD155',
32 green2='#A0D17B',
33 cyan1='#7EC4CF',
34 cyan2='#B6ECF3',
35 blue1='#5F95DE',
36 blue2='#93B6E6',
37 purple1='#C98FDE',
38 purple2='#AC92DE',
39) # type: Dict[str, str]
40COLORS = [
41 'cyan1',
42 'red1',
43 'green1',
44 'blue1',
45 'purple1',
46 'orange1',
47 'yellow1',
48 'light1',
49 'cyan2',
50 'red2',
51 'blue2',
52 'green2',
53] # type: List[str]
54FONT_FAMILY = 'sans-serif, "sans serif"' # type: str
57# APP---------------------------------------------------------------------------
58def get_dash_app(server, seconds=5.0, storage_type='session'):
59 # type: (flask.Flask, float, str) -> dash.Dash
60 '''
61 Generate Dash Flask app instance.
63 Args:
64 server (Flask): Flask instance.
65 seconds (float, optional): Time between progress updates. Default: 5.
66 storage_type (str): Storage type (used for testing). Default: session.
68 Returns:
69 Dash: Dash app instance.
70 '''
71 store = dcc.Store(id='store', storage_type=storage_type)
73 tab_style = {
74 'padding': '4px',
75 'background': COLOR_SCHEME['bg'],
76 'color': COLOR_SCHEME['light1'],
77 'border': '0px',
78 }
79 tab_selected_style = {
80 'padding': '4px',
81 'background': COLOR_SCHEME['grey1'],
82 'color': COLOR_SCHEME['cyan2'],
83 'border': '0px',
84 }
85 tabs = dcc.Tabs(
86 id='tabs',
87 className='tabs',
88 value='data',
89 children=[
90 dcc.Tab(
91 id='logo',
92 className='tab',
93 label='HIDEBOUND',
94 value='',
95 disabled_style=tab_style,
96 disabled=True,
97 ),
98 dcc.Tab(
99 className='tab',
100 label='data',
101 value='data',
102 style=tab_style,
103 selected_style=tab_selected_style,
104 ),
105 dcc.Tab(
106 className='tab',
107 label='graph',
108 value='graph',
109 style=tab_style,
110 selected_style=tab_selected_style,
111 ),
112 dcc.Tab(
113 className='tab',
114 label='config',
115 value='config',
116 style=tab_style,
117 selected_style=tab_selected_style,
118 ),
119 dcc.Tab(
120 className='tab',
121 label='api',
122 value='api',
123 style=tab_style,
124 selected_style=tab_selected_style,
125 ),
126 dcc.Tab(
127 className='tab',
128 label='docs',
129 value='docs',
130 style=tab_style,
131 selected_style=tab_selected_style,
132 )
133 ],
134 )
135 content = html.Div(
136 id="content-container",
137 className='content-container',
138 children=[
139 html.Div(
140 id="progressbar-container", className='progressbar-container',
141 ),
142 html.Div(id="content", className='content')
143 ],
144 )
145 clock = dcc.Interval(id='clock', interval=int(seconds * 1000))
147 app = dash.Dash(
148 server=server,
149 name='hidebound',
150 title='Hidebound',
151 update_title=None,
152 external_stylesheets=['/static/style.css'],
153 suppress_callback_exceptions=True,
154 )
155 app.layout = html.Div(id='layout', children=[store, clock, tabs, content])
157 return app
160# TABS--------------------------------------------------------------------------
161def get_data_tab(query=None):
162 # type: (Optional[str]) -> List
163 '''
164 Get tab element for Hidebound data.
166 Args:
167 query (str, optional): Query string. Default: None.
169 Return:
170 list: List of elements for data tab.
171 '''
172 # dummies muist go first for element props behavior to work
173 return [*get_dummy_elements(), get_searchbar(query)]
176def get_config_tab(config):
177 # type: (Dict) -> List
178 '''
179 Get tab element for Hidebound config.
181 Args:
182 config (dict): Configuration to be displayed.
184 Return:
185 list: List of elements for config tab.
186 '''
187 # dummies muist go first for element props behavior to work
188 return [*get_dummy_elements(), get_configbar(config)]
191# MENUBARS----------------------------------------------------------------------
192def get_searchbar(query=None):
193 # type: (Optional[str]) -> html.Div
194 '''
195 Get a row of elements used for querying Hidebound data.
197 Args:
198 query (str, optional): Query string. Default: None.
200 Returns:
201 Div: Div with query field, buttons and dropdown.
202 '''
203 if query is None:
204 query = 'SELECT * FROM data'
206 spacer = html.Div(className='col spacer')
207 query = dcc.Input(
208 id='query',
209 className='col query',
210 value=query,
211 placeholder='SQL query that uses "FROM data"',
212 type='text',
213 autoFocus=True,
214 debounce=True,
215 n_submit=0,
216 )
217 dropdown = get_dropdown(['asset', 'file'])
219 search = get_button('search')
220 workflow = get_button('workflow')
221 update = get_button('update')
222 create = get_button('create')
223 export = get_button('export')
224 delete = get_button('delete')
226 row0 = html.Div(
227 className='row',
228 children=[
229 query,
230 spacer,
231 search,
232 spacer,
233 dropdown,
234 spacer,
235 workflow,
236 spacer,
237 update,
238 spacer,
239 create,
240 spacer,
241 export,
242 spacer,
243 delete,
244 ],
245 )
246 row1 = html.Div(className='row-spacer')
247 row2 = html.Div(id='table-content', children=[])
248 searchbar = html.Div(
249 id='searchbar', className='menubar', children=[row0, row1, row2]
250 )
251 return searchbar
254def get_dummy_elements():
255 # type: () -> List
256 '''
257 Returns a list of all elements with callbacks so that the client will not
258 throw errors in each tab.
260 Returns:
261 list: List of html elements.
262 '''
263 return [
264 dcc.Input(className='dummy', id='query', value=None),
265 dcc.Dropdown(className='dummy', id='dropdown', value=None),
266 html.Div(className='dummy', id='search-button', n_clicks=None),
267 html.Div(className='dummy', id='workflow-button', n_clicks=None),
268 html.Div(className='dummy', id='update-button', n_clicks=None),
269 html.Div(className='dummy', id='create-button', n_clicks=None),
270 html.Div(className='dummy', id='export-button', n_clicks=None),
271 html.Div(className='dummy', id='delete-button', n_clicks=None),
272 ]
275def get_configbar(config):
276 # type: (Dict) -> html.Div
277 '''
278 Get a row of elements used for configuring Hidebound.
280 Args:
281 config (dict): Configuration to be displayed.
283 Returns:
284 Div: Div with buttons and JSON editor.
285 '''
286 rows = [
287 html.Div(id='config', children=[
288 get_key_value_card(config, header='config', id_='config-card')
289 ])
290 ]
291 configbar = html.Div(id='configbar', className='menubar', children=rows)
292 return configbar
295def get_progressbar(data):
296 # type: (dict) -> html.Div
297 '''
298 Creates a progress bar given progress data.
300 Args:
301 data (dict): Progress dictionary.
303 Returns:
304 Div: Progress bar.
305 '''
306 temp = dict(message='', progress=1.0) # type: dict
307 if data is None:
308 data = {}
309 temp.update(data)
311 pct = temp['progress'] # type: float
312 width = f'{pct * 100:.0f}%'
314 title = html.Div(
315 id='progressbar-title',
316 className='progressbar-title',
317 children=temp['message'],
318 )
319 body = html.Div(
320 id='progressbar-body',
321 className='progressbar-body',
322 style=dict(width=width)
323 )
324 progressbar = html.Div(id='progressbar', children=[title, body])
325 return progressbar
328# ELEMENTS----------------------------------------------------------------------
329def get_dropdown(options):
330 # type: (List[str]) -> dcc.Dropdown
331 '''
332 Gets html dropdown element with given options.
334 Args:
335 options (list[str]): List of options.
337 Raises:
338 TypeError: If options is not a list.
339 TypeError: If any option is not a string.
341 Returns:
342 Dropdown: Dropdown element.
343 '''
344 if not isinstance(options, list):
345 msg = f'{options} is not a list.'
346 raise TypeError(msg)
348 illegal = list(filter(lambda x: not isinstance(x, str), options))
349 if len(illegal) > 0:
350 msg = f'{illegal} are not strings.'
351 raise TypeError(msg)
353 return dcc.Dropdown(
354 id='dropdown',
355 className='col dropdown',
356 value=options[0],
357 options=[{'label': x, 'value': x} for x in options],
358 placeholder=options[0],
359 optionHeight=20,
360 style={
361 'background': COLOR_SCHEME['grey1'],
362 'color': COLOR_SCHEME['light1'],
363 'border': '0px',
364 'width': '100px',
365 }
366 )
369def get_button(title):
370 # type: (str) -> html.Button
371 '''
372 Get a html button with a given title.
374 Args:
375 title (str): Title of button.
377 Raises:
378 TypeError: If title is not a string.
380 Returns:
381 Button: Button element.
382 '''
383 if not isinstance(title, str):
384 msg = f'{title} is not a string.'
385 raise TypeError(msg)
386 return html.Button(id=f'{title}-button', children=[title], n_clicks=0)
389def get_key_value_card(data, header=None, id_='key-value-card', sorting=False):
390 # type: (Union[Dict, OrderedDict], Optional[str], str, bool) -> html.Div
391 '''
392 Creates a key-value card using the keys and values from the given data.
393 One key-value pair per row.
395 Args:
396 data (dict): Dictionary to be represented.
397 header (str, optional): Name of header. Default: None.
398 id_ (str): Name of id property. Default: "key-value-card".
399 sorting (bool, optional): Whether to sort the output by key.
400 Default: False.
402 Returns:
403 Div: Card with key-value child elements.
404 '''
405 data = blob_etl.BlobETL(data)\
406 .set(
407 predicate=lambda k, v: bool(re.search(r'<list_\d', k)),
408 key_setter=lambda k, v: re.sub('<list_|>', '', k))\
409 .to_flat_dict()
411 children = [] # type: List[Any]
412 if header is not None:
413 header = html.Div(
414 id=f'{id_}-header',
415 className='key-value-card-header',
416 children=[str(header)]
417 )
418 children.append(header)
420 items = data.items() # type: Any
421 if sorting:
422 items = sorted(items)
423 for i, (k, v) in enumerate(items):
424 even = i % 2 == 0
425 klass = 'odd'
426 if even:
427 klass = 'even'
429 key = html.Div(
430 id=f'{k}-key', className='key-value-card-key', children=[str(k)]
431 )
432 sep = html.Div(className='key-value-card-separator')
433 val = html.Div(
434 id=f'{k}-value', className='key-value-card-value', children=[str(v)]
435 )
437 row = html.Div(
438 id=f'{id_}-row',
439 className=f'key-value-card-row {klass}',
440 children=[key, sep, val]
441 )
442 children.append(row)
443 children[-1].className += ' last'
445 card = html.Div(
446 id=f'{id_}',
447 className='key-value-card',
448 children=children
449 )
450 return card
453def get_datatable(data):
454 # type: (List[Dict]) -> dash_table.DataTable
455 '''
456 Gets a Dash DataTable element using given data.
457 Assumes dict element has all columns of table as keys.
459 Args:
460 data (list[dict]): List of dicts.
462 Returns:
463 DataTable: Table of data.
464 '''
465 cols = [] # type: Any
466 if len(data) > 0:
467 cols = data[0].keys()
468 cols = [{'name': x, 'id': x} for x in cols]
469 error_cols = [
470 'asset_error',
471 'file_error',
472 'filename_error'
473 ]
475 return dash_table.DataTable(
476 data=data,
477 columns=cols,
478 id='datatable',
479 cell_selectable=False,
480 editable=False,
481 css=[
482 {
483 'selector': '.dash-cell div.dash-cell-value',
484 'rule': '''display: inline;
485 white-space: inherit;
486 overflow: inherit;
487 text-overflow: inherit;'''
488 }
489 ],
490 style_data={
491 'whiteSpace': 'normal',
492 'height': 'auto',
493 'width': 'auto'
495 },
496 style_data_conditional=[
497 {
498 'if': {'row_index': 'odd'},
499 'color': COLOR_SCHEME['light1'],
500 'background': COLOR_SCHEME['grey1'],
501 },
502 {
503 'if': {'row_index': 'even'},
504 'color': COLOR_SCHEME['light1'],
505 'background': COLOR_SCHEME['bg']
506 },
507 {
508 'if': {'column_id': error_cols},
509 'color': COLOR_SCHEME['red2'],
510 }
511 ],
512 style_table={
513 'zIndex': '0',
514 'maxWidth': '99.5vw',
515 'maxHeight': '95vh',
516 'overflowX': 'auto',
517 'overflowY': 'auto',
518 'padding': '0px 4px 0px 4px',
519 'borderWidth': '0px 1px 0px 1px',
520 'borderColor': COLOR_SCHEME['grey1'],
521 },
522 style_header={
523 'color': COLOR_SCHEME['light2'],
524 'background': COLOR_SCHEME['grey1'],
525 'fontWeight': 'bold',
526 },
527 style_filter={
528 'color': COLOR_SCHEME['cyan2'],
529 'background': COLOR_SCHEME['bg']
530 },
531 style_cell={
532 'textAlign': 'left',
533 # 'minWidth': '105px',
534 'maxWidth': '300px',
535 'borderColor': COLOR_SCHEME['grey1'],
536 'border': '0px',
537 'height': '25px',
538 'padding': '3px 0px 3px 10px'
539 }
540 )
543def get_asset_graph(data):
544 # type: (List[Dict]) -> cyto.Cytoscape
545 '''
546 Creates asset graph data for cytoscape component.
548 Args:
549 data (list[dict]): List of dicts.
551 Raises:
552 KeyError: If asset_valid or asset_path keys not found.
554 Returns:
555 Cytoscape: Cytoscape graph.
556 '''
557 data_ = DataFrame(data) # type: DataFrame
558 cols = ['asset_path', 'asset_valid']
560 temp = data_.columns.tolist()
561 if cols[0] not in temp or cols[1] not in temp:
562 msg = f'Rows must contain {cols} keys. Keys found: {temp}.'
563 raise KeyError(msg)
565 data_ = data_[cols]
566 keys = data_.asset_path.tolist()
567 vals = data_.asset_valid.apply(lambda x: f'asset_valid: {x}').tolist()
568 data_ = dict(zip(keys, vals))
569 graph = blob_etl.BlobETL(data_).to_networkx_graph()
571 edges = []
572 for s, t in list(graph.edges):
573 s = re.sub('"|/asset_valid.*', '', s)
574 t = re.sub('"|/asset_valid.*', '', t)
575 edge = dict(group='edges', source=s, target=t)
576 edges.append(edge)
578 nodes = []
579 for n in list(graph.nodes):
580 attrs = graph.nodes.get(n)
582 color = COLOR_SCHEME['cyan2']
583 val = attrs.get('value', [None])[0]
584 if val == 'asset_valid: True':
585 color = COLOR_SCHEME['green2']
586 elif val == 'asset_valid: False':
587 color = COLOR_SCHEME['red2']
589 label = attrs['short_name']
590 if not label.startswith('"asset_valid'):
591 node = dict(id=n, group='nodes', label=label, color=color)
592 nodes.append(node)
594 nodes.extend(edges)
595 data_ = [{'data': x} for x in nodes]
597 root = data_[0]['data']['id']
598 return cyto.Cytoscape(
599 id='asset-graph',
600 elements=data_,
601 layout={'name': 'breadthfirst', 'roots': [root]},
602 style=dict(
603 width='98% !important',
604 height='98% !important',
605 position='relative !important'
606 ),
607 stylesheet=[
608 dict(
609 selector='node',
610 style={
611 'background-color': 'data(color)',
612 'color': COLOR_SCHEME['bg'],
613 'content': 'data(label)',
614 'padding': '10px 10px 10px 10px',
615 'shape': 'rectangle',
616 'text-halign': 'center',
617 'text-valign': 'center',
618 'width': 'label',
619 'height': 'label',
620 'font-size': '25px',
621 }
622 ),
623 dict(
624 selector='edge',
625 style={
626 'line-color': COLOR_SCHEME['grey2'],
627 }
628 )
629 ]
630 )