Coverage for /home/ubuntu/shekels/python/shekels/server/app.py: 100%
118 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-11-15 00:54 +0000
« prev ^ index » next coverage.py v7.1.0, created at 2023-11-15 00:54 +0000
1from typing import Any, Dict, List, Tuple, Union # noqa: F401
3from copy import copy
4from copy import deepcopy
5from pathlib import Path
6import os
8from dash.dependencies import Input, Output, State
9from flask_caching import Cache
10import dash
11from dash import dash_table
12from dash import dcc
13from dash import html
14from flask_healthz import healthz, HealthError
15import flasgger as swg
16import flask
17import flask_monitoringdashboard as fmdb
18import jsoncomment as jsonc
20from shekels.server.api import API
21import shekels.core.config as cfg
22import shekels.server.components as svc
23import shekels.server.event_listener as sev
24import shekels.server.server_tools as svt
25# ------------------------------------------------------------------------------
28'''
29Shekels app used for displaying and interacting with database.
30'''
33def liveness():
34 # type: () -> None
35 '''Liveness probe for kubernetes.'''
36 pass
39def readiness():
40 # type: () -> None
41 '''
42 Readiness probe for kubernetes.
44 Raises:
45 HealthError: If api is not availiable.
46 '''
47 if not hasattr(APP, 'api'):
48 raise HealthError('App is missing API.')
51def get_app():
52 # type: () -> dash.Dash
53 '''
54 Creates a Shekels app.
56 Returns:
57 Dash: Dash app.
58 '''
59 flask_app = flask.Flask('Shekels')
60 swg.Swagger(flask_app)
61 flask_app.register_blueprint(API)
63 # healthz endpoints
64 flask_app.register_blueprint(healthz, url_prefix="/healthz")
65 flask_app.config.update(HEALTHZ={
66 "live": liveness,
67 "ready": readiness,
68 })
70 # flask monitoring
71 fmdb.config.link = 'monitor'
72 fmdb.config.monitor_level = 3
73 fmdb.config.git = 'https://theNewFlesh.github.io/shekels/'
74 fmdb.config.brand_name = 'Shekels Monitoring Dashboard'
75 fmdb.config.title_name = 'Shekels Monitoring Dashboard'
76 fmdb.config.description = 'Monitor Shekels App Performance'
77 fmdb.config.enable_logging = False
78 fmdb.config.show_login_banner = True
79 fmdb.config.show_login_footer = True
80 fmdb.config.database_name = 'sqlite:////tmp/shekels_monitor.db'
81 fmdb.bind(flask_app)
83 app = svc.get_dash_app(flask_app)
84 app.api = API
85 app.client = flask_app.test_client()
86 app.cache = Cache(flask_app, config={'CACHE_TYPE': 'SimpleCache'})
88 # register event listener
89 app.event_listener = sev.EventListener(app, {}) \
90 .listen('config-query', svt.config_query_event) \
91 .listen('config-search-button', svt.config_query_event) \
92 .listen('config-table', svt.config_edit_event) \
93 .listen('query', svt.data_query_event) \
94 .listen('search-button', svt.data_query_event) \
95 .listen('init-button', svt.init_event) \
96 .listen('update-button', svt.update_event) \
97 .listen('upload', svt.upload_event) \
98 .listen('save-button', svt.save_event)
99 return app
102APP = get_app()
105@APP.server.route('/static/<stylesheet>')
106def serve_stylesheet(stylesheet):
107 # type: (str) -> flask.Response
108 '''
109 Serve stylesheet to app.
111 Args:
112 stylesheet (str): stylesheet filename.
114 Returns:
115 flask.Response: Response.
116 '''
117 temp = APP.api.config or {}
118 color_scheme = copy(cfg.COLOR_SCHEME)
119 cs = temp.get('color_scheme', {})
120 color_scheme.update(cs)
122 params = dict(
123 COLOR_SCHEME=color_scheme,
124 FONT_FAMILY=temp.get('font_family', cfg.Config.font_family.default),
125 )
126 content = svt.render_template('style.css.j2', params)
127 return flask.Response(content, mimetype='text/css')
130# EVENTS------------------------------------------------------------------------
131@APP.callback(
132 Output('store', 'data'),
133 [
134 Input('config-query', 'value'),
135 Input('config-search-button', 'n_clicks'),
136 Input('config-table', 'data'),
137 Input('query', 'value'),
138 Input('init-button', 'n_clicks'),
139 Input('update-button', 'n_clicks'),
140 Input('search-button', 'n_clicks'),
141 Input('upload', 'contents'),
142 Input('save-button', 'n_clicks'),
143 ],
144 [State('config-table', 'data_previous')]
145)
146def on_event(*inputs):
147 # type: (Tuple[Any, ...]) -> Dict[str, Any]
148 '''
149 Update database instance, and updates store with input data.
151 Args:
152 inputs (tuple): Input elements.
154 Returns:
155 dict: Store data.
156 '''
157 event = dash.callback_context.triggered[0]['prop_id'].split('.')[0]
158 value = dash.callback_context.triggered[0]['value']
159 store = APP.event_listener.store
161 if event == 'config-table':
162 value = dict(new=value[0], old=inputs[-1][0])
164 if event == 'update-button' and store.get('/api/initialize') is None:
165 APP.event_listener.emit('init-button', None)
166 store = APP.event_listener.emit(event, value).store
167 return store
170@APP.callback(
171 Output('plots-content', 'children'),
172 [Input('store', 'data')]
173)
174@APP.cache.memoize(100)
175def on_plots_update(store):
176 # type: (Dict) -> dash_table.DataTable
177 '''
178 Updates plots with read information from store.
180 Args:
181 store (dict): Store data.
183 Returns:
184 list[dcc.Graph]: Plots.
185 '''
186 comp = svt.solve_component_state(store)
187 if comp is not None:
188 return comp
189 config = store.get('/config', deepcopy(APP.api.config))
190 plots = config.get('plots', [])
191 return svc.get_plots(store['/api/search']['response'], plots)
194@APP.callback(
195 Output('data-content', 'children'),
196 [Input('store', 'data')]
197)
198@APP.cache.memoize(100)
199def on_datatable_update(store):
200 # type: (Dict) -> dash_table.DataTable
201 '''
202 Updates datatable with read information from store.
204 Args:
205 store (dict): Store data.
207 Returns:
208 DataTable: Dash DataTable.
209 '''
210 comp = svt.solve_component_state(store)
211 if comp is not None:
212 return comp
213 return svc.get_datatable(store['/api/search']['response'])
216@APP.callback(
217 Output('config-content', 'children'),
218 [Input('store', 'data')]
219)
220@APP.cache.memoize(100)
221def on_config_update(store):
222 # type: (Dict[str, Any]) -> List[flask.Response]
223 '''
224 Updates config table with config information from store.
226 Args:
227 store (dict): Store data.
229 Returns:
230 flask.Response: Response.
231 '''
232 config = store.get('/config', deepcopy(APP.api.config))
233 store['/config'] = config
234 store['/config/search'] = store.get('/config/search', store['/config'])
235 comp = svt.solve_component_state(store, config=True)
236 if comp is not None:
237 return [
238 comp,
239 html.Div(className='dummy', children=[
240 dash_table.DataTable(id='config-table')
241 ])
242 ]
243 return svc.get_key_value_table(
244 store['/config/search'],
245 id_='config',
246 header='config',
247 editable=True,
248 )
251@APP.callback(
252 Output('content', 'children'),
253 [Input('tabs', 'value')],
254 [State('store', 'data')]
255)
256def on_get_tab(tab, store):
257 # type: (str, Dict) -> Union[flask.Response, List, None]
258 '''
259 Serve content for app tabs.
261 Args:
262 tab (str): Name of tab to render.
263 store (dict): Store.
265 Returns:
266 flask.Response: Response.
267 '''
268 store = store or {}
270 if tab == 'plots':
271 query = store.get('/api/search/query', APP.api.config['default_query'])
272 return svc.get_plots_tab(query)
274 elif tab == 'data':
275 query = store.get('/api/search/query', APP.api.config['default_query'])
276 return svc.get_data_tab(query)
278 elif tab == 'config':
279 config = store.get('/config', deepcopy(APP.api.config))
280 return svc.get_config_tab(config)
282 elif tab == 'api': # pragma: no cover
283 return dcc.Location(id='api', pathname='/api')
285 elif tab == 'docs': # pragma: no cover
286 return dcc.Location(
287 id='docs',
288 href='https://theNewFlesh.github.io/shekels/'
289 )
291 elif tab == 'monitor': # pragma: no cover
292 return dcc.Location(id='monitor', pathname='/monitor')
293# ------------------------------------------------------------------------------
296def run(app, config_path, debug=False, test=False):
297 '''
298 Runs a given Shekels app.
300 Args:
301 Dash: Shekels app.
302 config_path (str or Path): Path to configuration JSON.
303 debug (bool, optional): Whether debug mode is turned on. Default: False.
304 test (bool, optional): Calls app.run_server if False. Default: False.
305 '''
306 config_path = Path(config_path).as_posix()
307 with open(config_path) as f:
308 config = jsonc.JsonComment().load(f)
309 app.api.config = config
310 app.api.config_path = config_path
311 app.event_listener.state.clear()
312 app.event_listener.state.append({})
313 if not test:
314 app.run_server(debug=debug, host='0.0.0.0', port=8080) # pragma: no cover
317if __name__ == '__main__': # pragma: no cover
318 debug = 'DEBUG_MODE' in os.environ.keys()
320 config_path = '/mnt/storage/shekels_config.json'
321 if debug:
322 config_path = '/mnt/storage/test_config.json'
323 run(APP, config_path, debug=debug)