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

1from typing import Any # noqa F401 

2 

3from json import JSONDecodeError 

4import json 

5 

6import numpy as np 

7import flask 

8import flasgger as swg 

9from schematics.exceptions import DataError, ValidationError 

10from werkzeug.exceptions import BadRequest 

11 

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# ------------------------------------------------------------------------------ 

18 

19 

20''' 

21Hidebound service API. 

22''' 

23 

24 

25API = flask.Blueprint('hidebound_api', __name__, url_prefix='') 

26 

27 

28@API.route('/api') 

29def api(): 

30 # type: () -> Any 

31 ''' 

32 Route to Hidebound API documentation. 

33 

34 Returns: 

35 html: Flassger generated API page. 

36 ''' 

37 # TODO: Test this with selenium. 

38 return flask.redirect(flask.url_for('flasgger.apidocs')) 

39 

40 

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. 

72 

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() 

83 

84 ext.hidebound.database = Database.from_config(config) 

85 

86 return flask.Response( 

87 response=json.dumps(dict(message='Database initialized.')), 

88 mimetype='application/json' 

89 ) 

90 

91 

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. 

109 

110 Returns: 

111 Response: Flask Response instance. 

112 ''' 

113 try: 

114 ext.hidebound.database.create() 

115 except RuntimeError: 

116 return hst.get_update_error() 

117 

118 return flask.Response( 

119 response=json.dumps(dict(message='Hidebound data created.')), 

120 mimetype='application/json' 

121 ) 

122 

123 

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. 

149 

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() 

162 

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) 

170 

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 ) 

177 

178 

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. 

196 

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 ) 

205 

206 

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. 

224 

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 ) 

233 

234 

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. 

252 

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) 

260 

261 return flask.Response( 

262 response=json.dumps(dict(message='Hidebound data exported.')), 

263 mimetype='application/json' 

264 ) 

265 

266 

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. 

298 

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() 

312 

313 if ext.hidebound.database.data is None: 

314 return hst.get_update_error() 

315 

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) 

322 

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 ) 

330 

331 

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. 

356 

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'] 

363 

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) 

369 

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 

376 

377 return flask.Response( 

378 response=json.dumps(dict( 

379 message='Workflow completed.', steps=steps)), 

380 mimetype='application/json' 

381 ) 

382 

383 

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. 

401 

402 Returns: 

403 Response: Flask Response instance. 

404 ''' 

405 return flask.Response( 

406 response=json.dumps(hblog.get_progress()), 

407 mimetype='application/json' 

408 ) 

409 

410 

411@API.errorhandler(DataError) 

412def handle_data_error(error): 

413 # type: (DataError) -> flask.Response 

414 ''' 

415 Handles errors raise by config validation. 

416 

417 Args: 

418 error (DataError): Config validation error. 

419 

420 Returns: 

421 Response: DataError response. 

422 ''' 

423 return hst.error_to_response(error) 

424 

425 

426@API.errorhandler(KeyError) 

427def handle_key_error(error): 

428 # type: (KeyError) -> flask.Response 

429 ''' 

430 Handles key errors. 

431 

432 Args: 

433 error (KeyError): Key error. 

434 

435 Returns: 

436 Response: KeyError response. 

437 ''' 

438 return hst.error_to_response(error) 

439 

440 

441@API.errorhandler(TypeError) 

442def handle_type_error(error): 

443 # type: (TypeError) -> flask.Response 

444 ''' 

445 Handles key errors. 

446 

447 Args: 

448 error (TypeError): Key error. 

449 

450 Returns: 

451 Response: TypeError response. 

452 ''' 

453 return hst.error_to_response(error) 

454 

455 

456@API.errorhandler(JSONDecodeError) 

457def handle_json_decode_error(error): 

458 # type: (JSONDecodeError) -> flask.Response 

459 ''' 

460 Handles key errors. 

461 

462 Args: 

463 error (JSONDecodeError): Key error. 

464 

465 Returns: 

466 Response: JSONDecodeError response. 

467 ''' 

468 return hst.error_to_response(error) 

469# ------------------------------------------------------------------------------ 

470 

471 

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)