-
Notifications
You must be signed in to change notification settings - Fork 0
/
api.py
329 lines (263 loc) · 11.4 KB
/
api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
from flask import Flask, jsonify, request, make_response
from flask_restplus import Api, Resource, reqparse
from functools import cache, wraps
from flask_caching import Cache
from functools import wraps
from json import loads
import mysql.connector
import logging
import urllib
import jwt
# Flask
app = Flask(__name__)
config= {
"RESTPLUS_VALIDATE": True,
"SECRET_KEY": "javierorp2021",
"CACHE_TYPE": "SimpleCache",
"CACHE_DEFAULT_TIMEOUT": 600 # 10 min
}
app.config.from_mapping(config)
cache = Cache(app)
# Swagger
authorizations = {
"apikey": {
"type": "apiKey",
"in": "header",
"name": "SESSION"
}
}
api = Api(app, version="1.0", title="API Electricity data",
description="Get electricity data from server.\nAuthor: Javier Orti Priego (javierorp)",
authorizations=authorizations)
name_space = api.namespace('apielec', description='APIs to get electricity data from server.')
# Expected call parameters.
parserId = reqparse.RequestParser()
parserId.add_argument("id", type=str, help="Specifies the Id associated with the record. Accepts multiple identifiers separated by commas.")
parserDate = reqparse.RequestParser()
parserDate.add_argument("date", type=str, help="Date and time in the format 'YYYY-MM-DD HH:MM:SS' to be searched. Accepts '%' sign and '_' sign.")
parserRange = reqparse.RequestParser()
parserRange.add_argument("date", type=str, help="Initial date and time in the format 'YYYY-MM-DD HH:MM:SS' to be searched. Accepts '%' sign and '_' sign.")
parserRange.add_argument("end_date", type=str, help="End date and time in the format 'YYYY-MM-DD HH:MM:SS' to be searched. Accepts '%' sign and '_' sign.")
conn = "" # DB connection
columns = [] # Columns in db_tab
tokens = [] # Tokens allowed
# Database connection info
db_host = "localhost"
db_user = "blue"
db_pass = "blue21"
db_name = "elecprod"
db_tab = "consumpdata"
@app.route("/login", methods=["GET", "POST"])
def loging():
"""Checks that the user and password are valid and returns a token to be used"""
auth = request.authorization
if auth is not None and check_user(auth):
token = jwt.encode({"user": auth.username}, app.config["SECRET_KEY"], algorithm="HS256")
if token not in tokens:
tokens.append(token)
return jsonify({"Authorization_type": "API Key", "Key": "SESSION", "In": "header", "value_token": token})
return make_response(jsonify({"status": "ERROR", "statusCode": "401", "message": "Invalid user and/or password"})), 401, {"WWW-Authenticate": "Basic realm='Login Required'"}
def token_required(f):
"""Require a valid token to use the api"""
@wraps(f)
def decorated(*args, **kwargs):
token = None
if "SESSION" in request.headers:
token = request.headers["SESSION"]
if not token:
name_space.abort(405, status = "Token is missing", statusCode = "405")
if token not in tokens:
name_space.abort(406, status = "Invalid token", statusCode = "406")
return f(*args, **kwargs)
return decorated
def cache_key():
"""Allow to cache the solution."""
try:
args = request.get_json()
if args is None:
args = dict(request.args)
key = request.path
if args:
key += '?' + urllib.parse.urlencode([
(k, v) for k in sorted(args) for v in args[k]
])
return key
except KeyError as err:
name_space.abort(500, status = "Unable to obtain the data", statusCode = "500")
except Exception as err:
logging.error(err)
name_space.abort(400, status = "Unable to obtain the data", statusCode = "400")
@name_space.route("/ping") # /apielec/ping
class Ping(Resource):
@api.doc(responses={ 200: 'OK', 400: 'Invalid argument', 500: 'Mapping Key Error', 405: 'Token is missing', 406: 'Invalid token' })
@api.doc(security='apikey')
@token_required
def get(self):
"""Check if the server is responding."""
return format_result(status="OK", msg="I'm here!!")
@name_space.route("/getData") # /apielec/getData
class GetData(Resource):
@api.doc(responses={ 200: 'OK', 405: 'Token is missing', 406: 'Invalid token' })
@api.doc(security='apikey')
@token_required
@cache.cached(key_prefix=cache_key)
def get(self):
"""Get all the records in the database."""
try:
cursor = conn.cursor()
cursor.execute("SELECT * FROM {0}.{1};".format(db_name, db_tab))
rows = cursor.fetchall()
cursor.close()
return format_result(status="OK", msg="", rows=rows)
except KeyError as err:
name_space.abort(500, err.__doc__, status = "Unable to obtain the data", statusCode = "500")
except Exception as err:
logging.error(err)
name_space.abort(400, err.__doc__, status = "Unable to obtain the data", statusCode = "400")
@name_space.route("/getDataById") # /apielec/getDataById
class GetDataById(Resource):
@api.doc(responses={ 200: 'OK', 400: 'Invalid argument', 500: 'Mapping Key Error', 405: 'Token is missing', 406: 'Invalid token' })
@api.doc(security='apikey')
@token_required
@name_space.expect(parserId)
@cache.cached(key_prefix=cache_key)
def get(self):
"""Get all records in the database that match the id or identifiers.
Accepts multiple identifiers separated by commas. Eg: "1,2,3,4".
"""
try:
args = parserId.parse_args(strict=True)
id = args["id"]
cursor = conn.cursor()
cursor.execute("SELECT * FROM {0}.{1} where id in ({2});".format(db_name, db_tab, id))
rows = cursor.fetchall()
cursor.close()
return format_result(status="OK", msg="", rows=rows)
except KeyError as err:
name_space.abort(500, err.__doc__, status = "Unable to obtain the data", statusCode = "500")
except Exception as err:
logging.error(err)
name_space.abort(400, err.__doc__, status = "Unable to obtain the data", statusCode = "400")
@name_space.route("/getDataByDate") # /apielec/getDataByDate
class GetDataByDate(Resource):
@api.doc(responses={ 200: 'OK', 400: 'Invalid argument', 500: 'Mapping Key Error', 405: 'Token is missing', 406: 'Invalid token' })
@api.doc(security='apikey')
@token_required
@name_space.expect(parserDate)
@cache.cached(key_prefix=cache_key)
def get(self):
"""Get all records in the database that match the date.
Accepts '%' sign and '_' sign.
"""
try:
args = parserDate.parse_args(strict=True)
date = args["date"]
cursor = conn.cursor()
cursor.execute("SELECT * FROM {0}.{1} where date like '{2}';".format(db_name, db_tab, date))
rows = cursor.fetchall()
cursor.close()
return format_result(status="OK", msg="", rows=rows)
except KeyError as err:
name_space.abort(500, err.__doc__, status = "Unable to obtain the data", statusCode = "500")
except Exception as err:
logging.error(err)
name_space.abort(400, err.__doc__, status = "Unable to obtain the data", statusCode = "400")
@name_space.route("/getDataByRange") # /apielec/getDataByRange
class GetDataByRange(Resource):
@api.doc(responses={ 200: 'OK', 400: 'Invalid argument', 500: 'Mapping Key Error', 405: 'Token is missing', 406: 'Invalid token' })
@api.doc(security='apikey')
@token_required
@name_space.expect(parserRange)
@cache.cached(key_prefix=cache_key)
def get(self):
"""Get all records in the database that match between the given dates.
Accepts '%' sign and '_' sign.
"""
try:
args = parserRange.parse_args(strict=True)
date = args["date"]
end_date = args["end_date"]
cursor = conn.cursor()
cursor.execute("SELECT * FROM {0}.{1} where date between '{2}' and '{3}';".format(db_name, db_tab, date, end_date))
rows = cursor.fetchall()
cursor.close()
return format_result(status="OK", msg="", rows=rows)
except KeyError as err:
name_space.abort(500, err.__doc__, status = "Unable to obtain the data", statusCode = "500")
except Exception as err:
logging.error(err)
name_space.abort(400, err.__doc__, status = "Unable to obtain the data", statusCode = "400")
def format_result(status, msg="", rows=""):
"""Provide a specific format for responding to the request. Returns the formatted response."""
try:
result = {
"status": status,
"statusCode": 200,
"message": msg,
"values": { "numrecs": len(rows), "records": []}
}
if len(rows) > 0:
for row in rows:
record = "{"
for idx, col in enumerate(columns):
record += '"{0}": "{1}",'.format(col, row[idx])
record = record[:-1]
record += "}"
result['values']['records'].append(loads(record))
else:
if type(rows) is list:
result["message"] = "No data found"
return jsonify(result)
except Exception as err:
logging.error(err)
name_space.abort(400, err.__doc__, status = "Unable to obtain the data", statusCode = "400")
def get_columns():
"""Obtain the columns of the reference table"""
cursor = conn.cursor()
cursor.execute("select column_name from information_schema.columns"
" where table_schema = '{0}'"
" and table_name = '{1}'"
" order by table_name, ordinal_position".format(db_name, db_tab))
cols = cursor.fetchall()
for col in cols:
columns.append(col[0])
cursor.close()
def check_database(db_host = "localhost", db_user = "user", db_pass = "pass"):
"""Check the database connection. Returns the connection."""
conn = mysql.connector.connect(host=db_host, user=db_user, passwd=db_pass, database=db_name)
logging.info("Database connection OK")
return conn
def check_user(auth):
"""Check if authorised. Returns true or false"""
cursor = conn.cursor()
cursor.execute("SELECT CASE"
" when SHA2('{0}', 256) = password then true"
" else false end as result"
" from elecprod.users where user = '{1}';".format(auth.password, auth.username))
ret = cursor.fetchone()
cursor.close()
if ret is None:
return False
elif ret[0] == 1:
return True
else:
return False
if __name__ == "__main__":
try:
# Logging configuration
logging.basicConfig(level=logging.INFO,
format='%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
# Check if database connection is available
conn = check_database(db_host, db_user, db_pass)
# Get columns of the reference table to use in the response
get_columns()
#
app.run(debug=True, port=5000)
except KeyboardInterrupt:
logging.info("Program stopped")
except Exception as e:
logging.error("ERROR - {0}".format(e))
finally:
if type(conn) is not str and conn.is_connected():
conn.close()