-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
347 lines (284 loc) · 11.6 KB
/
app.py
File metadata and controls
347 lines (284 loc) · 11.6 KB
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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# Importing main flask modules for API calls
from flask import Flask, jsonify, request
# dateandtime library for timestamp, pytz to get EST timezone, fmt to specify time format for database insertion
from datetime import datetime
from pytz import timezone
EST = timezone('EST')
fmt = '%Y-%m-%d %H:%M:%S'
# Library to generate randomized token strings for API Keys
import secrets
# Wraps module to create authenticating decorator
from functools import wraps
# OS library to access environment variables
import os
# Importing Flask-Markdown library to render our README on the homepage
from markdown import markdown
# Initializing our main application instance
app = Flask(__name__)
# Setting SQLAlchemy variables
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# Importing our database models (tables) and schema
from models import Record, single_record_schema, multiple_records_schema, AuthenticationKey, db, ma
'''
The function "require_authentication" is a decorator function that requires users to add an authentication key to add, modify or delete record entries.
It will cross-check generated API keys in the "authentication_key" table first.
If there is a match = Allow the action.
If there is no match = Return an error message.
'''
def require_authentication(view_function):
@wraps(view_function)
def decorated_function(*args, **kwargs):
# Return the first API key match from the authentication_keys database
find_api_key = AuthenticationKey.query.filter_by(key = request.headers.get('api-key')).first()
# If there is an API key match, go ahead with the function
if find_api_key is not None:
return view_function(*args, **kwargs)
# If no match, show an error message
result = {
'status': "failed",
'message': "You are not authenticated! Please add an authentication key to the header and try again!"
}
return result
return decorated_function
'''
This function renders the homepage.
'''
@app.route('/')
def index():
readme = open("README.md", "r")
render_readme = markdown(readme.read())
return render_readme
'''
This functions handles errors if there is no username specified after /generate_key/.
'''
@app.route('/api/generate_key/', methods = ["POST"])
def show_generator_error():
# Failed Message
result = {
'status': "failed",
'message': "Oops! You forgot to add a username after! Please specify a username and try again!"
}
return result
'''
This function generates an API key for a new user. The user can proceed to enter their name in the URL. If the username exists, it will return an error message.
If the username is new, it will proceed by returning the api_key (to be put as a cURL header) alongside with other information. This information is appended to
the authentication_keys table.
'''
@app.route('/api/generate_key/<username>', methods = ["POST"])
def generate_key(username):
existing_username = AuthenticationKey.query.filter_by(username = username).first()
if existing_username is None:
key = secrets.token_urlsafe(16)
created_on = datetime.now(EST).strftime(fmt) + ' EST'
new_authentication_key = AuthenticationKey(username, key, created_on)
db.session.add(new_authentication_key)
db.session.commit()
result = {
'status': "success",
'username': username,
'api_key': key,
'created_on': created_on,
'message': "Congrats! Your API Key has been created!"
}
return result
result = {
'status': "failed",
'message': "The username you are trying to register has been already used!"
}
return result
'''
This function will return all the record entries in the record table/any entries added by the API.
'''
@app.route('/api/list', methods = ["GET"])
def show_all_records():
all_records = Record.query.all()
return jsonify(multiple_records_schema.dump(all_records))
'''
This function allows users to create a new record, provided they have added an API key to the header. The user can add a timestamp, value1, value2,
value3 (everything is optional) while the id, creationdate, and lastmodificationdate is automatically appended to the entry. It utilizes a try/except block
to get a value, if there is no value, it appends a NULL value to the database.
'''
@app.route('/api/create', methods = ["POST"])
@require_authentication
def add_new_record():
try:
timestamp = request.json['timestamp']
except KeyError:
timestamp = None
try:
value1 = request.json['value1']
except KeyError:
value1 = None
try:
value2 = request.json['value2']
except KeyError:
value2 = None
try:
value3 = request.json['value3']
except KeyError:
value3 = None
# Logic to check if atleast one field is populated. If everything is empty, it returns an error.
if (timestamp is None and value1 is None and value2 is None and value3 is None) == True:
result = {
'status': "failed",
'message': "Please populate atleast one field into the record and try again!"
}
return result
# Get current time in UNIX time, and append it to creationdate and lastmodificationdate
current_time = round(datetime.now().timestamp() * 1000)
# Adding current time information both creationdate and lastmodificationdate
creationdate = current_time
lastmodificationdate = current_time
# Querying the database to match the api-key to the username
lastmodifiedby = AuthenticationKey.query.filter_by(key = request.headers.get('api-key')).first().username
# Append record data to Record class structure
new_record = Record(timestamp, value1, value2, value3, creationdate, lastmodificationdate, lastmodifiedby)
# Add it and then commit it
db.session.add(new_record)
db.session.commit()
# Success Message
result = {
'status': "success",
'message': "Your entry has successfully been added to the database!",
'record_info': single_record_schema.dumps(new_record)
}
return result
'''
This function catches exceptions caused by not adding a number after the "read" API Endpoint.
'''
@app.route('/api/read/', methods = ["GET"])
def show_read_error():
# Failed Message
result = {
'status': "failed",
'message': "Oops! You forgot to add a number after! Please specify a number and try again!"
}
return result
'''
This function reads the record ID from the URL and then gets the entry from the database, provided the user has authenticated. If not, it throws an error.
If they have authenticated, it will proceed to show the information.
'''
@app.route('/api/read/<record_id>', methods = ["GET"])
def read_particular_record(record_id):
particular_record = Record.query.filter_by(id = record_id).first()
# If record does not exist, throw an error
if particular_record is None:
result = {
'status': "failed",
'message': "The record does not exist! Please try a different record number!"
}
return result
particular_record_data = single_record_schema.dumps(particular_record)
result = {
'status': "success",
'message': "Record found!",
'record_info': particular_record_data
}
return result
'''
This function catches exceptions caused by not adding a number after the "modify" API Endpoint.
'''
@app.route('/api/modify/', methods = ["PATCH"])
def show_modify_error():
# Failed Message
result = {
'status': "failed",
'message': "Oops! You forgot to add a number after! Please specify a number and try again!"
}
return result
'''
This function allows the users to modify a record, provided they have authenticated with an API key. It searches for a record, if it does not exist,
it throws an error message. Otherwise, it will utilize try/except blocks to further get modified information.
'''
@app.route('/api/modify/<record_id>', methods = ["PATCH"])
@require_authentication
def modify_a_record(record_id):
# Check if the URL number matches a record ID number
particular_record = Record.query.filter_by(id = record_id).first()
# If there is no record, throw an error (Failed Message)
if particular_record is None:
result = {
'status': "failed",
'message': "The record does not exist! Please try a different record number!"
}
return result
# If there is a record ID with that number, use try/except blocks to give the user options to modify some/all fields, if user makes no changes -
# then do nothing and keep everything the same.
try:
new_timestamp = request.json['timestamp']
particular_record.timestamp = new_timestamp
except KeyError:
pass
try:
new_value1 = request.json['value1']
particular_record.value1 = new_value1
except KeyError:
pass
try:
new_value2 = request.json['value2']
particular_record.value2 = new_value2
except KeyError:
pass
try:
new_value3 = request.json['value3']
particular_record.value3 = new_value3
except KeyError:
pass
# Get current rounded UNIX time in milliseconds
current_time = round(datetime.now().timestamp() * 1000)
newmodificationdate = current_time
# Appending new modification date to the record
particular_record.lastmodificationdate = newmodificationdate
# Getting the latest modifying person's username by matching their API key to their username from our authenticationkey database.
latest_username_to_modify = AuthenticationKey.query.filter_by(key = request.headers.get('api-key')).first().username
# Overriding the lastmodifiedby entry to the new username
particular_record.lastmodifiedby = latest_username_to_modify
# Committing the information to the database session.
db.session.commit()
# Success Message
result = {
'status': "success",
'message': "Your record has successfully been modified!",
'record_info': single_record_schema.dumps(particular_record)
}
return result
'''
This function catches exceptions caused by not adding a number after the "remove" API Endpoint.
'''
@app.route('/api/remove/', methods = ["DELETE"])
def show_delete_error():
# Failed Message
result = {
'status': "failed",
'message': "Oops! You forgot to add a number after! Please specify a number and try again!"
}
return result
'''
This function finds the record entry by extracting the number from the URL. If the record ID does not exist, it throws an error. Otherwise, it deletes the
record and commits the changes to the database session.
'''
@app.route('/api/remove/<record_id>', methods = ["DELETE"])
@require_authentication
def delete_a_record(record_id):
# Check if the URL number matches a record ID number
particular_record = Record.query.filter_by(id = record_id).first()
# If there is no record, throw an error (Failed Message)
if particular_record is None:
result = {
'status': "failed",
'message': "The record does not exist! Please try a different record number!"
}
return result
# Delete the record from the database session
db.session.delete(particular_record)
# Commit the changes
db.session.commit()
# Success Message
result = {
'status': "success",
'message': "Your record has been removed!"
}
return result
if __name__ == "__main__":
app.run(debug = False)