From 0d67373f993329545291321c2da7f75dddf3ecc0 Mon Sep 17 00:00:00 2001 From: skarakash Date: Wed, 9 Jul 2025 09:05:03 +0300 Subject: [PATCH] added Library class that adds and removes items --- data/items.json | 30 +++++++ library.py | 233 ++++++++++++++++++++++++++++++++++++++++++++++++ main.py | 64 ++++++++++--- models.py | 54 +++++++++++ 4 files changed, 371 insertions(+), 10 deletions(-) create mode 100644 data/items.json create mode 100644 library.py create mode 100644 models.py diff --git a/data/items.json b/data/items.json new file mode 100644 index 0000000..120c548 --- /dev/null +++ b/data/items.json @@ -0,0 +1,30 @@ +{ + "items": [ + { + "type": "book", + "title": "1984", + "author": "George Orwell", + "year": 1949 + }, + { + "type": "book", + "title": "Animal farm", + "author": "George Orwell", + "year": 1945 + }, + { + "type": "book", + "title": "Brave New World", + "author": "Aldous Huxley", + "year": 1932 + }, + { + "type": "magazine", + "title": "Sports Illustrated", + "author": "Alex Author", + "year": 1932, + "issue": 2, + "month": "January" + } + ] +} \ No newline at end of file diff --git a/library.py b/library.py new file mode 100644 index 0000000..fa43860 --- /dev/null +++ b/library.py @@ -0,0 +1,233 @@ +from typing import List, Generator +from functools import wraps +import logging +import json +import os +from contextlib import contextmanager + +from models import BookModel, MagazineModel, LibraryItemModel + + +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + +def log_item_addition(func): + """ + Decorator to log book addition + """ + + @wraps(func) + def wrapper(self, item: BookModel, *args, **kwargs): + + logging.info(f"Adding {'book' if item.type == 'book' else 'magazine'} to library: '{item.title}' by {item.author}") + + + result = func(self, item, *args, **kwargs) + + + logging.info(f"{'book' if item.type == 'book' else 'magazine'} successfully added. Total items in library: {len(self._items)}") + + return result + + return wrapper + + +def check_book_exists(func): + """ + Decorator to check if a book exists + """ + + @wraps(func) + def wrapper(self, item: BookModel, *args, **kwargs): + + if item not in self._items: + logging.warning(f"Book '{item.title}' by {item.author} not found in library") + raise ValueError(f"Book '{item.title}' by {item.author} is not in the library") + + + logging.info(f"Removing book from library: '{item.title}' by {item.author}") + + + result = func(self, item, *args, **kwargs) + + + logging.info(f"Book successfully removed. Total books in library: {len(self._items)}") + + return result + + return wrapper + +class Library: + """ + Class representing a library + """ + def __init__(self) -> None: + """ + Initializes the library with an empty list of books + """ + self._items: List[LibraryItemModel] = [] + self._current_book_index = 0 + + @property + def books(self) -> str: + """ + Returns copy of the list of books + """ + return "/n".join([str(item) for item in self._items if isinstance(item, BookModel)]) + + + def magazines(self) -> str: + """ + Returns copy of the list of magazines + """ + return "/n".join([str(item) for item in self._items if isinstance(item, MagazineModel)]) + + def all_library_items(self) -> List[LibraryItemModel]: + return list(self._items) + + def __str__(self): + """ + Returns a string representation of the library + """ + if not self._items: + return "No libray items available" + + return f"Library of {len(self._items)} books" + + def __iter__(self): + self._current_item_index = 0 + return self + + def get_books_by_author(self, author_name: str) -> Generator[BookModel, None, None]: + """ + Generator that yields books by a specific author + + Args: + author_name (str): Name of the author to search for + + Yields: + BookModel: Books written by the specified author + """ + for book in self._items: + if isinstance(book, BookModel) and book.author.lower() == author_name.lower(): + yield book + + def __next__(self): + if self._current_item_index < len(self._items): + book = self._items[self._current_item_index] + self._current_item_index += 1 + return book + else: + self._current_item_index = 0 + raise StopIteration + + @log_item_addition + def add_item(self, item: LibraryItemModel) -> None: + """ + Adds an item to the library + """ + self._items.append(item) + + @check_book_exists + def remove_item(self, item: LibraryItemModel) -> None: + """ + Removes a book from the library + + Args: + book (BookModel): Book to remove from the library + + Raises: + ValueError: If the book is not found in the library + :param item: + """ + self._items.remove(item) + + def to_dict(self) -> dict: + """ + Converts the library to a dictionary for serialization + + Returns: + dict: Dictionary representation of the library + """ + return { + 'items': [ + { + 'type': 'magazine' if isinstance(item, MagazineModel) else 'book', + 'title': item.title, + 'author': item.author, + 'year': item.year, + **({'issue': item.issue, 'month': item.month} if isinstance(item, MagazineModel) else {}) + + } + for item in self._items + ] + } + + @classmethod + def from_dict(cls, data: dict) -> 'Library': + """ + Creates a library instance from a dictionary + + Args: + data (dict): Dictionary containing library data + + Returns: + Library: New library instance with loaded books + """ + library = cls() + for item_data in data.get('items', []): + if item_data.get('type') == 'magazine': + item = MagazineModel( + type=item_data.get('type'), + title=item_data['title'], + author=item_data['author'], + year=item_data['year'], + issue=item_data['issue'], + month=item_data['month'] + ) + else: + item = BookModel( + type=item_data['type'], + title=item_data['title'], + author=item_data['author'], + year=item_data['year'] + ) + library._items.append(item) + return library + + @contextmanager + def file_manager(self, filename: str, mode: str = 'r'): + """ + Context manager for file operations with the library + """ + try: + if mode == 'r' and os.path.exists(filename): + logging.info(f"Loading library from file: {filename}") + try: + with open(filename, 'r', encoding='utf-8') as file: + data = json.load(file) + loaded_library = Library.from_dict(data) + self._items = loaded_library._items + logging.info(f"Successfully loaded {len(self._items)} books from {filename}") + except (json.JSONDecodeError, KeyError) as e: + logging.error(f"Error loading library from {filename}: {e}") + raise + elif mode == 'r' and not os.path.exists(filename): + logging.warning(f"File {filename} does not exist. Starting with empty library.") + + yield self + + except Exception as e: + logging.error(f"Error in file operation: {e}") + raise + finally: + if mode == 'w': + logging.info(f"Saving library to file: {filename}") + try: + os.makedirs(os.path.dirname(filename), exist_ok=True) if os.path.dirname(filename) else None + with open(filename, 'w', encoding='utf-8') as file: + json.dump(self.to_dict(), file, ensure_ascii=False, indent=2) + logging.info(f"Successfully saved {len(self._items)} books to {filename}") + except Exception as e: + logging.error(f"Error saving library to {filename}: {e}") + raise diff --git a/main.py b/main.py index 94e3a87..980fe67 100644 --- a/main.py +++ b/main.py @@ -1,16 +1,60 @@ -# This is a sample Python script. +import logging -# Press ⌃R to execute it or replace it with your code. -# Press Double ⇧ to search everywhere for classes, files, tool windows, actions, and settings. +from library import Library +from models import BookModel, MagazineModel +if __name__ == '__main__': + try: -def print_hi(name): - # Use a breakpoint in the code line below to debug your script. - print(f'Hi, {name}') # Press ⌘F8 to toggle the breakpoint. + library = Library() + library.add_item(BookModel(title="1984", author="George Orwell", year=1949, type='book')) + library.add_item(BookModel(title="Animal farm", author="George Orwell", year=1945, type='book')) + library.add_item(BookModel(title="Brave New World", author="Aldous Huxley", year=1932, type='book')) + library.add_item(MagazineModel(title="Sports Illustrated", author="Alex Author", year=1932, issue=2, month="January", type='magazine')) + books = library.books + print(books) + + gen = library.get_books_by_author("George Orwell") + + for book in gen: + print(f'Using our generator: {book}') + + with library.file_manager('data/items.json', 'w') as lib: + print(f"Saving {len(lib.books)} items to file") + + book_to_remove = next( + (b for b in library.get_books_by_author("George Orwell") if b.title == "1984"), + None + ) + + if book_to_remove: + library.remove_item(book_to_remove) + print("\nBook removed successfully.") + else: + print("\nBook not found.") + + print("\nUpdated book list:") + print(library.books) + + with library.file_manager('data/items.json', 'r') as loaded_lib: + for item in loaded_lib.all_library_items(): + already_exists = any( + i.title == item.title and i.author == item.author and i.year == item.year + for i in library.all_library_items() + ) + if not already_exists: + print(f"\t{item.title}: {item.author} {item.typ} ({item.year})") + library.add_item(BookModel(title=item.title, author=item.author, year=item.year, type=item.type)) + print(f"Added new item: {item.title} by {item.author}") + else: + print(f"Skipped duplicate: {item.title} by {item.author}") + + + logging.info("Reading library...") + books = library.books + print(books) -# Press the green button in the gutter to run the script. -if __name__ == '__main__': - print_hi('PyCharm') -# See PyCharm help at https://www.jetbrains.com/help/pycharm/ + except Exception as error: + print(error) diff --git a/models.py b/models.py new file mode 100644 index 0000000..95562ae --- /dev/null +++ b/models.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod + +from pydantic import BaseModel, Field + +class LibraryItemModel(BaseModel, ABC): + """ + Pydantic model for the Book class + """ + + title: str = Field(..., min_length=1, description='The title of the book') + author: str = Field(..., min_length=1, description='The author of the book') + year: int = Field(..., description='The year of the book') + + @abstractmethod + def get_publication_info(self) -> str: + """ + Abstract method to return publication-specific information + Must be implemented in subclasses + """ + return f"'Was published in {self.year}" + + def __str__(self) -> str: + """ + Returns a string representation of the book + """ + return f"'{self.title}' by {self.author}, {self.year}" + + def __repr__(self) -> str: + """ + Returns a string representation of the book. Good for printing and + debugging purposes + """ + return f"BookModel(title='{self.title}', author='{self.author}', year={self.year}')" + +class MagazineModel(LibraryItemModel): + issue: int = Field(..., gt=0, description='Issue number of the magazine') + month: str = Field(..., description='Month of publication') + type: str = Field(..., description='Type of the magazine') + + def get_publication_info(self) -> str: + return f"Issue {self.issue}, published in {self.month} {self.year}" + + def __str__(self): + return f"'{self.title}' by {self.author}, {self.year} - Issue {self.issue}, {self.month}" + +class BookModel(LibraryItemModel): + type: str = Field(..., description='Type of the book') + + def get_publication_info(self) -> str: + return f"Book {self.issue}, published in {self.month} {self.year}" + + def __str__(self): + return f"'{self.title}' by {self.author}, {self.year}" +