Coverage for /home/ubuntu/shekels/python/shekels/server/api.py: 100%

91 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-11-15 00:54 +0000

1from typing import Any # noqa: F401 

2 

3from json import JSONDecodeError 

4import json 

5 

6from pandasql import PandaSQLException 

7from schematics.exceptions import DataError 

8import flasgger as swg 

9import flask 

10 

11from shekels.core.database import Database 

12import shekels.server.server_tools as svt 

13# ------------------------------------------------------------------------------ 

14 

15 

16''' 

17Shekels REST API. 

18''' 

19 

20 

21def get_api(): 

22 # type: () -> Any 

23 ''' 

24 Creates a Blueprint for the Shekels REST API. 

25 

26 Returns: 

27 flask.Blueprint: API Blueprint. 

28 ''' 

29 class ApiBlueprint(flask.Blueprint): 

30 def __init__(self, *args, **kwargs): 

31 super().__init__(*args, **kwargs) 

32 self.database = None 

33 self.config = None 

34 return ApiBlueprint('api', __name__, url_prefix='') 

35 

36 

37API = get_api() 

38 

39 

40@API.route('/api') 

41def api(): 

42 # type: () -> Any 

43 ''' 

44 Route to Shekels API documentation. 

45 

46 Returns: 

47 html: Flassger generated API page. 

48 ''' 

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

50 

51 

52@API.route('/api/initialize', methods=['POST']) 

53@swg.swag_from(dict( 

54 parameters=[ 

55 dict( 

56 name='config', 

57 type='string', 

58 description='Database configuration as JSON string.', 

59 required=True, 

60 default='', 

61 ) 

62 ], 

63 responses={ 

64 200: dict( 

65 description='Shekels database successfully initialized.', 

66 content='application/json', 

67 ), 

68 400: dict( 

69 description='Invalid configuration.', 

70 example=dict( 

71 error=''' 

72DataError( 

73 {'data_path': ValidationError([ErrorMessage("/foo.bar is not in a valid CSV file.", None)])} 

74)'''[1:], 

75 success=False, 

76 ) 

77 ) 

78 } 

79)) 

80def initialize(): 

81 # type: () -> flask.Response 

82 ''' 

83 Initialize database with given config. 

84 

85 Raises: 

86 RuntimeError: If config is invalid. 

87 

88 Returns: 

89 Response: Flask Response instance. 

90 ''' 

91 msg = 'Please supply a config dictionary.' 

92 if len(flask.request.get_data()) == 0: 

93 raise RuntimeError(msg) 

94 

95 config = flask.request.get_json() # type: Any 

96 config = json.loads(config) 

97 if not isinstance(config, dict): 

98 raise RuntimeError(msg) 

99 

100 API.database = Database(config) 

101 API.config = API.database.config 

102 

103 return flask.Response( 

104 response=json.dumps(dict( 

105 message='Database initialized.', 

106 config=API.config, 

107 )), 

108 mimetype='application/json' 

109 ) 

110 

111 

112@API.route('/api/update', methods=['POST']) 

113@swg.swag_from(dict( 

114 parameters=[], 

115 responses={ 

116 200: dict( 

117 description='Shekels database successfully updated.', 

118 content='application/json', 

119 ), 

120 500: dict( 

121 description='Internal server error.', 

122 ) 

123 } 

124)) 

125def update(): 

126 # type: () -> flask.Response 

127 ''' 

128 Update database. 

129 

130 Raise: 

131 RuntimeError: If database has not been initialized. 

132 

133 Returns: 

134 Response: Flask Response instance. 

135 ''' 

136 if API.database is None: 

137 msg = 'Database not initialized. Please call initialize.' 

138 raise RuntimeError(msg) 

139 

140 API.database.update() 

141 return flask.Response( 

142 response=json.dumps(dict( 

143 message='Database updated.', 

144 config=API.config, 

145 )), 

146 mimetype='application/json' 

147 ) 

148 

149 

150@API.route('/api/read', methods=['GET', 'POST']) 

151@swg.swag_from(dict( 

152 responses={ 

153 200: dict( 

154 description='Read all data from database.', 

155 content='application/json', 

156 ), 

157 500: dict( 

158 description='Internal server error.', 

159 ) 

160 } 

161)) 

