Coverage for /home/ubuntu/hidebound/python/hidebound/server/api.py: 99%
136 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 # noqa F401
3from json import JSONDecodeError
4import json
6import numpy as np
7import flask
8import flasgger as swg
9from schematics.exceptions import DataError, ValidationError
10from werkzeug.exceptions import BadRequest
12from hidebound.core.database import Database
13import hidebound.core.logging as hblog
14import hidebound.core.validators as vd
15import hidebound.server.extensions as ext
16import hidebound.server.server_tools as hst
17# ------------------------------------------------------------------------------
20'''
21Hidebound service API.
22'''
25API = flask.Blueprint('hidebound_api', __name__, url_prefix='')
28@API.route('/api')
29def api():
30 # type: () -> Any
31 '''
32 Route to Hidebound API documentation.
34 Returns:
35 html: Flassger generated API page.
36 '''
37 # TODO: Test this with selenium.
38 return flask.redirect(flask.url_for('flasgger.apidocs'))
41@API.route('/api/initialize', methods=['POST'])
42@swg.swag_from(dict(
43 parameters=[
44 dict(
45 name='config',
46 type='dict',
47 description='Hidebound configuration.',
48 required=True,
49 )
50 ],
51 responses={
52 200: dict(
53 description='Hidebound database successfully initialized.',
54 content='application/json',
55 ),
56 400: dict(
57 description='Invalid configuration.',
58 example=dict(
59 error='''
60DataError(
61 {'write_mode': ValidationError([ErrorMessage("foo is not in ['copy', 'move'].", None)])}
62)'''[1:],
63 success=False,
64 )
65 )
66 }
67))
68def initialize():
69 # type: () -> flask.Response
70 '''
71 Initialize database with given config.
73 Returns:
74 Response: Flask Response instance.
75 '''
76 try:
77 config = flask.request.get_json() # type: Any
78 config = json.loads(config)
79 except (BadRequest, JSONDecodeError, TypeError):
80 return hst.get_config_error()
81 if not isinstance(config, dict):
82 return hst.get_config_error()
84 ext.hidebound.database = Database.from_config(config)
86 return flask.Response(
87 response=json.dumps(dict(message='Database initialized.')),
88 mimetype='application/json'
89 )
92@API.route('/api/create', methods=['POST'])
93@swg.swag_from(dict(
94 parameters=[],
95 responses={
96 200: dict(
97 description='Hidebound data successfully deleted.',
98 content='application/json',
99 ),
100 500: dict(
101 description='Internal server error.',
102 )
103 }
104))
105def create():
106 # type: () -> flask.Response
107 '''
108 Create hidebound data.
110 Returns:
111 Response: Flask Response instance.
112 '''
113 try:
114 ext.hidebound.database.create()
115 except RuntimeError:
116 return hst.get_update_error()
118 return flask.Response(
119 response=json.dumps(dict(message='Hidebound data created.')),
120 mimetype='application/json'
121 )
124@API.route('/api/read', methods=['GET', 'POST'])
125@swg.swag_from(dict(
126 parameters=[
127 dict(
128 name='group_by_asset',
129 type='bool',
130 description='Whether to group resulting data by asset.',
131 required=False,
132 default=False,
133 ),
134 ],
135 responses={
136 200: dict(
137 description='Read all data from database.',
138 content='application/json',
139 ),
140 500: dict(
141 description='Internal server error.',
142 )
143 }
144))
145def read():
146 # type: () -> flask.Response
147 '''
148 Read database.
150 Returns:
151 Response: Flask Response instance.
152 '''
153 params = flask.request.get_json() # type: Any
154 group_by_asset = False
155 if params not in [None, {}]:
156 try:
157 params = json.loads(params)
158 group_by_asset = params['group_by_asset']
159 assert isinstance(group_by_asset, bool)
160 except (JSONDecodeError, TypeError, KeyError, AssertionError):
161 return hst.get_read_error()
163 response = {} # type: Any
164 try:
165 response = ext.hidebound.database.read(group_by_asset=group_by_asset)
166 except Exception as error:
167 if isinstance(error, RuntimeError):
168 return hst.get_update_error()
169 return hst.error_to_response(error)
171 response = response.replace({np.nan: None}).to_dict(orient='records')
172 response = {'response': response}
173 return flask.Response(
174 response=json.dumps(response),
175 mimetype='application/json'
176 )
179@API.route('/api/update', methods=['POST'])
180@swg.swag_from(dict(
181 parameters=[],
182 responses={
183 200: dict(
184 description='Hidebound database successfully updated.',
185 content='application/json',
186 ),
187 500: dict(
188 description='Internal server error.',
189 )
190 }
191))
192def update():
193 # type: () -> flask.Response
194 '''
195 Update database.
197 Returns:
198 Response: Flask Response instance.
199 '''
200 ext.hidebound.database.update()
201 return flask.Response(
202 response=json.dumps(dict(message='Database updated.')),
203 mimetype='application/json'
204 )
207@API.route('/api/delete', methods=['POST'])
208@swg.swag_from(dict(
209 parameters=[],
210 responses={
211 200: dict(
212 description='Hidebound data successfully deleted.',
213 content='application/json',
214 ),
215 500: dict(
216 description='Internal server error.',
217 )
218 }
219))
220def delete():
221 # type: () -> flask.Response
222 '''
223 Delete hidebound data.
225 Returns:
226 Response: Flask Response instance.
227 '''
228 ext.hidebound.database.delete()
229 return flask.Response(
230 response=json.dumps(dict(message='Hidebound data deleted.')),
231 mimetype='application/json'
232 )
235@API.route('/api/export', methods=['POST'])
236@swg.swag_from(dict(
237 parameters=[],
238 responses={
239 200: dict(
240 description='Hidebound data successfully exported.',
241 content='application/json',
242 ),
243 500: dict(
244 description='Internal server error.',
245 )
246 }
247))
248def export():
249 # type: () -> flask.Response
250 '''
251 Export hidebound data.
253 Returns:
254 Response: Flask Response instance.
255 '''
256 try:
257 ext.hidebound.database.export()
258 except Exception as error:
259 return hst.error_to_response(error)
261 return flask.Response(
262 response=json.dumps(dict(message='Hidebound data exported.')),
263 mimetype='application/json'
264 )
267@API.route('/api/search', methods=['POST'])
268@swg.swag_from(dict(
269 parameters=[
270 dict(
271 name='query',
272 type='string',
273 description='SQL query for searching database. Make sure to use "FROM data" in query.',
274 required=True,
275 ),
276 dict(
277 name='group_by_asset',
278 type='bool',
279 description='Whether to group resulting search by asset.',
280 required=False,
281 default=False,
282 ),
283 ],
284 responses={
285 200: dict(
286 description='Returns a list of JSON compatible dictionaries, one per row.',
287 content='application/json',
288 ),
289 500: dict(
290 description='Internal server error.',
291 )
292 }
293))
294def search():
295 # type: () -> flask.Response
296 '''
297 Search database with a given SQL query.
299 Returns:
300 Response: Flask Response instance.
301 '''
302 params = flask.request.get_json() # type: Any
303 group_by_asset = False
304 try:
305 params = json.loads(params)
306 query = params['query']
307 if 'group_by_asset' in params.keys():
308 group_by_asset = params['group_by_asset']
309 assert isinstance(group_by_asset, bool)
310 except (JSONDecodeError, TypeError, KeyError, AssertionError):
311 return hst.get_search_error()
313 if ext.hidebound.database.data is None:
314 return hst.get_update_error()
316 response = None
317 try:
318 response = ext.hidebound.database \
319 .search(query, group_by_asset=group_by_asset)
320 except Exception as e:
321 return hst.error_to_response(e)
323 response.asset_valid = response.asset_valid.astype(bool)
324 response = response.replace({np.nan: None}).to_dict(orient='records')
325 response = {'response': response}
326 return flask.Response(
327 response=json.dumps(response),
328 mimetype='application/json'
329 )
332@API.route('/api/workflow', methods=['POST'])
333@swg.swag_from(dict(
334 parameters=[
335 dict(
336 name='steps',
337 type='list',
338 description='Ordered list of API calls.',
339 required=True,
340 )
341 ],
342 responses={
343 200: dict(
344 description='Hidebound workflow ran successfully.',
345 content='application/json',
346 ),
347 500: dict(
348 description='Internal server error.',
349 )
350 }
351))
352def workflow():
353 # type: () -> flask.Response
354 '''
355 Run given hidebound workflow.
357 Returns:
358 Response: Flask Response instance.
359 '''
360 params = flask.request.get_json() # type: Any
361 params = json.loads(params)
362 steps = params['steps']
364 # get and validate workflow steps
365 try:
366 vd.is_workflow(steps)
367 except ValidationError as e:
368 return hst.error_to_response(e)
370 # run through workflow
371 for step in steps:
372 try:
373 getattr(ext.hidebound.database, step)()
374 except Exception as error: # pragma: no cover
375 return hst.error_to_response(error) # pragma: no cover
377 return flask.Response(
378 response=json.dumps(dict(
379 message='Workflow completed.', steps=steps)),
380 mimetype='application/json'
381 )
384@API.route('/api/progress', methods=['GET', 'POST'])
385@swg.swag_from(dict(
386 parameters=[],
387 responses={
388 200: dict(
389 description='Current progress of Hidebound.',
390 content='application/json',
391 ),
392 500: dict(
393 description='Internal server error.',
394 )
395 }
396))
397def progress():
398 # type: () -> flask.Response
399 '''
400 Get hidebound app progress.
402 Returns:
403 Response: Flask Response instance.
404 '''
405 return flask.Response(
406 response=json.dumps(hblog.get_progress()),
407 mimetype='application/json'
408 )
411@API.errorhandler(DataError)
412def handle_data_error(error):
413 # type: (DataError) -> flask.Response
414 '''
415 Handles errors raise by config validation.
417 Args:
418 error (DataError): Config validation error.
420 Returns:
421 Response: DataError response.
422 '''
423 return hst.error_to_response(error)
426@API.errorhandler(KeyError)
427def handle_key_error(error):
428 # type: (KeyError) -> flask.Response
429 '''
430 Handles key errors.
432 Args:
433 error (KeyError): Key error.
435 Returns:
436 Response: KeyError response.
437 '''
438 return hst.error_to_response(error)
441@API.errorhandler(TypeError)
442def handle_type_error(error):
443 # type: (TypeError) -> flask.Response
444 '''
445 Handles key errors.
447 Args:
448 error (TypeError): Key error.
450 Returns:
451 Response: TypeError response.
452 '''
453 return hst.error_to_response(error)
456@API.errorhandler(JSONDecodeError)
457def handle_json_decode_error(error):
458 # type: (JSONDecodeError) -> flask.Response
459 '''
460 Handles key errors.
462 Args:
463 error (JSONDecodeError): Key error.
465 Returns:
466 Response: JSONDecodeError response.
467 '''
468 return hst.error_to_response(error)
469# ------------------------------------------------------------------------------
472API.register_error_handler(500, handle_data_error)
473API.register_error_handler(500, handle_key_error)
474API.register_error_handler(500, handle_type_error)
475API.register_error_handler(500, handle_json_decode_error)