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
« prev ^ index » next coverage.py v7.1.0, created at 2023-11-15 00:54 +0000
1from typing import Any # noqa: F401
3from json import JSONDecodeError
4import json
6from pandasql import PandaSQLException
7from schematics.exceptions import DataError
8import flasgger as swg
9import flask
11from shekels.core.database import Database
12import shekels.server.server_tools as svt
13# ------------------------------------------------------------------------------
16'''
17Shekels REST API.
18'''
21def get_api():
22 # type: () -> Any
23 '''
24 Creates a Blueprint for the Shekels REST API.
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='')
37API = get_api()
40@API.route('/api')
41def api():
42 # type: () -> Any
43 '''
44 Route to Shekels API documentation.
46 Returns:
47 html: Flassger generated API page.
48 '''
49 return flask.redirect(flask.url_for('flasgger.apidocs'))
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.
85 Raises:
86 RuntimeError: If config is invalid.
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)
95 config = flask.request.get_json() # type: Any
96 config = json.loads(config)
97 if not isinstance(config, dict):
98 raise RuntimeError(msg)
100 API.database = Database(config)
101 API.config = API.database.config
103 return flask.Response(
104 response=json.dumps(dict(
105 message='Database initialized.',
106 config=API.config,
107 )),
108 mimetype='application/json'
109 )
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.
130 Raise:
131 RuntimeError: If database has not been initialized.
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)
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 )
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.
167 Raises:
168 RuntimeError: If database has not been initilaized.
169 RuntimeError: If database has not been updated.
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)
178 response = {} # type: Any
179 try:
180 response = API.database.read()
181 except Exception as error:
182 return svt.error_to_response(error)
184 response = {'response': response}
185 return flask.Response(
186 response=json.dumps(response),
187 mimetype='application/json'
188 )
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.
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)
228 if API.database is None:
229 msg = 'Database not initialized. Please call initialize.'
230 raise RuntimeError(msg)
232 if API.database.data is None:
233 msg = 'Database not updated. Please call update.'
234 raise RuntimeError(msg)
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 )
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.
251 Args:
252 error (DataError): Config validation error.
254 Returns:
255 Response: DataError response.
256 '''
257 return svt.error_to_response(error)
260@API.errorhandler(RuntimeError)
261def handle_runtime_error(error):
262 # type: (RuntimeError) -> flask.Response
263 '''
264 Handles runtime errors.
266 Args:
267 error (RuntimeError): Runtime error.
269 Returns:
270 Response: RuntimeError response.
271 '''
272 return svt.error_to_response(error)
275@API.errorhandler(JSONDecodeError)
276def handle_json_decode_error(error):
277 # type: (JSONDecodeError) -> flask.Response
278 '''
279 Handles JSON decode errors.
281 Args:
282 error (JSONDecodeError): JSON decode error.
284 Returns:
285 Response: JSONDecodeError response.
286 '''
287 return svt.error_to_response(error)
290@API.errorhandler(PandaSQLException)
291def handle_sql_error(error):
292 # type: (PandaSQLException) -> flask.Response
293 '''
294 Handles SQL errors.
296 Args:
297 error (PandaSQLException): SQL error.
299 Returns:
300 Response: PandaSQLException response.
301 '''
302 return svt.error_to_response(error)
303# ------------------------------------------------------------------------------
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)