mirror of
https://github.com/blshaer/python-by-example.git
synced 2026-03-27 23:29:25 +01:00
390 lines
13 KiB
Python
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()
|