python-by-example/projects/03_library_management/solution.py

390 lines
13 KiB
Python

"""
================================================================================
Project 03: Library Management System
Difficulty: 🟠 Advanced
================================================================================
An object-oriented Library Management System for managing books, members,
and borrow/return operations with proper validation and error handling.
Concepts Used:
- Classes and Objects
- Inheritance and Encapsulation
- Properties and Methods
- Custom Exceptions
- __str__ and __repr__ magic methods
- Composition (Library has Books and Members)
================================================================================
"""
from datetime import datetime, timedelta
# -----------------------------------------------------------------------------
# Custom Exceptions
# -----------------------------------------------------------------------------
class LibraryError(Exception):
"""Base exception for library errors."""
pass
class BookNotFoundError(LibraryError):
"""Raised when a book is not found."""
pass
class BookNotAvailableError(LibraryError):
"""Raised when a book is already borrowed."""
pass
class MemberNotFoundError(LibraryError):
"""Raised when a member is not found."""
pass
class BorrowLimitError(LibraryError):
"""Raised when a member has reached their borrow limit."""
pass
# -----------------------------------------------------------------------------
# Book Class
# -----------------------------------------------------------------------------
class Book:
"""Represents a book in the library."""
def __init__(self, title: str, author: str, isbn: str):
self._title = title
self._author = author
self._isbn = isbn
self._is_available = True
self._borrowed_by = None
self._due_date = None
@property
def title(self) -> str:
return self._title
@property
def author(self) -> str:
return self._author
@property
def isbn(self) -> str:
return self._isbn
@property
def is_available(self) -> bool:
return self._is_available
@property
def due_date(self):
return self._due_date
def borrow(self, member_id: str, days: int = 14) -> None:
"""Mark the book as borrowed."""
if not self._is_available:
raise BookNotAvailableError(
f'"{self._title}" is already borrowed.'
)
self._is_available = False
self._borrowed_by = member_id
self._due_date = datetime.now() + timedelta(days=days)
def return_book(self) -> None:
"""Mark the book as returned."""
self._is_available = True
self._borrowed_by = None
self._due_date = None
def __str__(self) -> str:
status = "✅ Available" if self._is_available else f"❌ Borrowed (due: {self._due_date.strftime('%Y-%m-%d')})"
return f'"{self._title}" by {self._author} [ISBN: {self._isbn}] — {status}'
def __repr__(self) -> str:
return f"Book(title='{self._title}', author='{self._author}', isbn='{self._isbn}')"
# -----------------------------------------------------------------------------
# Member Class
# -----------------------------------------------------------------------------
class Member:
"""Represents a library member."""
MAX_BORROW_LIMIT = 3
def __init__(self, name: str, member_id: str):
self._name = name
self._member_id = member_id
self._borrowed_books: list[str] = [] # List of ISBNs
@property
def name(self) -> str:
return self._name
@property
def member_id(self) -> str:
return self._member_id
@property
def borrowed_books(self) -> list:
return self._borrowed_books.copy()
@property
def borrow_count(self) -> int:
return len(self._borrowed_books)
def can_borrow(self) -> bool:
"""Check if the member can borrow more books."""
return self.borrow_count < self.MAX_BORROW_LIMIT
def add_book(self, isbn: str) -> None:
"""Add a book ISBN to the borrowed list."""
if not self.can_borrow():
raise BorrowLimitError(
f"{self._name} has reached the borrow limit ({self.MAX_BORROW_LIMIT} books)."
)
self._borrowed_books.append(isbn)
def remove_book(self, isbn: str) -> None:
"""Remove a book ISBN from the borrowed list."""
if isbn in self._borrowed_books:
self._borrowed_books.remove(isbn)
def __str__(self) -> str:
return (
f"👤 {self._name} (ID: {self._member_id}) "
f"— Books borrowed: {self.borrow_count}/{self.MAX_BORROW_LIMIT}"
)
def __repr__(self) -> str:
return f"Member(name='{self._name}', member_id='{self._member_id}')"
# -----------------------------------------------------------------------------
# Library Class (Composition)
# -----------------------------------------------------------------------------
class Library:
"""Manages books and members."""
def __init__(self, name: str):
self.name = name
self._books: dict[str, Book] = {} # ISBN -> Book
self._members: dict[str, Member] = {} # member_id -> Member
# --- Book Management ---
def add_book(self, title: str, author: str, isbn: str) -> Book:
"""Add a new book to the catalog."""
if isbn in self._books:
print(f" ⚠️ Book with ISBN {isbn} already exists.")
return self._books[isbn]
book = Book(title, author, isbn)
self._books[isbn] = book
return book
def find_book(self, isbn: str) -> Book:
"""Find a book by ISBN."""
if isbn not in self._books:
raise BookNotFoundError(f"No book found with ISBN: {isbn}")
return self._books[isbn]
def search_books(self, query: str) -> list[Book]:
"""Search books by title or author."""
query_lower = query.lower()
return [
book for book in self._books.values()
if query_lower in book.title.lower() or query_lower in book.author.lower()
]
# --- Member Management ---
def register_member(self, name: str, member_id: str) -> Member:
"""Register a new library member."""
if member_id in self._members:
print(f" ⚠️ Member ID {member_id} already exists.")
return self._members[member_id]
member = Member(name, member_id)
self._members[member_id] = member
return member
def find_member(self, member_id: str) -> Member:
"""Find a member by ID."""
if member_id not in self._members:
raise MemberNotFoundError(f"No member found with ID: {member_id}")
return self._members[member_id]
# --- Borrow / Return ---
def borrow_book(self, member_id: str, isbn: str) -> None:
"""Allow a member to borrow a book."""
member = self.find_member(member_id)
book = self.find_book(isbn)
if not member.can_borrow():
raise BorrowLimitError(
f"{member.name} has reached the borrow limit."
)
book.borrow(member_id)
member.add_book(isbn)
print(f'"{book.title}" has been borrowed by {member.name}.')
print(f" Due date: {book.due_date.strftime('%Y-%m-%d')}")
print(f" Books borrowed: {member.borrow_count}/{Member.MAX_BORROW_LIMIT}")
def return_book(self, member_id: str, isbn: str) -> None:
"""Allow a member to return a book."""
member = self.find_member(member_id)
book = self.find_book(isbn)
book.return_book()
member.remove_book(isbn)
print(f'"{book.title}" has been returned by {member.name}.')
# --- Display ---
def display_catalog(self) -> None:
"""Display the full library catalog."""
if not self._books:
print(" 📭 No books in the catalog.")
return
print(f"\n 📚 {self.name} — Catalog ({len(self._books)} books)")
print(f" {'='*65}")
for book in self._books.values():
print(f" {book}")
def display_members(self) -> None:
"""Display all members."""
if not self._members:
print(" 📭 No registered members.")
return
print(f"\n 👥 Registered Members ({len(self._members)})")
print(f" {'='*50}")
for member in self._members.values():
print(f" {member}")
# -----------------------------------------------------------------------------
# Interactive Menu
# -----------------------------------------------------------------------------
def display_menu():
"""Display the main menu."""
print("""
╔══════════════════════════════════════════╗
║ 📚 LIBRARY MANAGEMENT SYSTEM 📚 ║
╠══════════════════════════════════════════╣
║ 1. View Catalog ║
║ 2. Add Book ║
║ 3. Search Books ║
║ 4. Register Member ║
║ 5. Borrow Book ║
║ 6. Return Book ║
║ 7. View Member Info ║
║ 8. Exit ║
╚══════════════════════════════════════════╝
""")
def main():
"""Main entry point."""
library = Library("City Public Library")
# Pre-load some sample data
library.add_book("The C Programming Language", "Kernighan & Ritchie", "978-0-13-110362-7")
library.add_book("Clean Code", "Robert C. Martin", "978-0-13-235088-4")
library.add_book("Python Crash Course", "Eric Matthes", "978-1-59327-603-4")
library.add_book("Design Patterns", "Gang of Four", "978-0-20-163361-0")
library.add_book("The Pragmatic Programmer", "Hunt & Thomas", "978-0-20-161622-4")
library.register_member("Alice", "M001")
library.register_member("Bob", "M002")
while True:
display_menu()
choice = input(" Choose an option (1-8): ").strip()
try:
if choice == "1":
library.display_catalog()
elif choice == "2":
title = input(" Enter title: ").strip()
author = input(" Enter author: ").strip()
isbn = input(" Enter ISBN: ").strip()
if title and author and isbn:
library.add_book(title, author, isbn)
print(f'"{title}" added to the catalog.')
else:
print(" ❌ All fields are required.")
elif choice == "3":
query = input(" Search (title or author): ").strip()
results = library.search_books(query)
if results:
print(f"\n 🔍 Found {len(results)} result(s):")
for book in results:
print(f" {book}")
else:
print(" 📭 No matching books found.")
elif choice == "4":
name = input(" Enter member name: ").strip()
member_id = input(" Enter member ID: ").strip()
if name and member_id:
library.register_member(name, member_id)
print(f"{name} registered as member {member_id}.")
else:
print(" ❌ All fields are required.")
elif choice == "5":
library.display_members()
member_id = input("\n Enter Member ID: ").strip()
library.display_catalog()
isbn = input("\n Enter Book ISBN: ").strip()
library.borrow_book(member_id, isbn)
elif choice == "6":
member_id = input(" Enter Member ID: ").strip()
isbn = input(" Enter Book ISBN: ").strip()
library.return_book(member_id, isbn)
elif choice == "7":
library.display_members()
member_id = input("\n Enter Member ID for details: ").strip()
member = library.find_member(member_id)
print(f"\n {member}")
if member.borrowed_books:
print(" Borrowed books:")
for isbn in member.borrowed_books:
book = library.find_book(isbn)
print(f" 📖 {book}")
else:
print(" No books currently borrowed.")
elif choice == "8":
print("\n 👋 Thank you for using the Library Management System!")
break
else:
print(" ❌ Invalid option. Please choose 1-8.")
except LibraryError as e:
print(f"{e}")
input("\n Press Enter to continue...")
if __name__ == "__main__":
main()