-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.py
398 lines (336 loc) · 12.4 KB
/
app.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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
import configparser
import mysql.connector
import sys
import tabulate
###########################
# Useful global variables #
###########################
"""
Column names for `tabulate` table headers
"""
TABLE_HEADERS = ["Title", "Artist", "Genre", "Year", "Comment",
"Composer", "Medium", "Type", "Complete"]
########################
# Function definitions #
########################
def album_exists(title, artist):
"""
Return whether an album exists in the database.
Parameters:
title: title of the album to search for
artist: artist of the album to search for
Return True if found, False otherwise
"""
album_check = conn.cursor(buffered=True)
check_query = "SELECT * FROM albums "\
f"WHERE title = '{title}' AND artist = '{artist}'"
album_check.execute(check_query)
if album_check.fetchone() is None:
return False
return True
def add_album(title, artist, genre, year, medium, type, complete,
comment=None, composer=None):
"""
Add an album to the database.
The `comment` and `composer` fields default to None because they are not
required in the database.
Return True if the data is inserted successfully. Else, return False.
"""
# make sure album doesn't already exist in database
if album_exists(title, artist):
print(f"Album '{title} by {artist}' already exists in database!",
file=sys.stderr)
return False
cursor = conn.cursor()
query = "INSERT INTO albums (title, artist, genre, year, comment,"\
"composer, medium, type, complete) VALUES ("\
f"'{title}', '{artist}', '{genre}', '{str(year)}', "
if comment is not None:
query += f"'{comment}', "
else:
query += "null, "
if composer is not None:
query += f"'{composer}', "
else:
query += "null, "
if medium in ['cd', 'digital', 'vinyl']:
query += f"'{medium}', "
else:
return False
if type in ["studio album", "single", "ep"]:
query += f"'{type}', "
else:
return False
if complete in ['y', 'n']:
query += f"'{complete}')"
else:
return False
cursor.execute(query)
conn.commit()
return True
def get_albums():
"""
Get all albums for listing.
Return the list of album tuples to be displayed; on error return None and
print the error.
"""
cursor = conn.cursor()
try:
cursor.execute("SELECT * FROM albums", multi=True)
return cursor.fetchall()
except mysql.connector.Error as e:
print(str(e), file=sys.stderr)
return None
def delete_album(title, artist):
"""
Delete an album from the database.
This function only needs the title and artist of the album in question
because those two fields make up the primary key of the `albums` table
Return True on success; otherwise return False and print an error.
"""
# make sure album exists
if not album_exists(title, artist):
print(f"Album '{title} by {artist}' does not exist, so can't delete.",
file=sys.stderr)
return False
cursor = conn.cursor()
query = f"DELETE FROM albums WHERE title = '{title}' AND "\
f"artist = '{artist}'"
try:
cursor.execute(query)
conn.commmit()
except mysql.connector.Error as e:
print(str(e), file=sys.stderr)
return False
return True
def __validate_field(field):
"""
Return whether a field name is part of the SQL schema.
In updating an album, the user needs a way to specify the field they want to
update. Doing this introduces the possibility of errors if the user inputs
an invalid field name, hence the need to check the input.
Hinted to be private because users should not need to do this themselves
Parameters:
field: the field name to check
"""
cursor = conn.cursor()
# Get a tuple of all the column names
query = "SELECT column_name "\
"FROM information_schema.columns "\
f"WHERE table_schema = '{parser.get('mysql', 'database')}' "\
"AND table_name = 'albums'"
cursor.execute(query)
results = cursor.fetchall()
for result in results:
if field == result[0]:
return True
valid_fields = [result[0] for result in results]
print("Invalid field. Choose one of the following:", file=sys.stderr)
for field in valid_fields:
print(f"\t{field}", file=sys.stderr)
return False
def __validate_enum(data, field):
"""
Return whether data has the correct type for its SQL enum field.
Parameters:
data: the data to check
field: the enum to check the data against
Return True if the data is a valid value for the specified field, false
otherwise.
"""
cursor = conn.cursor()
query = "SELECT column_type "\
"FROM information_schema.columns "\
f"WHERE table_schema = '{parser.get('mysql', 'database')}' "\
"AND table_name = 'albums' "\
f"AND column_name = '{field}'"
cursor.execute(query)
################################################
# decoding and parsing of messy SQL data types #
################################################
# the list of tuples, as it comes back from SQL
valid_options = cursor.fetchall()
# drill down through list and tuple, giving us a bytes object
valid_options = valid_options[0][0]
# decode the bytes into a UTF-8 string
valid_options = valid_options.decode()
# skip "enum"
valid_options = valid_options[4:]
# get rid of the parens...
valid_options = valid_options.strip('()')
# and the single quotes...
valid_options = valid_options.replace("'", "")
# now we can make a list out of it
valid_options = valid_options.split(",")
# finally we can see if the data is valid or not
if data in valid_options:
return True
else:
print(f"Invalid choice for enum {field}. Choose one of the following",
file=sys.stderr)
for option in valid_options:
print(f"\t{option}", file=sys.stderr)
return False
def update_album(title, artist, field, data):
"""
Update a field of an album.
The title and artist fields are necessary because the database uses them
together as the primary key of the `albums` table.
Parameters:
title: the title of the album to be updated
artist: the artist of the album to be updated
field: the field of the record to be udpated
data: the new data for the field to be updated
Return True on success; on error, return False and print an error.
"""
# make sure we got a valid field
if not __validate_field(field):
return False
# We should be okay now, so do the update
cursor = conn.cursor()
query = f"UPDATE albums SET {field} = '{data}' "\
f"WHERE title = '{title}' AND artist = '{artist}'"
try:
cursor.execute(query)
except mysql.connector.Error as e:
print(str(e), file=sys.stderr)
return False
conn.commit()
return True
def handle_add_album():
print("=== REQUIRED DATA ===".center(40))
title = input("Enter album title (<= 100 characters): ")
artist = input("Enter artist (<= 100 characters): ")
genre = input("Enter genre (<= 50 characters): ")
year = input("Enter album year (1901 through 2155 valid): ")
medium = input("Enter medium (one of [cd, digital, vinyl]): ")
type = input("Enter album type (one of [studio album, single, ep]: ")
complete = input("Enter completeness status (y/n): ")
print("=== OPTIONAL DATA ===".center(40))
comment = input("Enter a comment (empty for none, <= 100 characters): ")
if len(comment) == 0:
comment = None
composer = input("Enter a composer (empty for none, <= 50 characters): ")
if len(composer) == 0:
composer = None
add_album(title, artist, genre, year, medium, type, complete,
comment, composer)
def handle_delete_album():
title = input("Title of album to delete: ")
artist = input("Artist of album to delete: ")
if album_exists(title, artist):
delete_album(title, artist)
else:
print(f"Album '{title} by {artist}' does not exist, so can't delete.",
file=sys.stderr)
def handle_update_album():
title = input("Enter the title of the album to be updated: ")
artist = input("Enter the artist of the album to be updated: ")
# make sure album exists
if not album_exists(title, artist):
print(f"Album '{title} by {artist}' does not exist, so can't update.",
file=sys.stderr)
return
__show_album(title, artist)
field = input("Choose which field to update (all-lowercase): ")
while not __validate_field(field):
field = input("Choose which field to update (all-lowercase): ")
new_data = input("Enter the new data for this field: ")
# if a year, make sure it fits MySQL's constraints on years
while field == "year" and (int(new_data) < 1901 or int(new_data) > 2155):
print("Year is not accepted by MySQL. Must be [1901, 2155].",
file=sys.stderr)
new_data = input("Enter the new data for this field: ")
# Check varchar length constraints
while len(new_data) > 100 and (field in ['title', 'artist', 'comment']):
print(f"Data too long for field {field}. Reduce to <100 characters. "
f"Data was {len(new_data)} characters.",
file=sys.stderr)
new_data = input("Enter the new data for this field: ")
while len(new_data) > 50 and (field in ['genre', 'composer']):
print(f"Data too long for field {field}. Reduce to <50 characters. "
f"Data was {len(new_data)} characters.",
file=sys.stderr)
new_data = input("Enter the new data for this field: ")
# Check enum validity
if field in ['medium', 'type', 'complete']:
if not __validate_enum(new_data, field):
return
# if we get here, everything should be valid, so update the record
update_album(title, artist, field, new_data)
print("Here's the new record:".center(40))
__show_album(title, artist)
def __show_album(title, artist):
"""
Show the details for the specified album.
Parameters:
title: the title of the album to display
artist: the artist of the album to display
"""
cursor = conn.cursor()
cursor.execute("SELECT * FROM albums WHERE "\
f"title = '{title}' AND artist = '{artist}'")
print(tabulate.tabulate([list(cursor.fetchone())], headers=TABLE_HEADERS))
def handle_find_album():
title = input("Enter the title of the album to search for: ")
artist = input("Enter the artist of the album to search for: ")
if album_exists(title, artist):
__show_album(title, artist)
else:
print(f"Album does not exist in database.", file=sys.stderr)
def handle_list_albums():
all_albums = get_albums()
print(tabulate.tabulate(all_albums,
headers=["Title", "Artist", "Genre", "Year", "Comment",
"Composer", "Medium", "Type", "Complete"]))
############################
# Configuring the app data #
############################
parser = configparser.ConfigParser()
parser.read("config.ini")
###############################
# Setting up MySQL connection #
###############################
conn = None
try:
conn = mysql.connector.connect(
user=parser.get("mysql", "user"),
password=parser.get("mysql", "password"),
host=parser.get("mysql", "host"),
database=parser.get("mysql", "database")
)
except mysql.connector.Error as e:
print(str(e), file=sys.stderr)
exit(-1)
###################
# Create CLI loop #
###################
print("Welcome")
option = ""
while option != "q":
print("Choose an option:")
print("1. Add album")
print("2. Delete album")
print("3. Update album info")
print("4. List albums")
print("5. Find album")
print("q. Quit")
option = input("> ")
if option == '1':
handle_add_album()
elif option == '2':
handle_delete_album()
elif option == '3':
handle_update_album()
elif option == '4':
handle_list_albums()
elif option == '5':
handle_find_album()
elif option == 'q':
print("Goodbye!\n")
else:
pass
####################################
# Close the connection to clean up #
####################################
conn.close()