Your library system is steadily evolving. Users can now log in, borrow, and return books with ease. But one problem remains: finding the right book isn't always so easy. The current search function only works if users know the exact title. Imagine being a stressed student during exam week, remembering only "something about machine learning" but not the full book title -- frustrating, right?
The librarians want to fix this by introducing a keyword-based search, so users can explore the catalogue more naturally. On top of that, the university has just secured extra funding to expand the collection, and your system must now support adding brand-new books into the database.
Task Description
Updated Main Menu
For students, staff (non-library) and other users, a new option, Search by Keywords (Option 5), is added to the main menu.
Logged in as Chris Manner (Student)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice:
For library staff, alongside Search by Keywords (option 5), the existing Library Report option is expanded into Manage Library (option 6), offering more administrative features (described later).
Logged in as Mary Alan (Staff)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
6. Manage Library
==================================
Enter your choice:
Renew loan
All users may renew each borrowed book once, using the command renew <book ID> in the Borrow and Return console.
Renewal extends the due date by 5 days.
If the user has borrowed multiple copies of the same book, the system will renew the copy (or loan) with the earliest due date first.
A book is not eligible for renewal if:
Its extended due date has already passed (making it overdue). An error message "Renewal denied: This book is already overdue." will be printed out instead.
If it is already renewed once by the user, the system will display "Renewal unavailable: Each book can only be renewed once."
Users with unpaid fines cannot renew any loans until their fines are settled. The system will display "Renewal denied: You have unpaid fines."
Returning validation priority is similar to borrowing validation priority in Task 2.
The system first checks whether the user is eligible to renew.
Then it verifies if the loan is valid and is eligible to renew.
Logged in as Chris Manner (Student)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 4
> renew P0011
Renewal denied: You have unpaid fines.
An error will return if the loan record does not exist
Logged in as Noah (Others)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 4
> renew P0011
No loan record for P0011.
Search by Keywords
Upon entering 5, the system will prompt them to enter a list of keywords (case insensitive), separated by commas (,). The program will then search the catalogue and return a list of books that contain at least one of the specified keywords.
The results will be sorted in the following order of priority:
Number of matched keywords (highest first)
Publication year (newest first)
Book ID (ascending order)
If the keyword list is empty, the program should return "Found 0 book(s)."
Logged in as Noah (Others)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 5
Enter search keywords (separated by comma): python,programming
Found 7 book(s).
1. P0003 'Python Crash Course' by Eric Matthes (2023).
2. P0001 'Introduction to Python Programming' by S Gowrishankar (2019).
3. E0002 'Deep learning with Python: a hands-on introduction' by Ketkar Nikhil (2017).
4. E0001 'Python Crash Course' by Eric Matthes (2015).
5. P0002 'Python Programming: An Introduction to Computer Science' by John M. Zelle (2002).
6. E0003 'Machine Learning for Business' by Doug Hudgeon & Richard Nichol (2020).
7. P0006 'Hands-On ML' by Aurelien Geron (2019).
Manage Library (Library Staff only)
When staff members select option 6 to enter the Manage Library console, they can perform the following actions:
Print library report by enter report
Add a New Book
Use the command add physical to add a new physical book, or add online to add a new online book.
The system will then prompt the user to provide the book’s details, including title, authors, year, number of copies (required for physical books only)
The system will automatically assign keywords to newly added books by matching any words in their titles with the existing keyword list from the books.csv dataset (sorted by alphabet order).
Each new book is assigned a unique ID:
IDs for physical books begin with P.
IDs for online books begin with E.
The numeric portion of the ID is generated by incrementing the highest existing ID of that type in the library.
Type quit to exit the borrow and return console and go back to main menu.
Logged in as Mary Alan (Staff)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
6. Manage Library
==================================
Enter your choice: 6
> report
Library Report
- 9 users, including 4 student(s), 3 staff and 2 others.
- 14 books, including 10 physical book(s) (7 currently available) and 3 online book(s).
> add physical
Title: A Concise and Practical Introduction to Programming Algorithms in Java
Authors: Nielsen Frank
Year: 2017
Copies: 1
Detected keywords: algorithms:programming
Adding P0020 'A Concise and Practical Introduction to Programming Algorithms in Java' by Nielsen Frank (2017).
Examples
Example 1
Welcome to Library
Login as: s31267
Password: chr1267
Logged in as Chris Manner (Student)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 2
Student Chris Manner. Policies: maximum of 10 days, 4 items. Current loans: 2 (1 physical / 1 online). Fines: $ 1.00
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 3
You are currently have 2 loan(s).
1. P0006 'Hands-On ML' by Aurelien Geron (2019). Due date: 13/09/2025.
2. E0001 'Python Crash Course' by Eric Matthes (2015). Due date: 15/09/2025.
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 4
> renew P0101
Renewal denied: You have unpaid fines.
> return P0006
Returned 'Hands-On ML' by Aurelien Geron (2019). Overdue by 2 day(s). Fine: $ 1.00
> renew E0001
Renewal denied: This book is already overdue.
> return E0001
Returned 'Python Crash Course' by Eric Matthes (2015).
> borrow The Hobbit
Found 1 book(s).
- P0008 (physical) 'The Hobbit' by J.R.R. Tolkien (1937). Available copies: 1/2.
Confirm the Book ID you'd like to borrow: P0008
You have borrowed 'The Hobbit' by J.R.R. Tolkien (1937). Due: 25/09/2025.
> renew P0008
Renew 'The Hobbit' by J.R.R. Tolkien (1937) successfully. New due date: 30/09/2025
> quit
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 3
You are currently have 1 loan(s).
1. P0008 'The Hobbit' by J.R.R. Tolkien (1937). Due date: 30/09/2025.
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 0
Goodbye!
Example 2
Welcome to Library
Login as: o56799
Password: noa6799
Logged in as Noah (Others)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 5
Enter search keywords (separated by comma): python,programming
Found 7 book(s).
1. P0003 'Python Crash Course' by Eric Matthes (2023).
2. P0001 'Introduction to Python Programming' by S Gowrishankar (2019).
3. E0002 'Deep learning with Python: a hands-on introduction' by Ketkar Nikhil (2017).
4. E0001 'Python Crash Course' by Eric Matthes (2015).
5. P0002 'Python Programming: An Introduction to Computer Science' by John M. Zelle (2002).
6. E0003 'Machine Learning for Business' by Doug Hudgeon & Richard Nichol (2020).
7. P0006 'Hands-On ML' by Aurelien Geron (2019).
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 4
> borrow E0003
Found 1 book(s).
- E0003 (online) 'Machine Learning for Business' by Doug Hudgeon & Richard Nichol (2020). Available copies: 0/0.
Confirm the Book ID you'd like to borrow: E0003
You have borrowed 'Machine Learning for Business' by Doug Hudgeon & Richard Nichol (2020). Due: 22/09/2025.
> quit
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 3
You are currently have 1 loan(s).
1. E0003 'Machine Learning for Business' by Doug Hudgeon & Richard Nichol (2020). Due date: 22/09/2025.
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
==================================
Enter your choice: 0
Goodbye!
Example 3
Welcome to Library
Login as: e118102
Password: pa55word
Logged in as Mary Alan (Staff)
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
6. Manage Library
==================================
Enter your choice: 6
> report
Library report
- 9 users, including 4 student(s), 3 staff, and 2 others.
- 14 books, including 10 physical book(s) (7 currently available) and 4 online book(s).
> add physical
Title: A Concise and Practical Introduction to Programming Algorithms in Java
Authors: Nielsen Frank
Year: 2017
Copies: 1
Detected keywords: algorithms:programming
Adding P0020 'A Concise and Practical Introduction to Programming Algorithms in Java' by Nielsen Frank (2017).
> report
Library report
- 9 users, including 4 student(s), 3 staff, and 2 others.
- 15 books, including 11 physical book(s) (8 currently available) and 4 online book(s).
> quit
==================================
My Library Account
0. Quit
1. Log out
2. View account policies
3. View my loans
4. Borrow and Return
5. Search by Keywords
6. Manage Library
==================================
Enter your choice: 0
Goodbye!import user
import book
import csv
import datetime
from datetime import timedelta
import typing
def parse_date(date_str: str) -> typing.Optional[datetime.date]:
if not date_str or date_str.strip() == "None":
return None
day, month, year = map(int, date_str.split('/'))
return datetime.date(year, month, day)
def load_users(file_path: str) -> dict:
users = {}
with open(file_path, newline='') as csvfile:
reader = csv.reader(csvfile)
next(reader)
for row in reader:
user_id, password, name, role, department = row
role = row[3].capitalize()
if user_id.startswith('s'):
u = user.Student(user_id, password, name, role, None)
elif user_id.startswith('e'):
u = user.Staff(user_id, password, name, role, department)
else:
role = "Others"
u = user.Other(user_id, password, name, role, None)
users[user_id] = u
return users
def load_books(file_path: str) -> dict:
books = {}
with open(file_path, newline='') as csvfile:
reader = csv.reader(csvfile)
next(reader)
for row in reader:
book_id, book_type, copies, title, author, year, keywords = row
b = book.Book(book_id, book_type, int(copies), title, author, int(year), keywords)
books[book_id] = b
b.loans = []
return books
def load_loans(file_path: str, users: dict, books: dict):
loans = []
with open(file_path, newline='') as csvfile:
reader = csv.reader(csvfile)
next(reader)
for row in reader:
user_id, book_id, borrow_date, due_date, return_date = row
if user_id in users and book_id in books:
loan = Loan(users[user_id], books[book_id], borrow_date, due_date, return_date)
users[user_id].add_loan(loan)
books[book_id].loans.append(loan)
loans.append(loan)
return loans
def borrow_return_console(user, books, users):
TODAY = datetime.datetime.strptime("15/09/2025", "%d/%m/%Y").date()
while True:
cmd = input("> ").strip()
if cmd == "quit":
return
part = cmd.split(' ', 1)
if len(part) < 2:
continue
action, query = part[0].lower(), part[1]
if action == "borrow":
matched_books = []
for b in books.values():
if query.lower() in b.title.lower() or query == b.book_id:
matched_books.append(b)
matched_books.sort(key=lambda x: x.book_id)
if not matched_books:
print(f"No books match '{query}'.")
continue
physical_match = [b for b in matched_books if b.book_type == "physical"]
online_match = [b for b in matched_books if b.book_type == "online"]
available_str = sum(b.available_copies() for b in physical_match)
print(f"Found {len(matched_books)} book(s).")
all_matches = physical_match + online_match
all_matches.sort(key=lambda x: x.book_id)
for b in all_matches:
copies = f"Available copies: {available_str}/{b.total_copies}" if b.book_type == "physical" else "Available copies: 0/0"
print(f"- {b.book_id} ({b.book_type}) '{b.title}' by {b.authors} ({b.year}). {copies}.")
valid_ids = [b.book_id for b in all_matches]
while True:
borrowid = input(f"Confirm the Book ID you'd like to borrow: ").strip()
if borrowid == "quit":
break
if borrowid not in valid_ids:
continue
book = books[borrowid]
unpaid_fine = sum(loan.calculate_fine() for loan in user.get_active_loans())
if unpaid_fine > 0:
print("Borrowing unavailable: unpaid fines. Review your loan details for more info.")
break
current_physical = sum(1 for loan in user.get_active_loans() if loan.book.book_type == "physical")
if current_physical >= user.get_loan_policy()["quota"]:
print("Borrowing unavailable: quota reached. Review your loan details for more info.")
break
if book.book_type == "physical" and book.available_copies() <= 0:
print("No copies available.")
break
due_days = user.get_loan_policy()["days"]
due_date = TODAY + timedelta(days = due_days)
borrow_str = TODAY.strftime("%d/%m/%Y")
due_str = due_date.strftime("%d/%m/%Y")
return_str = "None"
loan = Loan(user, book, borrow_str, due_str, return_str)
user.loans.append(loan)
book.loans.append(loan)
print(f"You have borrowed '{book.title}' by {book.authors} ({book.year}). Due: {due_date.strftime('%d/%m/%Y')}.")
break
elif action == "return":
borrowid = query.strip()
if borrowid not in books:
print("No loan record for {}.".format(borrowid))
continue
book = books[borrowid]
active_loans = [loan for loan in user.get_active_loans() if loan.book.book_id == borrowid]
if not active_loans:
print(f"No loan record for {borrowid}.")
continue
loan_to_return = min(active_loans, key=lambda x: x.due_date)
user.loans.remove(loan_to_return)
return_date = TODAY
fine_amount = 0.0
overdue_days = 0
if book.book_type == "physical" and return_date > loan_to_return.due_date:
grace = {"Student": 0, "Staff": 2, "Others": 0}[user.role]
effective_due = loan_to_return.due_date + timedelta(days = grace)
overdue_days = max(0, (TODAY - effective_due).days)
rate = 0.50 if user.role in ['Student', 'Staff'] else 1.00
fine_amount = overdue_days * rate
status = f"Returned '{book.title}' by {book.authors} ({book.year})."
if overdue_days > 0:
status += f" Overdue by {overdue_days} day(s). Fine: $ {fine_amount:.2f}"
print(status)
else:
continue
class Loan:
def __init__(self, user, book, borrow_str, due_str, return_str):
self.user = user
self.book = book
self.borrow_date = parse_date(borrow_str)
self.due_date = parse_date(due_str)
self.return_date = parse_date(return_str)
def calculate_fine(self):
TODAY = datetime.datetime.strptime("15/09/2025", "%d/%m/%Y").date()
if self.book.book_type == "online":
return 0.0
if TODAY <= self.due_date:
return 0.0
grace = {"Student": 0, "Staff": 2, "Others": 0}[self.user.role]
effective_due = self.due_date + timedelta(days = grace)
overdue_days = max(0, (TODAY - effective_due).days)
rate = 0.50 if self.user.role in ['Student', 'Staff'] else 1.00
return overdue_days * rate
def main(user_file: str, book_file:str, loan_file:str) -> None:
"""
This is the entry of your program. Please DO NOT modify this function signature, i.e. function name, parameters
Parameteres:
- user_file (str): path the `users.csv` which stores user information
- book_file (str): path the `books.csv` which stores book information
- loan_file (str): path the `loans.csv` which stores loan information
"""
# Your implemetation goes here
users = load_users(user_file)
books = load_books(book_file)
load_loans(loan_file, users, books)
while True:
print("Welcome to Library")
user_input = input("Login as: ").strip()
if user_input == "quit":
print("Goodbye!")
return
else:
password = input("Password: ").strip()
if user_input not in users or users[user_input].password != password:
print("Invalid credentials. 2 attempt(s) remaining.")
user_input = input("Login as: ").strip()
password = input("Password: ").strip()
if user_input not in users or users[user_input].password != password:
print("Invalid credentials. 1 attempt(s) remaining.")
user_input = input("Login as: ").strip()
password = input("Password: ").strip()
if user_input not in users or users[user_input].password != password:
print("Sorry you're out of attempts. Please contact your librarian for assistance.")
continue
current_user = users[user_input]
print(f"Logged in as {current_user.name} ({current_user.role})")
while True:
menu = [
"My Library Account",
"0. Quit",
"1. Log out",
"2. View account policies",
"3. View my loans",
"4. Borrow and Return"
]
if current_user.is_library_staff():
menu.append("5. Library Report")
print("=" * 34)
for item in menu:
print(item)
print("=" * 34)
while True:
try:
choice = input("Enter your choice: ").strip()
if not choice.isdigit() or choice not in ['0', '1', '2', '3', '4', '5']:
continue
choice = int(choice)
break
except:
continue
if choice == 0:
print("Goodbye!")
return
elif choice == 1:
break
elif choice == 2:
policy = current_user.get_loan_policy()
active_loans = current_user.get_active_loans()
num_loans = len(active_loans)
phys, online = current_user.get_physical_online_count()
total_fine = sum(loan.calculate_fine() for loan in active_loans)
print(f"{current_user.role} {current_user.name}. Policies: maximum of {policy['days']} days, {policy['quota']} items. Current loans: {num_loans} ({phys} physical / {online} online). Fines: $ {total_fine:.2f}")
elif choice == 3:
active_loans = sorted(current_user.get_active_loans(), key=lambda x: x.due_date)
count = len(active_loans)
print(f"You are currently have {count} loan(s).")
for i, loan in enumerate(active_loans, 1):
print(f"{i}. {loan.book.book_id} '{loan.book.title}' by {loan.book.authors} ({loan.book.year}). Due date: {loan.due_date.strftime('%d/%m/%Y')}.")
elif choice == 4:
borrow_return_console(current_user, books, users)
elif choice == 5:
total_user = len(users)
students = sum(1 for u in users.values() if u.user_id.startswith('s'))
staffs = sum(1 for u in users.values() if u.user_id.startswith('e'))
others = sum(1 for u in users.values() if u.user_id.startswith('o'))
total_books = len(books)
physical_books = sum(1 for b in books.values() if b.book_type == "physical")
available_physical = sum(1 for b in books.values() if b.book_type == "physical" and b.is_available())
online_books = sum(1 for b in books.values() if b.book_type == "online")
print("Library report")
print(f"- {total_user} users, including {students} student(s), {staffs} staff, and {others} others.")
print(f"- {total_books} books, including {physical_books} physical book(s) ({available_physical} currently available) and {online_books} online book(s).")
else:
continue
if __name__ == "__main__":
main('data/users.csv', 'data/books.csv', 'data/loans.csv')
import csv
import datetime
import re
class Book:
# your code goes here
def __init__(self, book_id: str, book_type: str, total_copies: int, title: str, authors: str, year: int, keywords: str):
self.book_id = book_id
self.book_type = book_type
self.total_copies = total_copies
self.title = title
self.authors = authors
self.year = year
self.keywords = keywords.split(':') if keywords else []
self.loans = []
def available_copies(self):
borrowed_count = sum(1 for loan in self.loans if loan.return_date is None)
return self.total_copies - borrowed_count
def is_available(self) -> bool:
if self.book_type == "online":
return True
borrowed_count = sum(1 for loan in self.loans if loan.return_date is None)
return borrowed_count < self.total_copies
def __str__(self):
return f"{self.title} by {self.authors} ({self.year})"from abc import ABC, abstractmethod
import csv
import datetime
import re
TODAY = "15/09/2025"
class User(ABC):
# Your code goes here
def __init__(self, user_id: str, password: str, name: str, role: str, department: str = None):
self.user_id = user_id
self.password = password
self.name = name
self.role = role
self.department = department
self.loans = []
@abstractmethod
def get_loan_policy(self):
pass
def is_library_staff(self) -> bool:
return self.role == "Staff" and self.department == "Library"
def add_loan(self, loan):
self.loans.append(loan)
def get_active_loans(self):
return [loan for loan in self.loans if loan.return_date is None]
def get_physical_online_count(self):
physical = sum(1 for loan in self.get_active_loans() if loan.book.book_type == "physical")
online = sum(1 for loan in self.get_active_loans() if loan.book.book_type == "online")
return physical, online
def __str__(self):
return f"{self.name} ({self.role})"
class Student(User):
def get_loan_policy(self):
return {"days": 10, "quota": 4}
class Staff(User):
def get_loan_policy(self):
return {"days": 14, "quota": 6}
class Other(User):
def get_loan_policy(self):
return {"days": 7, "quota": 2}