162def read(): 

163 # type: () -> flask.Response 

164 ''' 

165 Read database. 

166 

167 Raises: 

168 RuntimeError: If database has not been initilaized. 

169 RuntimeError: If database has not been updated. 

170 

171 Returns: 

172 Response: Flask Response instance. 

173 ''' 

174 if API.database is None: 

175 msg = 'Database not initialized. Please call initialize.' 

176 raise RuntimeError(msg) 

177 

178 response = {} # type: Any 

179 try: 

180 response = API.database.read() 

181 except Exception as error: 

182 return svt.error_to_response(error) 

183 

184 response = {'response': response} 

185 return flask.Response( 

186 response=json.dumps(response), 

187 mimetype='application/json' 

188 ) 

189 

190 

191@API.route('/api/search', methods=['POST']) 

192@swg.swag_from(dict( 

193 parameters=[ 

194 dict( 

195 name='query', 

196 type='string', 

197 description='SQL query for searching database. Make sure to use "FROM data" in query.', 

198 required=True, 

199 ) 

200 ], 

201 responses={ 

202 200: dict( 

203 description='Returns a list of JSON compatible dictionaries, one per row.', 

204 content='application/json', 

205 ), 

206 500: dict( 

207 description='Internal server error.', 

208 ) 

209 } 

210)) 

211def search(): 

212 # type: () -> flask.Response 

213 ''' 

214 Search database with a given SQL query. 

215 

216 Returns: 

217 Response: Flask Response instance. 

218 ''' 

219 params = flask.request.get_json() # type: Any 

220 params = json.loads(params) 

221 try: 

222 query = params['query'] 

223 except KeyError: 

224 msg = 'Please supply valid search params in the form ' 

225 msg += '{"query": SQL query}.' 

226 raise RuntimeError(msg) 

227 

228 if API.database is None: 

229 msg = 'Database not initialized. Please call initialize.' 

230 raise RuntimeError(msg) 

231 

232 if API.database.data is None: 

233 msg = 'Database not updated. Please call update.' 

234 raise RuntimeError(msg) 

235 

236 response = API.database.search(query) # type: Any 

237 response = {'response': response} 

238 return flask.Response( 

239 response=json.dumps(response), 

240 mimetype='application/json' 

241 ) 

242 

243 

244# ERROR-HANDLERS---------------------------------------------------------------- 

245@API.errorhandler(DataError) 

246def handle_data_error(error): 

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

248 ''' 

249 Handles errors raise by config validation. 

250 

251 Args: 

252 error (DataError): Config validation error. 

253 

254 Returns: 

255 Response: DataError response. 

256 ''' 

257 return svt.error_to_response(error) 

258 

259 

260@API.errorhandler(RuntimeError) 

261def handle_runtime_error(error): 

262 # type: (RuntimeError) -> flask.Response 

263 ''' 

264 Handles runtime errors. 

265 

266 Args: 

267 error (RuntimeError): Runtime error. 

268 

269 Returns: 

270 Response: RuntimeError response. 

271 ''' 

272 return svt.error_to_response(error) 

273 

274 

275@API.errorhandler(JSONDecodeError) 

276def handle_json_decode_error(error): 

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

278 ''' 

279 Handles JSON decode errors. 

280 

281 Args: 

282 error (JSONDecodeError): JSON decode error. 

283 

284 Returns: 

285 Response: JSONDecodeError response. 

286 ''' 

287 return svt.error_to_response(error) 

288 

289 

290@API.errorhandler(PandaSQLException) 

291def handle_sql_error(error): 

292 # type: (PandaSQLException) -> flask.Response 

293 ''' 

294 Handles SQL errors. 

295 

296 Args: 

297 error (PandaSQLException): SQL error. 

298 

299 Returns: 

300 Response: PandaSQLException response. 

301 ''' 

302 return svt.error_to_response(error) 

303# ------------------------------------------------------------------------------ 

304 

305 

306API.register_error_handler(500, handle_data_error) 

307API.register_error_handler(500, handle_runtime_error) 

308API.register_error_handler(500, handle_json_decode_error) 

309API.register_error_handler(500, handle_sql_error)