Skip to content

Commit f3168b8

Browse files
mpolivkaCopilot
andcommitted
feat(book-app): add list-unread command and get_unread_books method
- Add get_unread_books() to BookCollection, delegating to search_books - Add handle_list_unread() handler and list-unread CLI command - Update help text with new command - Add 17 comprehensive tests covering happy path, edge cases, data integrity, integration, and persistence - Include search_books and year validation additions that get_unread_books depends on Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bfa047c commit f3168b8

File tree

3 files changed

+340
-5
lines changed

3 files changed

+340
-5
lines changed

samples/book-app-project/book_app.py

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ def handle_remove():
5050
print("\nBook removed if it existed.\n")
5151

5252

53+
def handle_mark_read():
54+
print("\nMark a Book as Read\n")
55+
56+
title = input("Enter the title of the book: ").strip()
57+
result = collection.mark_as_read(title)
58+
59+
if result:
60+
print("\nBook marked as read.\n")
61+
else:
62+
print("\nBook not found.\n")
63+
64+
65+
def handle_list_unread():
66+
books = collection.get_unread_books()
67+
show_books(books)
68+
69+
5370
def handle_find():
5471
print("\nFind Books by Author\n")
5572

@@ -59,16 +76,76 @@ def handle_find():
5976
show_books(books)
6077

6178

79+
def handle_search():
80+
"""Handle the search command with composable flags."""
81+
args = sys.argv[2:]
82+
83+
query = None
84+
read_status = None
85+
year = None
86+
year_from = None
87+
year_to = None
88+
sort_by = None
89+
90+
i = 0
91+
while i < len(args):
92+
arg = args[i]
93+
if arg == "--read":
94+
read_status = True
95+
elif arg == "--unread":
96+
read_status = False
97+
elif arg == "--year" and i + 1 < len(args):
98+
i += 1
99+
year = int(args[i])
100+
elif arg == "--from" and i + 1 < len(args):
101+
i += 1
102+
year_from = int(args[i])
103+
elif arg == "--to" and i + 1 < len(args):
104+
i += 1
105+
year_to = int(args[i])
106+
elif arg == "--sort" and i + 1 < len(args):
107+
i += 1
108+
sort_by = args[i]
109+
elif not arg.startswith("--"):
110+
query = arg
111+
i += 1
112+
113+
books = collection.search_books(
114+
query=query,
115+
read_status=read_status,
116+
year=year,
117+
year_from=year_from,
118+
year_to=year_to,
119+
sort_by=sort_by,
120+
)
121+
show_books(books)
122+
123+
62124
def show_help():
63125
print("""
64126
Book Collection Helper
65127
66128
Commands:
67-
list - Show all books
68-
add - Add a new book
69-
remove - Remove a book by title
70-
find - Find books by author
71-
help - Show this help message
129+
list - Show all books
130+
list-unread - Show only unread books
131+
add - Add a new book
132+
remove - Remove a book by title
133+
find - Find books by author
134+
search - Search, filter, and sort books
135+
mark-read - Mark a book as read
136+
help - Show this help message
137+
138+
Search options:
139+
search <keyword> - Search by title or author
140+
search --read - Show only read books
141+
search --unread - Show only unread books
142+
search --year 1965 - Filter by exact year
143+
search --from 1940 - Filter from year (inclusive)
144+
search --to 1970 - Filter to year (inclusive)
145+
search --sort title - Sort by title, author, or year
146+
147+
Options can be combined:
148+
search tolkien --unread --sort year
72149
""")
73150

74151

@@ -81,12 +158,18 @@ def main():
81158

82159
if command == "list":
83160
handle_list()
161+
elif command == "list-unread":
162+
handle_list_unread()
84163
elif command == "add":
85164
handle_add()
86165
elif command == "remove":
87166
handle_remove()
88167
elif command == "find":
89168
handle_find()
169+
elif command == "search":
170+
handle_search()
171+
elif command == "mark-read":
172+
handle_mark_read()
90173
elif command == "help":
91174
show_help()
92175
else:

samples/book-app-project/books.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from dataclasses import dataclass, asdict
3+
from datetime import datetime
34
from typing import List, Optional
45

56
DATA_FILE = "data.json"
@@ -36,6 +37,9 @@ def save_books(self):
3637
json.dump([asdict(b) for b in self.books], f, indent=2)
3738

3839
def add_book(self, title: str, author: str, year: int) -> Book:
40+
current_year = datetime.now().year
41+
if year < 1000 or year > current_year:
42+
raise ValueError(f"Year must be between 1000 and {current_year}")
3943
book = Book(title=title, author=author, year=year)
4044
self.books.append(book)
4145
self.save_books()
@@ -67,6 +71,46 @@ def remove_book(self, title: str) -> bool:
6771
return True
6872
return False
6973

74+
def get_unread_books(self) -> List[Book]:
75+
"""Return all books that have not been read."""
76+
return self.search_books(read_status=False)
77+
7078
def find_by_author(self, author: str) -> List[Book]:
7179
"""Find all books by a given author."""
7280
return [b for b in self.books if b.author.lower() == author.lower()]
81+
82+
def search_books(
83+
self,
84+
query: Optional[str] = None,
85+
read_status: Optional[bool] = None,
86+
year: Optional[int] = None,
87+
year_from: Optional[int] = None,
88+
year_to: Optional[int] = None,
89+
sort_by: Optional[str] = None,
90+
) -> List[Book]:
91+
"""Search, filter, and sort books. All filters are combined with AND logic."""
92+
results = list(self.books)
93+
94+
if query:
95+
q = query.lower()
96+
results = [
97+
b for b in results
98+
if q in b.title.lower() or q in b.author.lower()
99+
]
100+
101+
if read_status is not None:
102+
results = [b for b in results if b.read == read_status]
103+
104+
if year is not None:
105+
results = [b for b in results if b.year == year]
106+
107+
if year_from is not None:
108+
results = [b for b in results if b.year >= year_from]
109+
110+
if year_to is not None:
111+
results = [b for b in results if b.year <= year_to]
112+
113+
if sort_by in ("title", "author", "year"):
114+
results.sort(key=lambda b: getattr(b, sort_by))
115+
116+
return results

0 commit comments

Comments
 (0)