Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions data/items.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
233 changes: 233 additions & 0 deletions library.py
Original file line number Diff line number Diff line change
@@ -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
64 changes: 54 additions & 10 deletions main.py
Original file line number Diff line number Diff line change
@@ -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)
54 changes: 54 additions & 0 deletions models.py
Original file line number Diff line number Diff line change
@@ -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}"