├── .gitignore ├── 01_03_GitHub_Actions ├── app.py └── test_app.py ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── main.yml └── ISSUE_TEMPLATE.md ├── CONTRIBUTING.md ├── NOTICE ├── .devcontainer └── devcontainer.json ├── .vscode └── settings.json ├── 02_09_CodeProject_CodeReview ├── README.md └── book_service.py ├── 02_07_CodeProject_testCases ├── README.md ├── book_service.py └── test_book_service.py ├── Handouts ├── Prompts for To-Do List App.md └── Prompts for High-Signal Communication for Engineers.md ├── README.md ├── 02_06_CodeProject_BookService ├── README.md └── book_service_endState.py ├── 02_08_CodeProject └── book_service.py ├── 02_11_CodeProject_refactor ├── endState │ ├── test_refactor.py │ ├── book_service.py │ └── test_book_service.py └── beginState │ ├── book_service.py │ └── test_book_service.py ├── LICENSE └── 02_10_CodeProject_nav ├── book_service.py └── test_book_service.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tmp 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /01_03_GitHub_Actions/app.py: -------------------------------------------------------------------------------- 1 | def average(nums): 2 | if not nums: 3 | return 0 4 | return sum(nums) / len(nums) 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Codeowners for these exercise files: 2 | # * (asterisk) deotes "all files and folders" 3 | # Example: * @producer @instructor 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /01_03_GitHub_Actions/test_app.py: -------------------------------------------------------------------------------- 1 | import math 2 | from app import average 3 | 4 | def test_average_handles_empty(): 5 | assert average([]) == 0 6 | 7 | def test_average(): 8 | assert average([2,3]) == 2.5 9 | assert average([1,2,3]) == 2.0 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Copy To Branches 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | copy-to-branches: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - name: Copy To Branches Action 12 | uses: planetoftheweb/copy-to-branches@v1.2 13 | env: 14 | key: main 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Contribution Agreement 3 | ====================== 4 | 5 | This repository does not accept pull requests (PRs). All pull requests will be closed. 6 | 7 | However, if any contributions (through pull requests, issues, feedback or otherwise) are provided, as a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code (or otherwise providing feedback), you (and, if applicable, your employer) are licensing the submitted code (and/or feedback) to LinkedIn and the open source community subject to the BSD 2-Clause license. 8 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2025 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the LinkedIn Learning Exercise File License (the "License"). 5 | See LICENSE in the project root for license information. 6 | 7 | Please note, this project may automatically load third party code from external 8 | repositories (for example, NPM modules, Composer packages, or other dependencies). 9 | If so, such third party code may be subject to other license terms than as set 10 | forth above. In addition, such third party code may also depend on and load 11 | multiple tiers of dependencies. Please review the applicable licenses of the 12 | additional dependencies. 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "customizations": { 3 | "codespaces": { 4 | "openFiles": [ 5 | "README.md" 6 | ] 7 | }, 8 | "vscode": { 9 | // Set *default* container specific settings.json values on container create. 10 | "settings": { 11 | "terminal.integrated.shell.linux": "/bin/bash" 12 | }, 13 | // Add the IDs of extensions you want installed when the container is created. 14 | "extensions": [ 15 | "linkedinlearning.linkedinlearning-vscode-theme" 16 | ] 17 | } 18 | }, 19 | // Update welcome text and set terminal prompt to '$ ' 20 | "onCreateCommand": "echo PS1='\"$ \"' >> ~/.bashrc", 21 | // Pull all branches 22 | "postAttachCommand": "git pull --all" 23 | } 24 | // DevContainer Reference: https://code.visualstudio.com/docs/remote/devcontainerjson-reference -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.cursorBlinking": "solid", 4 | "editor.fontFamily": "ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace", 5 | "editor.fontLigatures": false, 6 | "editor.fontSize": 22, 7 | "editor.formatOnPaste": true, 8 | "editor.formatOnSave": true, 9 | "editor.lineNumbers": "on", 10 | "editor.matchBrackets": "always", 11 | "editor.minimap.enabled": false, 12 | "editor.smoothScrolling": true, 13 | "editor.tabSize": 2, 14 | "editor.useTabStops": true, 15 | "emmet.triggerExpansionOnTab": true, 16 | "files.autoSave": "afterDelay", 17 | "terminal.integrated.fontSize": 18, 18 | "workbench.colorTheme": "LinkedIn Learning Dark", 19 | "workbench.fontAliasing": "antialiased", 20 | "workbench.statusBar.visible": true 21 | } -------------------------------------------------------------------------------- /02_09_CodeProject_CodeReview/README.md: -------------------------------------------------------------------------------- 1 | This document demonstrates how to use AI as a code reviewer for learning and development purposes. This is a companion to Chapter 2, Video 9 "Coding Project - Review Python code with AI". 2 | 3 | ## Overview 4 | 5 | Learn to leverage AI for constructive code reviews by providing clear roles, context, and specific formatting requirements. This approach helps simulate real-world code review experiences. 6 | 7 | ### The Prompt 8 | 9 | Copy and use this exact prompt with your AI assistant when reviewing code: 10 | 11 | ``` 12 | Role: Staff Engineer & code reviewer — kind but direct. 13 | 14 | Context: The class below was written by a new grad for a tiny in-memory Books service. 15 | 16 | Task: Critique the class with a prioritized list. Use tags [Blocker], [Major], [Minor]. For each item include: 17 | 1) the issue, 2) why it matters, 3) the smallest fix (code or one-line diff). 18 | 19 | Keep it under 12 bullets. Suggest only minimal changes that keep the spirit of the code. 20 | ``` 21 | -------------------------------------------------------------------------------- /02_07_CodeProject_testCases/README.md: -------------------------------------------------------------------------------- 1 | These are the prompts used to design test cases for Chapter 2, Video 7 "Coding Project - Create AI-assisted test cases in Python". Follow the prompts below to practice AI-assisted development and testing workflows. 2 | 3 | ### Training Prompt 1 - Basic Demo Function 4 | 5 | Copy and use this prompt with your AI assistant: 6 | 7 | ``` 8 | Write a demo() function for this BookService that: 9 | Creates two books. 10 | Gets the first book. 11 | Lists all books. 12 | Deletes one book and verifies it's gone. 13 | Use plain assert statements. 14 | ``` 15 | ### Training Prompt 2 - Comprehensive Edge Cases 16 | 17 | Copy and use this prompt with your AI assistant: 18 | 19 | ``` 20 | For this pure-Python BookService, list edge cases to test. Include invalid inputs, boundary ids, search quirks, sort stability, duplicate titles/authors, and very large lists. 21 | ``` 22 | 23 | ### Training Prompt 3 - Convert to pytest 24 | 25 | Next, we’ll convert these to pytest with parametrization. Copy and use this prompt with your AI assistant: 26 | 27 | ``` 28 | Convert these tests to pytest with parametrize for sorting. Keep tests simple and readable. 29 | ``` 30 | 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Issue Overview 9 | 10 | 11 | ## Describe your environment 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ## Expected Behavior 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | ## Possible Solution 28 | 29 | 30 | ## Screenshots / Video 31 | 32 | 33 | ## Related Issues 34 | 35 | -------------------------------------------------------------------------------- /Handouts/Prompts for To-Do List App.md: -------------------------------------------------------------------------------- 1 | # Prompts for To-Do List App 2 | 3 | --- 4 | 5 | ## First Prompt 6 | 7 | > I want to build a Todo list web app. Please help me break this down into user stories, epics, and subtasks with estimated effort (e.g., in story points) and a suggested timeline for sprints. You can see here that I’ve provided the context and the output format I would like. 8 | 9 | **Context:** 10 | - It’s a simple web app where users can create, update, and delete tasks 11 | - Core features: User authentication (sign up or login), task CRUD, categories or tags, due dates, and a clean UI 12 | - Tech stack: React (front end), Node.js/Express (back end), and PostgreSQL (database) 13 | - Team size: Three developers 14 | - Sprint length: Two weeks 15 | 16 | **Output format I would like:** 17 | - Epics (for example, “Authentication”, “Task Management”, or “UI/UX”) 18 | - User stories under each epic (in user story format, for example, “As a user, I want to…so that…”) 19 | - Subtasks for each story (for example, API endpoint, database migration, or UI component) 20 | - Effort estimates (use story points: 1, 2, 3, 5, 8) 21 | - Suggested sprint plan (assign stories or subtasks to two-week sprints given a team velocity of approximately 25 points per sprint) 22 | 23 | **Please present the output in a clear, structured table with columns: Sprint, Epic, User Story, Subtasks, Estimate (SP).** 24 | 25 | ## Second Prompt 26 | 27 | > I’m building a simple to-do app in Python. Please write only the function signatures (no implementations) for the four basic CRUD operations on a task resource. 28 | -------------------------------------------------------------------------------- /Handouts/Prompts for High-Signal Communication for Engineers.md: -------------------------------------------------------------------------------- 1 | # Prompts for High-Signal Communication for Engineers 2 | 3 | --- 4 | 5 | ## Slack update to an engineer teammate 6 | 7 | **Intent:** Unblock a tiny product decision 8 | **Audience:** Peer engineer 9 | **Medium:** Slack 10 | 11 | **Message:** 12 | - **Status:** Fix ready behind a flag; added title validation 13 | - **Impact:** Empty items confuse lists and generate support pings 14 | - **Details:** 15 | - PR #123 adds trim + empty-check 16 | - Proposed UX: Block save with inline error “Title can’t be empty.” 17 | - Alt: auto-fill “Untitled” (not preferred) 18 | 19 | **Ask:** Pick block vs. auto-fill by 2 p.m. so I can merge. 20 | 21 | --- 22 | 23 | **AI prompt 1:** 24 | “Rewrite this for Slack using Status, Impact, Details, and end with the Ask plus the deadline.” 25 | 26 | **AI prompt 2:** 27 | “Turn this into a 30-second standup update using Status, Impact, Details, then the Ask.” 28 | 29 | --- 30 | 31 | ## Prompt for Summarizing Meeting Prep Documents 32 | 33 | Summarize the design doc for pre-meeting prep in eight or less bullets: 34 | - Purpose 35 | - Decisions requested 36 | - Key constraints (dates, SLAs, versions, or owners) 37 | - Proposed approach 38 | - Top risks and mitigations 39 | - Open questions (with suggested owner) 40 | - Metrics and success criteria 41 | - Links and PRs 42 | 43 | Additionally, you can also use a prompt like the following to generate questions to ask during the meeting: 44 | Read the doc below and propose six meeting questions, labeled: 45 | 1) Clarifying 46 | 2) Scope 47 | 3) Risk 48 | 4) Dependency 49 | 5) Metrics 50 | 6) Timeline 51 | Keep each to one sentence. 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Accelerate DevOps and Software Development with AI: Modern Tools and Workflows for Enhanced Software Delivery 2 | This is the repository for the LinkedIn Learning course `Accelerate DevOps and Software Development with AI: Modern Tools and Workflows for Enhanced Software Delivery`. The full course is available from [LinkedIn Learning][lil-course-url]. 3 | 4 | ![course-name-alt-text][lil-thumbnail-url] 5 | 6 | ## Course Description 7 | 8 | AI is dramatically changing the career landscape for entry-level software engineers and DevOps professionals. In this course, join instructor Fatima Taj as she shows you how AI is reshaping modern software development. Explore how tools like GitHub Copilot and ChatGPT integrate into every phase of the software lifecycle—from writing code to automating CI/CD pipelines and responding to incidents. Through hands-on examples and real-world case studies, Fatima shares pointers on when to trust AI, when to double-check its output, and how to use it ethically and responsibly. Along the way, learn how to leverage critical skills that remain essential in an AI-driven world, like debugging, system design, and secure coding practices. By the end of this course, you’ll also be prepared to showcase your newly acquired AI skills in interviews to effectively communicate your value to potential employers. 9 | 10 | ## Instructor 11 | 12 | Fatima Taj 13 | 14 | Senior Software Engineer at Yelp 15 | 16 | 17 | [0]: # (Replace these placeholder URLs with actual course URLs) 18 | 19 | [lil-course-url]: https://www.linkedin.com/learning/ai-for-junior-developers-devops-enhance-your-software-development-and-devops-workflows-with-ai 20 | [lil-thumbnail-url]: https://media.licdn.com/dms/image/v2/D4E0DAQG6RZXq4McTdA/learning-public-crop_675_1200/B4EZqEQ0rgGcAY-/0/1763155594206?e=2147483647&v=beta&t=4lr2yIhG_bGdy3HCANIzFikEVLu6yiifRI9oMRXDdX4 21 | 22 | -------------------------------------------------------------------------------- /02_06_CodeProject_BookService/README.md: -------------------------------------------------------------------------------- 1 | # Book Service Project 2 | 3 | This readme file is a reference for the Coding Project in Chapter 2, Video 6 "Coding Project - Speed up python coding with smart prompts". You can compare your work to the completed file in this directory labeled "book_service_endState.py". 4 | 5 | We'll start with the shape of the class: method names, arguments, return types. Then we'll fill in the details together. 6 | 7 | ## Getting Started 8 | 9 | ### Step 1: Class Structure 10 | 11 | I'm going to use this prompt: 12 | 13 | ``` 14 | Generate a Python class called BookService with four methods: create_book(title, author), get_book(book_id), list_books(search?, sort_by?, ascending?), and delete_book(book_id). Just return type hints and docstrings for now — no implementation. 15 | ``` 16 | 17 | ### Step 2: Constructor 18 | 19 | We'll start with the constructor. Every service needs state. We'll store books in a list and keep a counter for the next ID. 20 | 21 | I'll now use this prompt: 22 | 23 | ``` 24 | Implement the constructor for BookService so it initializes an empty list of books and a counter _next_id starting at 1. 25 | ``` 26 | 27 | ### Step 3: Create Book Method 28 | 29 | Next, we're going to implement the `create_book` method. We need validation since the title and author can't be empty, then assign an ID, store the book, and return it. This time we'll use a more detailed prompt: 30 | 31 | ``` 32 | Implement create_book so it: 33 | - Validates that title and author are non-empty strings after stripping whitespace. 34 | - Raises ValueError if invalid. 35 | - Creates a dict with id, title, and author. 36 | - Appends it to the internal list. 37 | - Increments the counter 38 | - Returns the dict. 39 | ``` 40 | 41 | ### Step 4: Remaining Methods 42 | 43 | I'll leave the remaining functions for you to implement: 44 | 45 | - `get_book(book_id)` - Retrieve a book by its ID 46 | - `list_books(search?, sort_by?, ascending?)` - List books with optional search and sorting 47 | - `delete_book(book_id)` - Remove a book by its ID 48 | -------------------------------------------------------------------------------- /02_08_CodeProject/book_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Dict, Any 2 | 3 | 4 | class BookService: 5 | """In-memory book service with CRUD operations.""" 6 | 7 | def __init__(self) -> None: 8 | """Initialize BookService with empty book list and ID counter.""" 9 | self.books: List[Dict[str, Any]] = [] 10 | self._next_id: int = 1 11 | 12 | def create_book(self, title: str, author: str) -> Dict[str, Any]: 13 | """Create a new book and return it with generated ID. 14 | 15 | Args: 16 | title: Book title (non-empty string) 17 | author: Book author (non-empty string) 18 | 19 | Returns: 20 | Dict containing book data with generated ID 21 | 22 | Raises: 23 | ValueError: If title or author is invalid 24 | """ 25 | # Strip whitespace and validate 26 | title = title.strip() 27 | author = author.strip() 28 | 29 | if not title or not author: 30 | raise ValueError("Title and author must be non-empty strings") 31 | 32 | # Create book dict 33 | book = { 34 | "id": str(self._next_id), 35 | "title": title, 36 | "author": author 37 | } 38 | 39 | # Add to list and increment counter 40 | self.books.append(book) 41 | self._next_id += 1 42 | 43 | return book 44 | 45 | def get_book(self, book_id: str) -> Optional[Dict[str, Any]]: 46 | """Retrieve a book by its ID. 47 | 48 | Args: 49 | book_id: Unique book identifier 50 | 51 | Returns: 52 | Book data dict if found, None otherwise 53 | """ 54 | for book in self.books: 55 | if book["id"] == book_id: 56 | return book 57 | return None 58 | 59 | def list_books(self, search: Optional[str] = None, 60 | sort_by: Optional[str] = None, 61 | ascending: bool = True) -> List[Dict[str, Any]]: 62 | """List books with optional search and sorting. 63 | 64 | Args: 65 | search: Optional search term for title/author filtering 66 | sort_by: Field to sort by ('title', 'author', 'id') 67 | ascending: Sort direction (default: True) 68 | 69 | Returns: 70 | List of book dictionaries 71 | 72 | Raises: 73 | ValueError: If sort_by field is invalid 74 | """ 75 | # Start with all books 76 | result = self.books.copy() 77 | 78 | # Filter by search if provided 79 | if search: 80 | search_lower = search.lower() 81 | result = [ 82 | book for book in result 83 | if search_lower in book["title"].lower() or 84 | search_lower in book["author"].lower() 85 | ] 86 | 87 | # Sort if sort_by is provided 88 | if sort_by: 89 | valid_fields = {"id", "title", "author"} 90 | if sort_by not in valid_fields: 91 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 92 | 93 | def sort_key(book): 94 | value = book[sort_by] 95 | if sort_by == "id": 96 | return int(value) # Sort IDs numerically 97 | return value.lower() # Case-insensitive for strings 98 | 99 | result.sort(key=sort_key, reverse=not ascending) 100 | 101 | return result 102 | 103 | def delete_book(self, book_id: str) -> bool: 104 | """Delete a book by its ID. 105 | 106 | Args: 107 | book_id: Unique book identifier 108 | 109 | Returns: 110 | True if deleted, False if not found 111 | """ 112 | for i, book in enumerate(self.books): 113 | if book["id"] == book_id: 114 | self.books.pop(i) 115 | return True 116 | return False 117 | -------------------------------------------------------------------------------- /02_06_CodeProject_BookService/book_service_endState.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Dict, Any 2 | 3 | 4 | class BookService: 5 | """In-memory book service with CRUD operations.""" 6 | 7 | def __init__(self) -> None: 8 | """Initialize BookService with empty book list and ID counter.""" 9 | self.books: List[Dict[str, Any]] = [] 10 | self._next_id: int = 1 11 | 12 | def create_book(self, title: str, author: str) -> Dict[str, Any]: 13 | """Create a new book and return it with generated ID. 14 | 15 | Args: 16 | title: Book title (non-empty string) 17 | author: Book author (non-empty string) 18 | 19 | Returns: 20 | Dict containing book data with generated ID 21 | 22 | Raises: 23 | ValueError: If title or author is invalid 24 | """ 25 | # Strip whitespace and validate 26 | title = title.strip() 27 | author = author.strip() 28 | 29 | if not title or not author: 30 | raise ValueError("Title and author must be non-empty strings") 31 | 32 | # Create book dict 33 | book = { 34 | "id": str(self._next_id), 35 | "title": title, 36 | "author": author 37 | } 38 | 39 | # Add to list and increment counter 40 | self.books.append(book) 41 | self._next_id += 1 42 | 43 | return book 44 | 45 | def get_book(self, book_id: str) -> Optional[Dict[str, Any]]: 46 | """Retrieve a book by its ID. 47 | 48 | Args: 49 | book_id: Unique book identifier 50 | 51 | Returns: 52 | Book data dict if found, None otherwise 53 | """ 54 | for book in self.books: 55 | if book["id"] == book_id: 56 | return book 57 | return None 58 | 59 | def list_books(self, search: Optional[str] = None, 60 | sort_by: Optional[str] = None, 61 | ascending: bool = True) -> List[Dict[str, Any]]: 62 | """List books with optional search and sorting. 63 | 64 | Args: 65 | search: Optional search term for title/author filtering 66 | sort_by: Field to sort by ('title', 'author', 'id') 67 | ascending: Sort direction (default: True) 68 | 69 | Returns: 70 | List of book dictionaries 71 | 72 | Raises: 73 | ValueError: If sort_by field is invalid 74 | """ 75 | # Start with all books 76 | result = self.books.copy() 77 | 78 | # Filter by search if provided 79 | if search: 80 | search_lower = search.lower() 81 | result = [ 82 | book for book in result 83 | if search_lower in book["title"].lower() or 84 | search_lower in book["author"].lower() 85 | ] 86 | 87 | # Sort if sort_by is provided 88 | if sort_by: 89 | valid_fields = {"id", "title", "author"} 90 | if sort_by not in valid_fields: 91 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 92 | 93 | def sort_key(book): 94 | value = book[sort_by] 95 | if sort_by == "id": 96 | return int(value) # Sort IDs numerically 97 | return value.lower() # Case-insensitive for strings 98 | 99 | result.sort(key=sort_key, reverse=not ascending) 100 | 101 | return result 102 | 103 | def delete_book(self, book_id: str) -> bool: 104 | """Delete a book by its ID. 105 | 106 | Args: 107 | book_id: Unique book identifier 108 | 109 | Returns: 110 | True if deleted, False if not found 111 | """ 112 | for i, book in enumerate(self.books): 113 | if book["id"] == book_id: 114 | self.books.pop(i) 115 | return True 116 | return False 117 | -------------------------------------------------------------------------------- /02_11_CodeProject_refactor/endState/test_refactor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Simple test to verify the refactored list_books behavior. 4 | """ 5 | 6 | # Create a simplified version without type annotations for testing 7 | class BookService: 8 | def __init__(self): 9 | self.books = [] 10 | self._next_id = 1 11 | 12 | def create_book(self, title, author): 13 | title = title.strip() 14 | author = author.strip() 15 | 16 | if not title or not author: 17 | raise ValueError("Title and author must be non-empty strings") 18 | 19 | book = { 20 | "id": str(self._next_id), 21 | "title": title, 22 | "author": author 23 | } 24 | 25 | self.books.append(book) 26 | self._next_id += 1 27 | return book 28 | 29 | def list_books(self, search=None, sort_by=None, ascending=True): 30 | # Start with all books 31 | result = self.books.copy() 32 | 33 | # Filter by search if provided 34 | if search: 35 | search_lower = search.lower() 36 | result = [ 37 | book for book in result 38 | if search_lower in book["title"].lower() or 39 | search_lower in book["author"].lower() 40 | ] 41 | 42 | # Sort if sort_by is provided 43 | if sort_by: 44 | result = self._sort_books(result, sort_by, ascending) 45 | 46 | return result 47 | 48 | def _sort_books(self, books, sort_by, ascending=True): 49 | """Sort books by the specified field.""" 50 | valid_fields = {"id", "title", "author"} 51 | if sort_by not in valid_fields: 52 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 53 | 54 | def sort_key(book): 55 | value = book[sort_by] 56 | if sort_by == "id": 57 | return int(value) # Sort IDs numerically 58 | return value.lower() # Case-insensitive for strings 59 | 60 | sorted_books = books.copy() 61 | sorted_books.sort(key=sort_key, reverse=not ascending) 62 | return sorted_books 63 | 64 | def test_refactored_behavior(): 65 | """Test that the refactored behavior is identical to the original.""" 66 | print("Testing refactored BookService behavior...") 67 | 68 | # Create service and add test books 69 | service = BookService() 70 | service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 71 | service.create_book("Animal Farm", "George Orwell") 72 | service.create_book("1984", "George Orwell") 73 | service.create_book("To Kill a Mockingbird", "Harper Lee") 74 | 75 | print("\n1. Testing unsorted list:") 76 | books = service.list_books() 77 | for book in books: 78 | print(f" {book['title']} by {book['author']} (ID: {book['id']})") 79 | 80 | print("\n2. Testing sort by title (ascending):") 81 | books = service.list_books(sort_by="title") 82 | for book in books: 83 | print(f" {book['title']} by {book['author']}") 84 | 85 | print("\n3. Testing sort by title (descending):") 86 | books = service.list_books(sort_by="title", ascending=False) 87 | for book in books: 88 | print(f" {book['title']} by {book['author']}") 89 | 90 | print("\n4. Testing sort by author:") 91 | books = service.list_books(sort_by="author") 92 | for book in books: 93 | print(f" {book['title']} by {book['author']}") 94 | 95 | print("\n5. Testing sort by ID:") 96 | books = service.list_books(sort_by="id") 97 | for book in books: 98 | print(f" {book['title']} by {book['author']} (ID: {book['id']})") 99 | 100 | print("\n6. Testing search with sorting:") 101 | books = service.list_books(search="Orwell", sort_by="title") 102 | for book in books: 103 | print(f" {book['title']} by {book['author']}") 104 | 105 | print("\n7. Testing invalid sort field (should raise error):") 106 | try: 107 | service.list_books(sort_by="invalid_field") 108 | print(" ERROR: Should have raised ValueError") 109 | except ValueError as e: 110 | print(f" ✓ Correctly raised ValueError: {e}") 111 | 112 | print("\n✓ All tests completed successfully!") 113 | 114 | if __name__ == "__main__": 115 | test_refactored_behavior() 116 | -------------------------------------------------------------------------------- /02_07_CodeProject_testCases/book_service.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Dict, Any 2 | 3 | 4 | class BookService: 5 | """In-memory book service with CRUD operations.""" 6 | 7 | def __init__(self) -> None: 8 | """Initialize BookService with empty book list and ID counter.""" 9 | self.books: List[Dict[str, Any]] = [] 10 | self._next_id: int = 1 11 | 12 | def create_book(self, title: str, author: str) -> Dict[str, Any]: 13 | """Create a new book and return it with generated ID. 14 | 15 | Args: 16 | title: Book title (non-empty string) 17 | author: Book author (non-empty string) 18 | 19 | Returns: 20 | Dict containing book data with generated ID 21 | 22 | Raises: 23 | ValueError: If title or author is invalid 24 | """ 25 | # Strip whitespace and validate 26 | title = title.strip() 27 | author = author.strip() 28 | 29 | if not title or not author: 30 | raise ValueError("Title and author must be non-empty strings") 31 | 32 | # Create book dict 33 | book = { 34 | "id": str(self._next_id), 35 | "title": title, 36 | "author": author 37 | } 38 | 39 | # Add to list and increment counter 40 | self.books.append(book) 41 | self._next_id += 1 42 | 43 | return book 44 | 45 | def get_book(self, book_id: str) -> Optional[Dict[str, Any]]: 46 | """Retrieve a book by its ID. 47 | 48 | Args: 49 | book_id: Unique book identifier 50 | 51 | Returns: 52 | Book data dict if found, None otherwise 53 | """ 54 | for book in self.books: 55 | if book["id"] == book_id: 56 | return book 57 | return None 58 | 59 | def list_books(self, search: Optional[str] = None, 60 | sort_by: Optional[str] = None, 61 | ascending: bool = True) -> List[Dict[str, Any]]: 62 | """List books with optional search and sorting. 63 | 64 | Args: 65 | search: Optional search term for title/author filtering 66 | sort_by: Field to sort by ('title', 'author', 'id') 67 | ascending: Sort direction (default: True) 68 | 69 | Returns: 70 | List of book dictionaries 71 | 72 | Raises: 73 | ValueError: If sort_by field is invalid 74 | """ 75 | # Start with all books 76 | result = self.books.copy() 77 | 78 | # Filter by search if provided 79 | if search: 80 | search_lower = search.lower() 81 | result = [ 82 | book for book in result 83 | if search_lower in book["title"].lower() or 84 | search_lower in book["author"].lower() 85 | ] 86 | 87 | # Sort if sort_by is provided 88 | if sort_by: 89 | valid_fields = {"id", "title", "author"} 90 | if sort_by not in valid_fields: 91 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 92 | 93 | def sort_key(book): 94 | value = book[sort_by] 95 | if sort_by == "id": 96 | return int(value) # Sort IDs numerically 97 | return value.lower() # Case-insensitive for strings 98 | 99 | result.sort(key=sort_key, reverse=not ascending) 100 | 101 | return result 102 | 103 | def delete_book(self, book_id: str) -> bool: 104 | """Delete a book by its ID. 105 | 106 | Args: 107 | book_id: Unique book identifier 108 | 109 | Returns: 110 | True if deleted, False if not found 111 | """ 112 | for i, book in enumerate(self.books): 113 | if book["id"] == book_id: 114 | self.books.pop(i) 115 | return True 116 | return False 117 | 118 | 119 | def demo() -> None: 120 | """Demonstrate BookService functionality with basic CRUD operations.""" 121 | # Create BookService instance 122 | service = BookService() 123 | 124 | # Create two books 125 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 126 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 127 | 128 | # Get the first book 129 | retrieved_book = service.get_book(book1["id"]) 130 | assert retrieved_book is not None, "First book should be retrievable" 131 | assert retrieved_book["title"] == "The Great Gatsby", "Retrieved book should have correct title" 132 | assert retrieved_book["author"] == "F. Scott Fitzgerald", "Retrieved book should have correct author" 133 | 134 | # List all books 135 | all_books = service.list_books() 136 | assert len(all_books) == 2, "Should have 2 books total" 137 | assert book1 in all_books, "First book should be in the list" 138 | assert book2 in all_books, "Second book should be in the list" 139 | 140 | # Delete one book and verify it's gone 141 | delete_result = service.delete_book(book1["id"]) 142 | assert delete_result is True, "Book deletion should return True" 143 | 144 | # Verify the book is gone 145 | deleted_book = service.get_book(book1["id"]) 146 | assert deleted_book is None, "Deleted book should not be retrievable" 147 | 148 | # Verify only one book remains 149 | remaining_books = service.list_books() 150 | assert len(remaining_books) == 1, "Should have 1 book remaining" 151 | assert book2 in remaining_books, "Second book should still be in the list" 152 | assert book1 not in remaining_books, "First book should not be in the list" 153 | 154 | print("Demo completed successfully! All assertions passed.") 155 | 156 | 157 | if __name__ == "__main__": 158 | demo() 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LinkedIn Learning Exercise Files License Agreement 2 | ================================================== 3 | 4 | This License Agreement (the "Agreement") is a binding legal agreement 5 | between you (as an individual or entity, as applicable) and LinkedIn 6 | Corporation (“LinkedIn”). By downloading or using the LinkedIn Learning 7 | exercise files in this repository (“Licensed Materials”), you agree to 8 | be bound by the terms of this Agreement. If you do not agree to these 9 | terms, do not download or use the Licensed Materials. 10 | 11 | 1. License. 12 | - a. Subject to the terms of this Agreement, LinkedIn hereby grants LinkedIn 13 | members during their LinkedIn Learning subscription a non-exclusive, 14 | non-transferable copyright license, for internal use only, to 1) make a 15 | reasonable number of copies of the Licensed Materials, and 2) make 16 | derivative works of the Licensed Materials for the sole purpose of 17 | practicing skills taught in LinkedIn Learning courses. 18 | - b. Distribution. Unless otherwise noted in the Licensed Materials, subject 19 | to the terms of this Agreement, LinkedIn hereby grants LinkedIn members 20 | with a LinkedIn Learning subscription a non-exclusive, non-transferable 21 | copyright license to distribute the Licensed Materials, except the 22 | Licensed Materials may not be included in any product or service (or 23 | otherwise used) to instruct or educate others. 24 | 25 | 2. Restrictions and Intellectual Property. 26 | - a. You may not to use, modify, copy, make derivative works of, publish, 27 | distribute, rent, lease, sell, sublicense, assign or otherwise transfer the 28 | Licensed Materials, except as expressly set forth above in Section 1. 29 | - b. Linkedin (and its licensors) retains its intellectual property rights 30 | in the Licensed Materials. Except as expressly set forth in Section 1, 31 | LinkedIn grants no licenses. 32 | - c. You indemnify LinkedIn and its licensors and affiliates for i) any 33 | alleged infringement or misappropriation of any intellectual property rights 34 | of any third party based on modifications you make to the Licensed Materials, 35 | ii) any claims arising from your use or distribution of all or part of the 36 | Licensed Materials and iii) a breach of this Agreement. You will defend, hold 37 | harmless, and indemnify LinkedIn and its affiliates (and our and their 38 | respective employees, shareholders, and directors) from any claim or action 39 | brought by a third party, including all damages, liabilities, costs and 40 | expenses, including reasonable attorneys’ fees, to the extent resulting from, 41 | alleged to have resulted from, or in connection with: (a) your breach of your 42 | obligations herein; or (b) your use or distribution of any Licensed Materials. 43 | 44 | 3. Open source. This code may include open source software, which may be 45 | subject to other license terms as provided in the files. 46 | 47 | 4. Warranty Disclaimer. LINKEDIN PROVIDES THE LICENSED MATERIALS ON AN “AS IS” 48 | AND “AS AVAILABLE” BASIS. LINKEDIN MAKES NO REPRESENTATION OR WARRANTY, 49 | WHETHER EXPRESS OR IMPLIED, ABOUT THE LICENSED MATERIALS, INCLUDING ANY 50 | REPRESENTATION THAT THE LICENSED MATERIALS WILL BE FREE OF ERRORS, BUGS OR 51 | INTERRUPTIONS, OR THAT THE LICENSED MATERIALS ARE ACCURATE, COMPLETE OR 52 | OTHERWISE VALID. TO THE FULLEST EXTENT PERMITTED BY LAW, LINKEDIN AND ITS 53 | AFFILIATES DISCLAIM ANY IMPLIED OR STATUTORY WARRANTY OR CONDITION, INCLUDING 54 | ANY IMPLIED WARRANTY OR CONDITION OF MERCHANTABILITY OR FITNESS FOR A 55 | PARTICULAR PURPOSE, AVAILABILITY, SECURITY, TITLE AND/OR NON-INFRINGEMENT. 56 | YOUR USE OF THE LICENSED MATERIALS IS AT YOUR OWN DISCRETION AND RISK, AND 57 | YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE THAT RESULTS FROM USE OF THE 58 | LICENSED MATERIALS TO YOUR COMPUTER SYSTEM OR LOSS OF DATA. NO ADVICE OR 59 | INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US OR THROUGH OR 60 | FROM THE LICENSED MATERIALS WILL CREATE ANY WARRANTY OR CONDITION NOT 61 | EXPRESSLY STATED IN THESE TERMS. 62 | 63 | 5. Limitation of Liability. LINKEDIN SHALL NOT BE LIABLE FOR ANY INDIRECT, 64 | INCIDENTAL, SPECIAL, PUNITIVE, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING 65 | BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER 66 | INTANGIBLE LOSSES . IN NO EVENT WILL LINKEDIN'S AGGREGATE LIABILITY TO YOU 67 | EXCEED $100. THIS LIMITATION OF LIABILITY SHALL: 68 | - i. APPLY REGARDLESS OF WHETHER (A) YOU BASE YOUR CLAIM ON CONTRACT, TORT, 69 | STATUTE, OR ANY OTHER LEGAL THEORY, (B) WE KNEW OR SHOULD HAVE KNOWN ABOUT 70 | THE POSSIBILITY OF SUCH DAMAGES, OR (C) THE LIMITED REMEDIES PROVIDED IN THIS 71 | SECTION FAIL OF THEIR ESSENTIAL PURPOSE; AND 72 | - ii. NOT APPLY TO ANY DAMAGE THAT LINKEDIN MAY CAUSE YOU INTENTIONALLY OR 73 | KNOWINGLY IN VIOLATION OF THESE TERMS OR APPLICABLE LAW, OR AS OTHERWISE 74 | MANDATED BY APPLICABLE LAW THAT CANNOT BE DISCLAIMED IN THESE TERMS. 75 | 76 | 6. Termination. This Agreement automatically terminates upon your breach of 77 | this Agreement or termination of your LinkedIn Learning subscription. On 78 | termination, all licenses granted under this Agreement will terminate 79 | immediately and you will delete the Licensed Materials. Sections 2-7 of this 80 | Agreement survive any termination of this Agreement. LinkedIn may discontinue 81 | the availability of some or all of the Licensed Materials at any time for any 82 | reason. 83 | 84 | 7. Miscellaneous. This Agreement will be governed by and construed in 85 | accordance with the laws of the State of California without regard to conflict 86 | of laws principles. The exclusive forum for any disputes arising out of or 87 | relating to this Agreement shall be an appropriate federal or state court 88 | sitting in the County of Santa Clara, State of California. If LinkedIn does 89 | not act to enforce a breach of this Agreement, that does not mean that 90 | LinkedIn has waived its right to enforce this Agreement. The Agreement does 91 | not create a partnership, agency relationship, or joint venture between the 92 | parties. Neither party has the power or authority to bind the other or to 93 | create any obligation or responsibility on behalf of the other. You may not, 94 | without LinkedIn’s prior written consent, assign or delegate any rights or 95 | obligations under these terms, including in connection with a change of 96 | control. Any purported assignment and delegation shall be ineffective. The 97 | Agreement shall bind and inure to the benefit of the parties, their respective 98 | successors and permitted assigns. If any provision of the Agreement is 99 | unenforceable, that provision will be modified to render it enforceable to the 100 | extent possible to give effect to the parties’ intentions and the remaining 101 | provisions will not be affected. This Agreement is the only agreement between 102 | you and LinkedIn regarding the Licensed Materials, and supersedes all prior 103 | agreements relating to the Licensed Materials. 104 | 105 | Last Updated: March 2019 106 | -------------------------------------------------------------------------------- /02_10_CodeProject_nav/book_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | BookService: In-memory book management with CRUD operations. 3 | 4 | This module provides a simple in-memory book service for managing a collection 5 | of books with basic CRUD (Create, Read, Update, Delete) operations. The service 6 | supports creating books with titles and authors, retrieving books by ID, 7 | listing books with optional search and sorting, and deleting books. 8 | 9 | Methods: 10 | create_book(title, author): Create a new book with auto-generated ID 11 | get_book(book_id): Retrieve a book by its unique ID 12 | list_books(search=None, sort_by=None, ascending=True): List books with optional filtering and sorting 13 | delete_book(book_id): Remove a book from the collection 14 | 15 | Usage Example: 16 | from book_service import BookService 17 | 18 | # Create service instance 19 | service = BookService() 20 | 21 | # Add some books 22 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 23 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 24 | 25 | # Search and sort books 26 | gatsby_books = service.list_books(search="Gatsby") 27 | sorted_books = service.list_books(sort_by="title", ascending=True) 28 | 29 | # Retrieve specific book 30 | book = service.get_book(book1["id"]) 31 | 32 | # Delete a book 33 | service.delete_book(book2["id"]) 34 | """ 35 | 36 | 37 | 38 | class BookService: 39 | """In-memory book service with CRUD operations.""" 40 | 41 | def __init__(self) -> None: 42 | """Initialize BookService with empty book list and ID counter. 43 | 44 | Initializes a new BookService instance with an empty list of books 45 | and sets the next available ID to 1. 46 | """ 47 | self.books: list[dict[str, str]] = [] 48 | self._next_id: int = 1 49 | 50 | def create_book(self, title: str, author: str) -> dict[str, str]: 51 | """Create a new book and return it with generated ID. 52 | 53 | Creates a new book entry with the provided title and author. The book 54 | is assigned a unique auto-generated ID and added to the service's 55 | collection. 56 | 57 | Args: 58 | title: The title of the book. Must be a non-empty string after 59 | whitespace is stripped. 60 | author: The author of the book. Must be a non-empty string after 61 | whitespace is stripped. 62 | 63 | Returns: 64 | A dictionary containing the book data with keys 'id', 'title', and 65 | 'author'. The 'id' is a string representation of the auto-generated 66 | unique identifier. 67 | 68 | Raises: 69 | ValueError: If either title or author is empty or contains only 70 | whitespace after stripping. 71 | 72 | Example: 73 | >>> service = BookService() 74 | >>> book = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 75 | >>> print(book['id']) 76 | '1' 77 | >>> print(book['title']) 78 | 'The Great Gatsby' 79 | """ 80 | # Strip whitespace and validate 81 | title = title.strip() 82 | author = author.strip() 83 | 84 | if not title or not author: 85 | raise ValueError("Title and author must be non-empty strings") 86 | 87 | # Create book dict 88 | book = { 89 | "id": str(self._next_id), 90 | "title": title, 91 | "author": author 92 | } 93 | 94 | # Add to list and increment counter 95 | self.books.append(book) 96 | self._next_id += 1 97 | 98 | return book 99 | 100 | def get_book(self, book_id: str) -> dict[str, str] | None: 101 | """Retrieve a book by its unique identifier. 102 | 103 | Searches the book collection for a book with the specified ID and 104 | returns its data if found. 105 | 106 | Args: 107 | book_id: The unique string identifier of the book to retrieve. 108 | 109 | Returns: 110 | A dictionary containing the book data with keys 'id', 'title', and 111 | 'author' if the book is found, otherwise None. 112 | 113 | Example: 114 | >>> service = BookService() 115 | >>> service.create_book("1984", "George Orwell") 116 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 117 | >>> book = service.get_book('1') 118 | >>> print(book['title']) 119 | 1984 120 | >>> service.get_book('999') 121 | None 122 | """ 123 | for book in self.books: 124 | if book["id"] == book_id: 125 | return book 126 | return None 127 | 128 | def list_books(self, search: str | None = None, 129 | sort_by: str | None = None, 130 | ascending: bool = True) -> list[dict[str, str]]: 131 | """List books with optional search filtering and sorting. 132 | 133 | Returns a list of all books in the service, optionally filtered by 134 | a search term and sorted by a specified field. 135 | 136 | Args: 137 | search: Optional search term to filter books by. Searches both 138 | title and author fields (case-insensitive). If None, no 139 | filtering is applied. 140 | sort_by: Optional field name to sort by. Valid values are 'title', 141 | 'author', or 'id'. If None, no sorting is applied. 142 | ascending: Sort direction when sort_by is specified. True for 143 | ascending order, False for descending order. Defaults to True. 144 | 145 | Returns: 146 | A list of dictionaries, each containing book data with keys 'id', 147 | 'title', and 'author'. The list may be filtered and/or sorted 148 | based on the provided parameters. 149 | 150 | Raises: 151 | ValueError: If sort_by is specified but not one of the valid 152 | field names ('title', 'author', 'id'). 153 | 154 | Example: 155 | >>> service = BookService() 156 | >>> service.create_book("1984", "George Orwell") 157 | >>> service.create_book("Animal Farm", "George Orwell") 158 | >>> service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 159 | >>> # List all books 160 | >>> all_books = service.list_books() 161 | >>> # Search for Orwell books 162 | >>> orwell_books = service.list_books(search="Orwell") 163 | >>> # Sort by title 164 | >>> sorted_books = service.list_books(sort_by="title") 165 | """ 166 | # Start with all books 167 | result = self.books.copy() 168 | 169 | # Filter by search if provided 170 | if search: 171 | search_lower = search.lower() 172 | result = [ 173 | book for book in result 174 | if search_lower in book["title"].lower() or 175 | search_lower in book["author"].lower() 176 | ] 177 | 178 | # Sort if sort_by is provided 179 | if sort_by: 180 | valid_fields = {"id", "title", "author"} 181 | if sort_by not in valid_fields: 182 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 183 | 184 | def sort_key(book): 185 | value = book[sort_by] 186 | if sort_by == "id": 187 | return int(value) # Sort IDs numerically 188 | return value.lower() # Case-insensitive for strings 189 | 190 | result.sort(key=sort_key, reverse=not ascending) 191 | 192 | return result 193 | 194 | def delete_book(self, book_id: str) -> bool: 195 | """Delete a book from the collection by its unique identifier. 196 | 197 | Removes the book with the specified ID from the service's collection. 198 | The book is permanently removed and cannot be recovered. 199 | 200 | Args: 201 | book_id: The unique string identifier of the book to delete. 202 | 203 | Returns: 204 | True if the book was found and successfully deleted, False if 205 | no book with the specified ID was found. 206 | 207 | Example: 208 | >>> service = BookService() 209 | >>> service.create_book("1984", "George Orwell") 210 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 211 | >>> service.delete_book('1') 212 | True 213 | >>> service.delete_book('999') 214 | False 215 | >>> service.get_book('1') 216 | None 217 | """ 218 | for i, book in enumerate(self.books): 219 | if book["id"] == book_id: 220 | self.books.pop(i) 221 | return True 222 | return False 223 | 224 | 225 | def demo() -> None: 226 | """Demonstrate BookService functionality with basic CRUD operations.""" 227 | # Create BookService instance 228 | service = BookService() 229 | 230 | # Create two books 231 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 232 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 233 | 234 | # Get the first book 235 | retrieved_book = service.get_book(book1["id"]) 236 | assert retrieved_book is not None, "First book should be retrievable" 237 | assert retrieved_book["title"] == "The Great Gatsby", "Retrieved book should have correct title" 238 | assert retrieved_book["author"] == "F. Scott Fitzgerald", "Retrieved book should have correct author" 239 | 240 | # List all books 241 | all_books = service.list_books() 242 | assert len(all_books) == 2, "Should have 2 books total" 243 | assert book1 in all_books, "First book should be in the list" 244 | assert book2 in all_books, "Second book should be in the list" 245 | 246 | # Delete one book and verify it's gone 247 | delete_result = service.delete_book(book1["id"]) 248 | assert delete_result is True, "Book deletion should return True" 249 | 250 | # Verify the book is gone 251 | deleted_book = service.get_book(book1["id"]) 252 | assert deleted_book is None, "Deleted book should not be retrievable" 253 | 254 | # Verify only one book remains 255 | remaining_books = service.list_books() 256 | assert len(remaining_books) == 1, "Should have 1 book remaining" 257 | assert book2 in remaining_books, "Second book should still be in the list" 258 | assert book1 not in remaining_books, "First book should not be in the list" 259 | 260 | print("Demo completed successfully! All assertions passed.") 261 | 262 | 263 | if __name__ == "__main__": 264 | demo() 265 | -------------------------------------------------------------------------------- /02_09_CodeProject_CodeReview/book_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | BookService: In-memory book management with CRUD operations. 3 | 4 | This module provides a simple in-memory book service for managing a collection 5 | of books with basic CRUD (Create, Read, Update, Delete) operations. The service 6 | supports creating books with titles and authors, retrieving books by ID, 7 | listing books with optional search and sorting, and deleting books. 8 | 9 | Methods: 10 | create_book(title, author): Create a new book with auto-generated ID 11 | get_book(book_id): Retrieve a book by its unique ID 12 | list_books(search=None, sort_by=None, ascending=True): List books with optional filtering and sorting 13 | delete_book(book_id): Remove a book from the collection 14 | 15 | Usage Example: 16 | from book_service import BookService 17 | 18 | # Create service instance 19 | service = BookService() 20 | 21 | # Add some books 22 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 23 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 24 | 25 | # Search and sort books 26 | gatsby_books = service.list_books(search="Gatsby") 27 | sorted_books = service.list_books(sort_by="title", ascending=True) 28 | 29 | # Retrieve specific book 30 | book = service.get_book(book1["id"]) 31 | 32 | # Delete a book 33 | service.delete_book(book2["id"]) 34 | """ 35 | 36 | 37 | 38 | class BookService: 39 | """In-memory book service with CRUD operations.""" 40 | 41 | def __init__(self) -> None: 42 | """Initialize BookService with empty book list and ID counter. 43 | 44 | Initializes a new BookService instance with an empty list of books 45 | and sets the next available ID to 1. 46 | """ 47 | self.books: list[dict[str, str]] = [] 48 | self._next_id: int = 1 49 | 50 | def create_book(self, title: str, author: str) -> dict[str, str]: 51 | """Create a new book and return it with generated ID. 52 | 53 | Creates a new book entry with the provided title and author. The book 54 | is assigned a unique auto-generated ID and added to the service's 55 | collection. 56 | 57 | Args: 58 | title: The title of the book. Must be a non-empty string after 59 | whitespace is stripped. 60 | author: The author of the book. Must be a non-empty string after 61 | whitespace is stripped. 62 | 63 | Returns: 64 | A dictionary containing the book data with keys 'id', 'title', and 65 | 'author'. The 'id' is a string representation of the auto-generated 66 | unique identifier. 67 | 68 | Raises: 69 | ValueError: If either title or author is empty or contains only 70 | whitespace after stripping. 71 | 72 | Example: 73 | >>> service = BookService() 74 | >>> book = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 75 | >>> print(book['id']) 76 | '1' 77 | >>> print(book['title']) 78 | 'The Great Gatsby' 79 | """ 80 | # Strip whitespace and validate 81 | title = title.strip() 82 | author = author.strip() 83 | 84 | if not title or not author: 85 | raise ValueError("Title and author must be non-empty strings") 86 | 87 | # Create book dict 88 | book = { 89 | "id": str(self._next_id), 90 | "title": title, 91 | "author": author 92 | } 93 | 94 | # Add to list and increment counter 95 | self.books.append(book) 96 | self._next_id += 1 97 | 98 | return book 99 | 100 | def get_book(self, book_id: str) -> dict[str, str] | None: 101 | """Retrieve a book by its unique identifier. 102 | 103 | Searches the book collection for a book with the specified ID and 104 | returns its data if found. 105 | 106 | Args: 107 | book_id: The unique string identifier of the book to retrieve. 108 | 109 | Returns: 110 | A dictionary containing the book data with keys 'id', 'title', and 111 | 'author' if the book is found, otherwise None. 112 | 113 | Example: 114 | >>> service = BookService() 115 | >>> service.create_book("1984", "George Orwell") 116 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 117 | >>> book = service.get_book('1') 118 | >>> print(book['title']) 119 | 1984 120 | >>> service.get_book('999') 121 | None 122 | """ 123 | for book in self.books: 124 | if book["id"] == book_id: 125 | return book 126 | return None 127 | 128 | def list_books(self, search: str | None = None, 129 | sort_by: str | None = None, 130 | ascending: bool = True) -> list[dict[str, str]]: 131 | """List books with optional search filtering and sorting. 132 | 133 | Returns a list of all books in the service, optionally filtered by 134 | a search term and sorted by a specified field. 135 | 136 | Args: 137 | search: Optional search term to filter books by. Searches both 138 | title and author fields (case-insensitive). If None, no 139 | filtering is applied. 140 | sort_by: Optional field name to sort by. Valid values are 'title', 141 | 'author', or 'id'. If None, no sorting is applied. 142 | ascending: Sort direction when sort_by is specified. True for 143 | ascending order, False for descending order. Defaults to True. 144 | 145 | Returns: 146 | A list of dictionaries, each containing book data with keys 'id', 147 | 'title', and 'author'. The list may be filtered and/or sorted 148 | based on the provided parameters. 149 | 150 | Raises: 151 | ValueError: If sort_by is specified but not one of the valid 152 | field names ('title', 'author', 'id'). 153 | 154 | Example: 155 | >>> service = BookService() 156 | >>> service.create_book("1984", "George Orwell") 157 | >>> service.create_book("Animal Farm", "George Orwell") 158 | >>> service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 159 | >>> # List all books 160 | >>> all_books = service.list_books() 161 | >>> # Search for Orwell books 162 | >>> orwell_books = service.list_books(search="Orwell") 163 | >>> # Sort by title 164 | >>> sorted_books = service.list_books(sort_by="title") 165 | """ 166 | # Start with all books 167 | result = self.books.copy() 168 | 169 | # Filter by search if provided 170 | if search: 171 | search_lower = search.lower() 172 | result = [ 173 | book for book in result 174 | if search_lower in book["title"].lower() or 175 | search_lower in book["author"].lower() 176 | ] 177 | 178 | # Sort if sort_by is provided 179 | if sort_by: 180 | valid_fields = {"id", "title", "author"} 181 | if sort_by not in valid_fields: 182 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 183 | 184 | def sort_key(book): 185 | value = book[sort_by] 186 | if sort_by == "id": 187 | return int(value) # Sort IDs numerically 188 | return value.lower() # Case-insensitive for strings 189 | 190 | result.sort(key=sort_key, reverse=not ascending) 191 | 192 | return result 193 | 194 | def delete_book(self, book_id: str) -> bool: 195 | """Delete a book from the collection by its unique identifier. 196 | 197 | Removes the book with the specified ID from the service's collection. 198 | The book is permanently removed and cannot be recovered. 199 | 200 | Args: 201 | book_id: The unique string identifier of the book to delete. 202 | 203 | Returns: 204 | True if the book was found and successfully deleted, False if 205 | no book with the specified ID was found. 206 | 207 | Example: 208 | >>> service = BookService() 209 | >>> service.create_book("1984", "George Orwell") 210 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 211 | >>> service.delete_book('1') 212 | True 213 | >>> service.delete_book('999') 214 | False 215 | >>> service.get_book('1') 216 | None 217 | """ 218 | for i, book in enumerate(self.books): 219 | if book["id"] == book_id: 220 | self.books.pop(i) 221 | return True 222 | return False 223 | 224 | 225 | def demo() -> None: 226 | """Demonstrate BookService functionality with basic CRUD operations.""" 227 | # Create BookService instance 228 | service = BookService() 229 | 230 | # Create two books 231 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 232 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 233 | 234 | # Get the first book 235 | retrieved_book = service.get_book(book1["id"]) 236 | assert retrieved_book is not None, "First book should be retrievable" 237 | assert retrieved_book["title"] == "The Great Gatsby", "Retrieved book should have correct title" 238 | assert retrieved_book["author"] == "F. Scott Fitzgerald", "Retrieved book should have correct author" 239 | 240 | # List all books 241 | all_books = service.list_books() 242 | assert len(all_books) == 2, "Should have 2 books total" 243 | assert book1 in all_books, "First book should be in the list" 244 | assert book2 in all_books, "Second book should be in the list" 245 | 246 | # Delete one book and verify it's gone 247 | delete_result = service.delete_book(book1["id"]) 248 | assert delete_result is True, "Book deletion should return True" 249 | 250 | # Verify the book is gone 251 | deleted_book = service.get_book(book1["id"]) 252 | assert deleted_book is None, "Deleted book should not be retrievable" 253 | 254 | # Verify only one book remains 255 | remaining_books = service.list_books() 256 | assert len(remaining_books) == 1, "Should have 1 book remaining" 257 | assert book2 in remaining_books, "Second book should still be in the list" 258 | assert book1 not in remaining_books, "First book should not be in the list" 259 | 260 | print("Demo completed successfully! All assertions passed.") 261 | 262 | 263 | if __name__ == "__main__": 264 | demo() 265 | -------------------------------------------------------------------------------- /02_11_CodeProject_refactor/beginState/book_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | BookService: In-memory book management with CRUD operations. 3 | 4 | This module provides a simple in-memory book service for managing a collection 5 | of books with basic CRUD (Create, Read, Update, Delete) operations. The service 6 | supports creating books with titles and authors, retrieving books by ID, 7 | listing books with optional search and sorting, and deleting books. 8 | 9 | Methods: 10 | create_book(title, author): Create a new book with auto-generated ID 11 | get_book(book_id): Retrieve a book by its unique ID 12 | list_books(search=None, sort_by=None, ascending=True): List books with optional filtering and sorting 13 | delete_book(book_id): Remove a book from the collection 14 | 15 | Usage Example: 16 | from book_service import BookService 17 | 18 | # Create service instance 19 | service = BookService() 20 | 21 | # Add some books 22 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 23 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 24 | 25 | # Search and sort books 26 | gatsby_books = service.list_books(search="Gatsby") 27 | sorted_books = service.list_books(sort_by="title", ascending=True) 28 | 29 | # Retrieve specific book 30 | book = service.get_book(book1["id"]) 31 | 32 | # Delete a book 33 | service.delete_book(book2["id"]) 34 | """ 35 | 36 | 37 | 38 | class BookService: 39 | """In-memory book service with CRUD operations.""" 40 | 41 | def __init__(self) -> None: 42 | """Initialize BookService with empty book list and ID counter. 43 | 44 | Initializes a new BookService instance with an empty list of books 45 | and sets the next available ID to 1. 46 | """ 47 | self.books: list[dict[str, str]] = [] 48 | self._next_id: int = 1 49 | 50 | def create_book(self, title: str, author: str) -> dict[str, str]: 51 | """Create a new book and return it with generated ID. 52 | 53 | Creates a new book entry with the provided title and author. The book 54 | is assigned a unique auto-generated ID and added to the service's 55 | collection. 56 | 57 | Args: 58 | title: The title of the book. Must be a non-empty string after 59 | whitespace is stripped. 60 | author: The author of the book. Must be a non-empty string after 61 | whitespace is stripped. 62 | 63 | Returns: 64 | A dictionary containing the book data with keys 'id', 'title', and 65 | 'author'. The 'id' is a string representation of the auto-generated 66 | unique identifier. 67 | 68 | Raises: 69 | ValueError: If either title or author is empty or contains only 70 | whitespace after stripping. 71 | 72 | Example: 73 | >>> service = BookService() 74 | >>> book = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 75 | >>> print(book['id']) 76 | '1' 77 | >>> print(book['title']) 78 | 'The Great Gatsby' 79 | """ 80 | # Strip whitespace and validate 81 | title = title.strip() 82 | author = author.strip() 83 | 84 | if not title or not author: 85 | raise ValueError("Title and author must be non-empty strings") 86 | 87 | # Create book dict 88 | book = { 89 | "id": str(self._next_id), 90 | "title": title, 91 | "author": author 92 | } 93 | 94 | # Add to list and increment counter 95 | self.books.append(book) 96 | self._next_id += 1 97 | 98 | return book 99 | 100 | def get_book(self, book_id: str) -> dict[str, str] | None: 101 | """Retrieve a book by its unique identifier. 102 | 103 | Searches the book collection for a book with the specified ID and 104 | returns its data if found. 105 | 106 | Args: 107 | book_id: The unique string identifier of the book to retrieve. 108 | 109 | Returns: 110 | A dictionary containing the book data with keys 'id', 'title', and 111 | 'author' if the book is found, otherwise None. 112 | 113 | Example: 114 | >>> service = BookService() 115 | >>> service.create_book("1984", "George Orwell") 116 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 117 | >>> book = service.get_book('1') 118 | >>> print(book['title']) 119 | 1984 120 | >>> service.get_book('999') 121 | None 122 | """ 123 | for book in self.books: 124 | if book["id"] == book_id: 125 | return book 126 | return None 127 | 128 | def list_books(self, search: str | None = None, 129 | sort_by: str | None = None, 130 | ascending: bool = True) -> list[dict[str, str]]: 131 | """List books with optional search filtering and sorting. 132 | 133 | Returns a list of all books in the service, optionally filtered by 134 | a search term and sorted by a specified field. 135 | 136 | Args: 137 | search: Optional search term to filter books by. Searches both 138 | title and author fields (case-insensitive). If None, no 139 | filtering is applied. 140 | sort_by: Optional field name to sort by. Valid values are 'title', 141 | 'author', or 'id'. If None, no sorting is applied. 142 | ascending: Sort direction when sort_by is specified. True for 143 | ascending order, False for descending order. Defaults to True. 144 | 145 | Returns: 146 | A list of dictionaries, each containing book data with keys 'id', 147 | 'title', and 'author'. The list may be filtered and/or sorted 148 | based on the provided parameters. 149 | 150 | Raises: 151 | ValueError: If sort_by is specified but not one of the valid 152 | field names ('title', 'author', 'id'). 153 | 154 | Example: 155 | >>> service = BookService() 156 | >>> service.create_book("1984", "George Orwell") 157 | >>> service.create_book("Animal Farm", "George Orwell") 158 | >>> service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 159 | >>> # List all books 160 | >>> all_books = service.list_books() 161 | >>> # Search for Orwell books 162 | >>> orwell_books = service.list_books(search="Orwell") 163 | >>> # Sort by title 164 | >>> sorted_books = service.list_books(sort_by="title") 165 | """ 166 | # Start with all books 167 | result = self.books.copy() 168 | 169 | # Filter by search if provided 170 | if search: 171 | search_lower = search.lower() 172 | result = [ 173 | book for book in result 174 | if search_lower in book["title"].lower() or 175 | search_lower in book["author"].lower() 176 | ] 177 | 178 | # Sort if sort_by is provided 179 | if sort_by: 180 | valid_fields = {"id", "title", "author"} 181 | if sort_by not in valid_fields: 182 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 183 | 184 | def sort_key(book): 185 | value = book[sort_by] 186 | if sort_by == "id": 187 | return int(value) # Sort IDs numerically 188 | return value.lower() # Case-insensitive for strings 189 | 190 | result.sort(key=sort_key, reverse=not ascending) 191 | 192 | return result 193 | 194 | def delete_book(self, book_id: str) -> bool: 195 | """Delete a book from the collection by its unique identifier. 196 | 197 | Removes the book with the specified ID from the service's collection. 198 | The book is permanently removed and cannot be recovered. 199 | 200 | Args: 201 | book_id: The unique string identifier of the book to delete. 202 | 203 | Returns: 204 | True if the book was found and successfully deleted, False if 205 | no book with the specified ID was found. 206 | 207 | Example: 208 | >>> service = BookService() 209 | >>> service.create_book("1984", "George Orwell") 210 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 211 | >>> service.delete_book('1') 212 | True 213 | >>> service.delete_book('999') 214 | False 215 | >>> service.get_book('1') 216 | None 217 | """ 218 | for i, book in enumerate(self.books): 219 | if book["id"] == book_id: 220 | self.books.pop(i) 221 | return True 222 | return False 223 | 224 | 225 | def demo() -> None: 226 | """Demonstrate BookService functionality with basic CRUD operations.""" 227 | # Create BookService instance 228 | service = BookService() 229 | 230 | # Create two books 231 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 232 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 233 | 234 | # Get the first book 235 | retrieved_book = service.get_book(book1["id"]) 236 | assert retrieved_book is not None, "First book should be retrievable" 237 | assert retrieved_book["title"] == "The Great Gatsby", "Retrieved book should have correct title" 238 | assert retrieved_book["author"] == "F. Scott Fitzgerald", "Retrieved book should have correct author" 239 | 240 | # List all books 241 | all_books = service.list_books() 242 | assert len(all_books) == 2, "Should have 2 books total" 243 | assert book1 in all_books, "First book should be in the list" 244 | assert book2 in all_books, "Second book should be in the list" 245 | 246 | # Delete one book and verify it's gone 247 | delete_result = service.delete_book(book1["id"]) 248 | assert delete_result is True, "Book deletion should return True" 249 | 250 | # Verify the book is gone 251 | deleted_book = service.get_book(book1["id"]) 252 | assert deleted_book is None, "Deleted book should not be retrievable" 253 | 254 | # Verify only one book remains 255 | remaining_books = service.list_books() 256 | assert len(remaining_books) == 1, "Should have 1 book remaining" 257 | assert book2 in remaining_books, "Second book should still be in the list" 258 | assert book1 not in remaining_books, "First book should not be in the list" 259 | 260 | print("Demo completed successfully! All assertions passed.") 261 | 262 | 263 | if __name__ == "__main__": 264 | demo() 265 | -------------------------------------------------------------------------------- /02_11_CodeProject_refactor/endState/book_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | BookService: In-memory book management with CRUD operations. 3 | 4 | This module provides a simple in-memory book service for managing a collection 5 | of books with basic CRUD (Create, Read, Update, Delete) operations. The service 6 | supports creating books with titles and authors, retrieving books by ID, 7 | listing books with optional search and sorting, and deleting books. 8 | 9 | Methods: 10 | create_book(title, author): Create a new book with auto-generated ID 11 | get_book(book_id): Retrieve a book by its unique ID 12 | list_books(search=None, sort_by=None, ascending=True): List books with optional filtering and sorting 13 | delete_book(book_id): Remove a book from the collection 14 | 15 | Usage Example: 16 | from book_service import BookService 17 | 18 | # Create service instance 19 | service = BookService() 20 | 21 | # Add some books 22 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 23 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 24 | 25 | # Search and sort books 26 | gatsby_books = service.list_books(search="Gatsby") 27 | sorted_books = service.list_books(sort_by="title", ascending=True) 28 | 29 | # Retrieve specific book 30 | book = service.get_book(book1["id"]) 31 | 32 | # Delete a book 33 | service.delete_book(book2["id"]) 34 | """ 35 | 36 | 37 | 38 | class BookService: 39 | """In-memory book service with CRUD operations.""" 40 | 41 | def __init__(self) -> None: 42 | """Initialize BookService with empty book list and ID counter. 43 | 44 | Initializes a new BookService instance with an empty list of books 45 | and sets the next available ID to 1. 46 | """ 47 | self.books: list[dict[str, str]] = [] 48 | self._next_id: int = 1 49 | 50 | def create_book(self, title: str, author: str) -> dict[str, str]: 51 | """Create a new book and return it with generated ID. 52 | 53 | Creates a new book entry with the provided title and author. The book 54 | is assigned a unique auto-generated ID and added to the service's 55 | collection. 56 | 57 | Args: 58 | title: The title of the book. Must be a non-empty string after 59 | whitespace is stripped. 60 | author: The author of the book. Must be a non-empty string after 61 | whitespace is stripped. 62 | 63 | Returns: 64 | A dictionary containing the book data with keys 'id', 'title', and 65 | 'author'. The 'id' is a string representation of the auto-generated 66 | unique identifier. 67 | 68 | Raises: 69 | ValueError: If either title or author is empty or contains only 70 | whitespace after stripping. 71 | 72 | Example: 73 | >>> service = BookService() 74 | >>> book = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 75 | >>> print(book['id']) 76 | '1' 77 | >>> print(book['title']) 78 | 'The Great Gatsby' 79 | """ 80 | # Strip whitespace and validate 81 | title = title.strip() 82 | author = author.strip() 83 | 84 | if not title or not author: 85 | raise ValueError("Title and author must be non-empty strings") 86 | 87 | # Create book dict 88 | book = { 89 | "id": str(self._next_id), 90 | "title": title, 91 | "author": author 92 | } 93 | 94 | # Add to list and increment counter 95 | self.books.append(book) 96 | self._next_id += 1 97 | 98 | return book 99 | 100 | def get_book(self, book_id: str) -> dict[str, str] | None: 101 | """Retrieve a book by its unique identifier. 102 | 103 | Searches the book collection for a book with the specified ID and 104 | returns its data if found. 105 | 106 | Args: 107 | book_id: The unique string identifier of the book to retrieve. 108 | 109 | Returns: 110 | A dictionary containing the book data with keys 'id', 'title', and 111 | 'author' if the book is found, otherwise None. 112 | 113 | Example: 114 | >>> service = BookService() 115 | >>> service.create_book("1984", "George Orwell") 116 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 117 | >>> book = service.get_book('1') 118 | >>> print(book['title']) 119 | 1984 120 | >>> service.get_book('999') 121 | None 122 | """ 123 | for book in self.books: 124 | if book["id"] == book_id: 125 | return book 126 | return None 127 | 128 | def list_books(self, search: str | None = None, 129 | sort_by: str | None = None, 130 | ascending: bool = True) -> list[dict[str, str]]: 131 | """List books with optional search filtering and sorting. 132 | 133 | Returns a list of all books in the service, optionally filtered by 134 | a search term and sorted by a specified field. 135 | 136 | Args: 137 | search: Optional search term to filter books by. Searches both 138 | title and author fields (case-insensitive). If None, no 139 | filtering is applied. 140 | sort_by: Optional field name to sort by. Valid values are 'title', 141 | 'author', or 'id'. If None, no sorting is applied. 142 | ascending: Sort direction when sort_by is specified. True for 143 | ascending order, False for descending order. Defaults to True. 144 | 145 | Returns: 146 | A list of dictionaries, each containing book data with keys 'id', 147 | 'title', and 'author'. The list may be filtered and/or sorted 148 | based on the provided parameters. 149 | 150 | Raises: 151 | ValueError: If sort_by is specified but not one of the valid 152 | field names ('title', 'author', 'id'). 153 | 154 | Example: 155 | >>> service = BookService() 156 | >>> service.create_book("1984", "George Orwell") 157 | >>> service.create_book("Animal Farm", "George Orwell") 158 | >>> service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 159 | >>> # List all books 160 | >>> all_books = service.list_books() 161 | >>> # Search for Orwell books 162 | >>> orwell_books = service.list_books(search="Orwell") 163 | >>> # Sort by title 164 | >>> sorted_books = service.list_books(sort_by="title") 165 | """ 166 | # Start with all books 167 | result = self.books.copy() 168 | 169 | # Filter by search if provided 170 | if search: 171 | search_lower = search.lower() 172 | result = [ 173 | book for book in result 174 | if search_lower in book["title"].lower() or 175 | search_lower in book["author"].lower() 176 | ] 177 | 178 | # Sort if sort_by is provided 179 | if sort_by: 180 | result = self._sort_books(result, sort_by, ascending) 181 | 182 | return result 183 | 184 | def _sort_books(self, books: list[dict[str, str]], 185 | sort_by: str, ascending: bool = True) -> list[dict[str, str]]: 186 | """Sort books by the specified field. 187 | 188 | Helper method to sort a list of books by a given field with optional 189 | sort direction. Handles both string and numeric sorting appropriately. 190 | 191 | Args: 192 | books: List of book dictionaries to sort 193 | sort_by: Field name to sort by ('id', 'title', or 'author') 194 | ascending: Sort direction - True for ascending, False for descending 195 | 196 | Returns: 197 | A new list of books sorted by the specified field 198 | 199 | Raises: 200 | ValueError: If sort_by is not one of the valid field names 201 | """ 202 | valid_fields = {"id", "title", "author"} 203 | if sort_by not in valid_fields: 204 | raise ValueError(f"Invalid sort field: {sort_by}. Must be one of {valid_fields}") 205 | 206 | def sort_key(book): 207 | value = book[sort_by] 208 | if sort_by == "id": 209 | return int(value) # Sort IDs numerically 210 | return value.lower() # Case-insensitive for strings 211 | 212 | sorted_books = books.copy() 213 | sorted_books.sort(key=sort_key, reverse=not ascending) 214 | return sorted_books 215 | 216 | def delete_book(self, book_id: str) -> bool: 217 | """Delete a book from the collection by its unique identifier. 218 | 219 | Removes the book with the specified ID from the service's collection. 220 | The book is permanently removed and cannot be recovered. 221 | 222 | Args: 223 | book_id: The unique string identifier of the book to delete. 224 | 225 | Returns: 226 | True if the book was found and successfully deleted, False if 227 | no book with the specified ID was found. 228 | 229 | Example: 230 | >>> service = BookService() 231 | >>> service.create_book("1984", "George Orwell") 232 | {'id': '1', 'title': '1984', 'author': 'George Orwell'} 233 | >>> service.delete_book('1') 234 | True 235 | >>> service.delete_book('999') 236 | False 237 | >>> service.get_book('1') 238 | None 239 | """ 240 | for i, book in enumerate(self.books): 241 | if book["id"] == book_id: 242 | self.books.pop(i) 243 | return True 244 | return False 245 | 246 | 247 | def demo() -> None: 248 | """Demonstrate BookService functionality with basic CRUD operations.""" 249 | # Create BookService instance 250 | service = BookService() 251 | 252 | # Create two books 253 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 254 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 255 | 256 | # Get the first book 257 | retrieved_book = service.get_book(book1["id"]) 258 | assert retrieved_book is not None, "First book should be retrievable" 259 | assert retrieved_book["title"] == "The Great Gatsby", "Retrieved book should have correct title" 260 | assert retrieved_book["author"] == "F. Scott Fitzgerald", "Retrieved book should have correct author" 261 | 262 | # List all books 263 | all_books = service.list_books() 264 | assert len(all_books) == 2, "Should have 2 books total" 265 | assert book1 in all_books, "First book should be in the list" 266 | assert book2 in all_books, "Second book should be in the list" 267 | 268 | # Delete one book and verify it's gone 269 | delete_result = service.delete_book(book1["id"]) 270 | assert delete_result is True, "Book deletion should return True" 271 | 272 | # Verify the book is gone 273 | deleted_book = service.get_book(book1["id"]) 274 | assert deleted_book is None, "Deleted book should not be retrievable" 275 | 276 | # Verify only one book remains 277 | remaining_books = service.list_books() 278 | assert len(remaining_books) == 1, "Should have 1 book remaining" 279 | assert book2 in remaining_books, "Second book should still be in the list" 280 | assert book1 not in remaining_books, "First book should not be in the list" 281 | 282 | print("Demo completed successfully! All assertions passed.") 283 | 284 | 285 | if __name__ == "__main__": 286 | demo() 287 | -------------------------------------------------------------------------------- /02_10_CodeProject_nav/test_book_service.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from book_service import BookService 3 | 4 | 5 | class TestBookServiceCreate: 6 | """Test create_book method edge cases.""" 7 | 8 | def test_create_valid_books(self): 9 | """Test creating valid books.""" 10 | service = BookService() 11 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 12 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 13 | 14 | assert book1["title"] == "The Great Gatsby" 15 | assert book1["author"] == "F. Scott Fitzgerald" 16 | assert book1["id"] == "1" 17 | assert book2["id"] == "2" 18 | assert len(service.books) == 2 19 | 20 | @pytest.mark.parametrize("title,author", [ 21 | ("", "Author"), 22 | ("Title", ""), 23 | (" ", "Author"), 24 | ("Title", " "), 25 | ("", ""), 26 | (" ", " "), 27 | ]) 28 | def test_create_empty_strings(self, title, author): 29 | """Test creating books with empty or whitespace-only strings.""" 30 | service = BookService() 31 | with pytest.raises(ValueError, match="Title and author must be non-empty strings"): 32 | service.create_book(title, author) 33 | 34 | @pytest.mark.parametrize("title,author", [ 35 | (None, "Author"), 36 | ("Title", None), 37 | (123, "Author"), 38 | ("Title", 456), 39 | ([], "Author"), 40 | ("Title", {}), 41 | ]) 42 | def test_create_invalid_types(self, title, author): 43 | """Test creating books with invalid input types.""" 44 | service = BookService() 45 | with pytest.raises((TypeError, ValueError)): 46 | service.create_book(title, author) 47 | 48 | def test_create_whitespace_stripping(self): 49 | """Test that whitespace is properly stripped.""" 50 | service = BookService() 51 | book = service.create_book(" The Great Gatsby ", " F. Scott Fitzgerald ") 52 | 53 | assert book["title"] == "The Great Gatsby" 54 | assert book["author"] == "F. Scott Fitzgerald" 55 | 56 | def test_create_unicode_characters(self): 57 | """Test creating books with Unicode characters.""" 58 | service = BookService() 59 | book = service.create_book("Café", "José María") 60 | 61 | assert book["title"] == "Café" 62 | assert book["author"] == "José María" 63 | 64 | def test_create_duplicate_titles_authors(self): 65 | """Test creating books with duplicate titles and authors.""" 66 | service = BookService() 67 | book1 = service.create_book("The Book", "Author") 68 | book2 = service.create_book("The Book", "Author") 69 | 70 | assert book1["id"] == "1" 71 | assert book2["id"] == "2" 72 | assert len(service.books) == 2 73 | 74 | 75 | class TestBookServiceGet: 76 | """Test get_book method edge cases.""" 77 | 78 | def test_get_existing_book(self): 79 | """Test getting an existing book.""" 80 | service = BookService() 81 | book = service.create_book("Test Book", "Test Author") 82 | retrieved = service.get_book(book["id"]) 83 | 84 | assert retrieved == book 85 | 86 | @pytest.mark.parametrize("book_id", [ 87 | "999", 88 | "0", 89 | "-1", 90 | "", 91 | " ", 92 | None, 93 | 123, 94 | [], 95 | {}, 96 | ]) 97 | def test_get_nonexistent_book(self, book_id): 98 | """Test getting non-existent books.""" 99 | service = BookService() 100 | service.create_book("Test Book", "Test Author") 101 | result = service.get_book(book_id) 102 | 103 | assert result is None 104 | 105 | def test_get_after_deletion(self): 106 | """Test getting a book after it's been deleted.""" 107 | service = BookService() 108 | book = service.create_book("Test Book", "Test Author") 109 | service.delete_book(book["id"]) 110 | result = service.get_book(book["id"]) 111 | 112 | assert result is None 113 | 114 | 115 | class TestBookServiceList: 116 | """Test list_books method edge cases.""" 117 | 118 | def test_list_empty_service(self): 119 | """Test listing books from empty service.""" 120 | service = BookService() 121 | books = service.list_books() 122 | 123 | assert books == [] 124 | 125 | def test_list_all_books(self): 126 | """Test listing all books.""" 127 | service = BookService() 128 | book1 = service.create_book("Book A", "Author A") 129 | book2 = service.create_book("Book B", "Author B") 130 | books = service.list_books() 131 | 132 | assert len(books) == 2 133 | assert book1 in books 134 | assert book2 in books 135 | 136 | @pytest.mark.parametrize("search_term,expected_count", [ 137 | ("book", 2), 138 | ("BOOK", 2), 139 | ("Book A", 1), 140 | ("Author A", 1), 141 | ("nonexistent", 0), 142 | ("", 2), 143 | (" ", 0), 144 | ]) 145 | def test_list_search(self, search_term, expected_count): 146 | """Test searching books.""" 147 | service = BookService() 148 | service.create_book("Book A", "Author A") 149 | service.create_book("Book B", "Author B") 150 | books = service.list_books(search=search_term) 151 | 152 | assert len(books) == expected_count 153 | 154 | def test_list_search_unicode(self): 155 | """Test searching with Unicode characters.""" 156 | service = BookService() 157 | service.create_book("Café", "José") 158 | books = service.list_books(search="café") 159 | 160 | assert len(books) == 1 161 | assert books[0]["title"] == "Café" 162 | 163 | @pytest.mark.parametrize("sort_by,ascending,expected_order", [ 164 | ("id", True, ["1", "2", "3"]), 165 | ("id", False, ["3", "2", "1"]), 166 | ("title", True, ["A Book", "B Book", "C Book"]), 167 | ("title", False, ["C Book", "B Book", "A Book"]), 168 | ("author", True, ["Author A", "Author B", "Author C"]), 169 | ("author", False, ["Author C", "Author B", "Author A"]), 170 | ]) 171 | def test_list_sorting(self, sort_by, ascending, expected_order): 172 | """Test sorting books by different fields and directions.""" 173 | service = BookService() 174 | service.create_book("C Book", "Author C") 175 | service.create_book("A Book", "Author A") 176 | service.create_book("B Book", "Author B") 177 | 178 | books = service.list_books(sort_by=sort_by, ascending=ascending) 179 | actual_order = [book[sort_by] for book in books] 180 | 181 | assert actual_order == expected_order 182 | 183 | def test_list_sort_stability(self): 184 | """Test that sorting is stable for identical values.""" 185 | service = BookService() 186 | book1 = service.create_book("Same Title", "Author A") 187 | book2 = service.create_book("Same Title", "Author B") 188 | 189 | books = service.list_books(sort_by="title", ascending=True) 190 | # Should maintain original order for identical titles 191 | assert books[0]["author"] == "Author A" 192 | assert books[1]["author"] == "Author B" 193 | 194 | @pytest.mark.parametrize("sort_by", [ 195 | "invalid_field", 196 | "price", 197 | "year", 198 | "", 199 | None, 200 | 123, 201 | ]) 202 | def test_list_invalid_sort_field(self, sort_by): 203 | """Test listing with invalid sort fields.""" 204 | service = BookService() 205 | service.create_book("Test Book", "Test Author") 206 | 207 | with pytest.raises(ValueError): 208 | service.list_books(sort_by=sort_by) 209 | 210 | def test_list_combined_search_sort(self): 211 | """Test combining search and sort.""" 212 | service = BookService() 213 | service.create_book("Book A", "Author A") 214 | service.create_book("Book B", "Author B") 215 | service.create_book("Book C", "Author C") 216 | 217 | books = service.list_books(search="Book", sort_by="title", ascending=False) 218 | titles = [book["title"] for book in books] 219 | 220 | assert titles == ["Book C", "Book B", "Book A"] 221 | 222 | 223 | class TestBookServiceDelete: 224 | """Test delete_book method edge cases.""" 225 | 226 | def test_delete_existing_book(self): 227 | """Test deleting an existing book.""" 228 | service = BookService() 229 | book = service.create_book("Test Book", "Test Author") 230 | result = service.delete_book(book["id"]) 231 | 232 | assert result is True 233 | assert len(service.books) == 0 234 | assert service.get_book(book["id"]) is None 235 | 236 | @pytest.mark.parametrize("book_id", [ 237 | "999", 238 | "0", 239 | "-1", 240 | "", 241 | " ", 242 | None, 243 | 123, 244 | [], 245 | {}, 246 | ]) 247 | def test_delete_nonexistent_book(self, book_id): 248 | """Test deleting non-existent books.""" 249 | service = BookService() 250 | service.create_book("Test Book", "Test Author") 251 | result = service.delete_book(book_id) 252 | 253 | assert result is False 254 | assert len(service.books) == 1 255 | 256 | def test_delete_idempotency(self): 257 | """Test that deleting the same book twice is idempotent.""" 258 | service = BookService() 259 | book = service.create_book("Test Book", "Test Author") 260 | 261 | # First deletion 262 | result1 = service.delete_book(book["id"]) 263 | assert result1 is True 264 | assert len(service.books) == 0 265 | 266 | # Second deletion 267 | result2 = service.delete_book(book["id"]) 268 | assert result2 is False 269 | assert len(service.books) == 0 270 | 271 | def test_delete_specific_book(self): 272 | """Test that deleting one book doesn't affect others.""" 273 | service = BookService() 274 | book1 = service.create_book("Book 1", "Author 1") 275 | book2 = service.create_book("Book 2", "Author 2") 276 | 277 | service.delete_book(book1["id"]) 278 | 279 | assert len(service.books) == 1 280 | assert service.get_book(book2["id"]) == book2 281 | assert service.get_book(book1["id"]) is None 282 | 283 | 284 | class TestBookServiceIntegration: 285 | """Test integration scenarios and complex workflows.""" 286 | 287 | def test_full_crud_workflow(self): 288 | """Test complete CRUD workflow.""" 289 | service = BookService() 290 | 291 | # Create 292 | book = service.create_book("Test Book", "Test Author") 293 | assert book["id"] == "1" 294 | 295 | # Read 296 | retrieved = service.get_book(book["id"]) 297 | assert retrieved == book 298 | 299 | # List 300 | books = service.list_books() 301 | assert len(books) == 1 302 | assert book in books 303 | 304 | # Update (simulated by delete + create) 305 | service.delete_book(book["id"]) 306 | updated_book = service.create_book("Updated Book", "Updated Author") 307 | assert updated_book["id"] == "2" 308 | 309 | # Delete 310 | result = service.delete_book(updated_book["id"]) 311 | assert result is True 312 | assert len(service.books) == 0 313 | 314 | def test_large_dataset(self): 315 | """Test with a larger dataset.""" 316 | service = BookService() 317 | 318 | # Create many books 319 | books = [] 320 | for i in range(100): 321 | book = service.create_book(f"Book {i}", f"Author {i}") 322 | books.append(book) 323 | 324 | assert len(service.books) == 100 325 | 326 | # Test search performance 327 | search_results = service.list_books(search="Book 5") 328 | assert len(search_results) == 1 329 | 330 | # Test sort performance 331 | sorted_books = service.list_books(sort_by="title", ascending=True) 332 | assert len(sorted_books) == 100 333 | assert sorted_books[0]["title"] == "Book 0" 334 | assert sorted_books[-1]["title"] == "Book 99" 335 | 336 | # Test delete performance 337 | service.delete_book(books[50]["id"]) 338 | assert len(service.books) == 99 339 | 340 | def test_unicode_handling(self): 341 | """Test comprehensive Unicode handling.""" 342 | service = BookService() 343 | 344 | unicode_books = [ 345 | ("Café", "José María"), 346 | ("naïve", "François"), 347 | ("résumé", "André"), 348 | ("中文", "作者"), 349 | ("العربية", "كاتب"), 350 | ("русский", "автор"), 351 | ] 352 | 353 | for title, author in unicode_books: 354 | book = service.create_book(title, author) 355 | assert book["title"] == title 356 | assert book["author"] == author 357 | 358 | # Test search with Unicode 359 | results = service.list_books(search="café") 360 | assert len(results) == 1 361 | assert results[0]["title"] == "Café" 362 | 363 | # Test sort with Unicode 364 | sorted_books = service.list_books(sort_by="title", ascending=True) 365 | assert len(sorted_books) == len(unicode_books) 366 | 367 | def test_edge_case_whitespace(self): 368 | """Test various whitespace edge cases.""" 369 | service = BookService() 370 | 371 | # Test whitespace in search 372 | service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 373 | results = service.list_books(search=" gatsby ") 374 | assert len(results) == 1 375 | 376 | # Test empty search 377 | results = service.list_books(search="") 378 | assert len(results) == 1 379 | 380 | # Test whitespace-only search 381 | results = service.list_books(search=" ") 382 | assert len(results) == 0 383 | 384 | def test_id_generation_sequence(self): 385 | """Test ID generation sequence and uniqueness.""" 386 | service = BookService() 387 | 388 | # Create and delete books to test ID sequence 389 | book1 = service.create_book("Book 1", "Author 1") 390 | book2 = service.create_book("Book 2", "Author 2") 391 | book3 = service.create_book("Book 3", "Author 3") 392 | 393 | assert book1["id"] == "1" 394 | assert book2["id"] == "2" 395 | assert book3["id"] == "3" 396 | 397 | # Delete middle book 398 | service.delete_book(book2["id"]) 399 | 400 | # Create new book - should get next ID 401 | book4 = service.create_book("Book 4", "Author 4") 402 | assert book4["id"] == "4" 403 | 404 | # Verify all IDs are unique 405 | all_books = service.list_books() 406 | ids = [book["id"] for book in all_books] 407 | assert len(set(ids)) == len(ids) # All unique 408 | 409 | 410 | if __name__ == "__main__": 411 | pytest.main([__file__, "-v"]) 412 | -------------------------------------------------------------------------------- /02_07_CodeProject_testCases/test_book_service.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from book_service import BookService 3 | 4 | 5 | class TestBookServiceCreate: 6 | """Test create_book method edge cases.""" 7 | 8 | def test_create_valid_books(self): 9 | """Test creating valid books.""" 10 | service = BookService() 11 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 12 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 13 | 14 | assert book1["title"] == "The Great Gatsby" 15 | assert book1["author"] == "F. Scott Fitzgerald" 16 | assert book1["id"] == "1" 17 | assert book2["id"] == "2" 18 | assert len(service.books) == 2 19 | 20 | @pytest.mark.parametrize("title,author", [ 21 | ("", "Author"), 22 | ("Title", ""), 23 | (" ", "Author"), 24 | ("Title", " "), 25 | ("", ""), 26 | (" ", " "), 27 | ]) 28 | def test_create_empty_strings(self, title, author): 29 | """Test creating books with empty or whitespace-only strings.""" 30 | service = BookService() 31 | with pytest.raises(ValueError, match="Title and author must be non-empty strings"): 32 | service.create_book(title, author) 33 | 34 | @pytest.mark.parametrize("title,author", [ 35 | (None, "Author"), 36 | ("Title", None), 37 | (123, "Author"), 38 | ("Title", 456), 39 | ([], "Author"), 40 | ("Title", {}), 41 | ]) 42 | def test_create_invalid_types(self, title, author): 43 | """Test creating books with invalid input types.""" 44 | service = BookService() 45 | with pytest.raises((TypeError, ValueError)): 46 | service.create_book(title, author) 47 | 48 | def test_create_whitespace_stripping(self): 49 | """Test that whitespace is properly stripped.""" 50 | service = BookService() 51 | book = service.create_book(" The Great Gatsby ", " F. Scott Fitzgerald ") 52 | 53 | assert book["title"] == "The Great Gatsby" 54 | assert book["author"] == "F. Scott Fitzgerald" 55 | 56 | def test_create_unicode_characters(self): 57 | """Test creating books with Unicode characters.""" 58 | service = BookService() 59 | book = service.create_book("Café", "José María") 60 | 61 | assert book["title"] == "Café" 62 | assert book["author"] == "José María" 63 | 64 | def test_create_duplicate_titles_authors(self): 65 | """Test creating books with duplicate titles and authors.""" 66 | service = BookService() 67 | book1 = service.create_book("The Book", "Author") 68 | book2 = service.create_book("The Book", "Author") 69 | 70 | assert book1["id"] == "1" 71 | assert book2["id"] == "2" 72 | assert len(service.books) == 2 73 | 74 | 75 | class TestBookServiceGet: 76 | """Test get_book method edge cases.""" 77 | 78 | def test_get_existing_book(self): 79 | """Test getting an existing book.""" 80 | service = BookService() 81 | book = service.create_book("Test Book", "Test Author") 82 | retrieved = service.get_book(book["id"]) 83 | 84 | assert retrieved == book 85 | 86 | @pytest.mark.parametrize("book_id", [ 87 | "999", 88 | "0", 89 | "-1", 90 | "", 91 | " ", 92 | None, 93 | 123, 94 | [], 95 | {}, 96 | ]) 97 | def test_get_nonexistent_book(self, book_id): 98 | """Test getting non-existent books.""" 99 | service = BookService() 100 | service.create_book("Test Book", "Test Author") 101 | result = service.get_book(book_id) 102 | 103 | assert result is None 104 | 105 | def test_get_after_deletion(self): 106 | """Test getting a book after it's been deleted.""" 107 | service = BookService() 108 | book = service.create_book("Test Book", "Test Author") 109 | service.delete_book(book["id"]) 110 | result = service.get_book(book["id"]) 111 | 112 | assert result is None 113 | 114 | 115 | class TestBookServiceList: 116 | """Test list_books method edge cases.""" 117 | 118 | def test_list_empty_service(self): 119 | """Test listing books from empty service.""" 120 | service = BookService() 121 | books = service.list_books() 122 | 123 | assert books == [] 124 | 125 | def test_list_all_books(self): 126 | """Test listing all books.""" 127 | service = BookService() 128 | book1 = service.create_book("Book A", "Author A") 129 | book2 = service.create_book("Book B", "Author B") 130 | books = service.list_books() 131 | 132 | assert len(books) == 2 133 | assert book1 in books 134 | assert book2 in books 135 | 136 | @pytest.mark.parametrize("search_term,expected_count", [ 137 | ("book", 2), 138 | ("BOOK", 2), 139 | ("Book A", 1), 140 | ("Author A", 1), 141 | ("nonexistent", 0), 142 | ("", 2), 143 | (" ", 0), 144 | ]) 145 | def test_list_search(self, search_term, expected_count): 146 | """Test searching books.""" 147 | service = BookService() 148 | service.create_book("Book A", "Author A") 149 | service.create_book("Book B", "Author B") 150 | books = service.list_books(search=search_term) 151 | 152 | assert len(books) == expected_count 153 | 154 | def test_list_search_unicode(self): 155 | """Test searching with Unicode characters.""" 156 | service = BookService() 157 | service.create_book("Café", "José") 158 | books = service.list_books(search="café") 159 | 160 | assert len(books) == 1 161 | assert books[0]["title"] == "Café" 162 | 163 | @pytest.mark.parametrize("sort_by,ascending,expected_order", [ 164 | ("id", True, ["1", "2", "3"]), 165 | ("id", False, ["3", "2", "1"]), 166 | ("title", True, ["A Book", "B Book", "C Book"]), 167 | ("title", False, ["C Book", "B Book", "A Book"]), 168 | ("author", True, ["Author A", "Author B", "Author C"]), 169 | ("author", False, ["Author C", "Author B", "Author A"]), 170 | ]) 171 | def test_list_sorting(self, sort_by, ascending, expected_order): 172 | """Test sorting books by different fields and directions.""" 173 | service = BookService() 174 | service.create_book("C Book", "Author C") 175 | service.create_book("A Book", "Author A") 176 | service.create_book("B Book", "Author B") 177 | 178 | books = service.list_books(sort_by=sort_by, ascending=ascending) 179 | actual_order = [book[sort_by] for book in books] 180 | 181 | assert actual_order == expected_order 182 | 183 | def test_list_sort_stability(self): 184 | """Test that sorting is stable for identical values.""" 185 | service = BookService() 186 | book1 = service.create_book("Same Title", "Author A") 187 | book2 = service.create_book("Same Title", "Author B") 188 | 189 | books = service.list_books(sort_by="title", ascending=True) 190 | # Should maintain original order for identical titles 191 | assert books[0]["author"] == "Author A" 192 | assert books[1]["author"] == "Author B" 193 | 194 | @pytest.mark.parametrize("sort_by", [ 195 | "invalid_field", 196 | "price", 197 | "year", 198 | "", 199 | None, 200 | 123, 201 | ]) 202 | def test_list_invalid_sort_field(self, sort_by): 203 | """Test listing with invalid sort fields.""" 204 | service = BookService() 205 | service.create_book("Test Book", "Test Author") 206 | 207 | with pytest.raises(ValueError): 208 | service.list_books(sort_by=sort_by) 209 | 210 | def test_list_combined_search_sort(self): 211 | """Test combining search and sort.""" 212 | service = BookService() 213 | service.create_book("Book A", "Author A") 214 | service.create_book("Book B", "Author B") 215 | service.create_book("Book C", "Author C") 216 | 217 | books = service.list_books(search="Book", sort_by="title", ascending=False) 218 | titles = [book["title"] for book in books] 219 | 220 | assert titles == ["Book C", "Book B", "Book A"] 221 | 222 | 223 | class TestBookServiceDelete: 224 | """Test delete_book method edge cases.""" 225 | 226 | def test_delete_existing_book(self): 227 | """Test deleting an existing book.""" 228 | service = BookService() 229 | book = service.create_book("Test Book", "Test Author") 230 | result = service.delete_book(book["id"]) 231 | 232 | assert result is True 233 | assert len(service.books) == 0 234 | assert service.get_book(book["id"]) is None 235 | 236 | @pytest.mark.parametrize("book_id", [ 237 | "999", 238 | "0", 239 | "-1", 240 | "", 241 | " ", 242 | None, 243 | 123, 244 | [], 245 | {}, 246 | ]) 247 | def test_delete_nonexistent_book(self, book_id): 248 | """Test deleting non-existent books.""" 249 | service = BookService() 250 | service.create_book("Test Book", "Test Author") 251 | result = service.delete_book(book_id) 252 | 253 | assert result is False 254 | assert len(service.books) == 1 255 | 256 | def test_delete_idempotency(self): 257 | """Test that deleting the same book twice is idempotent.""" 258 | service = BookService() 259 | book = service.create_book("Test Book", "Test Author") 260 | 261 | # First deletion 262 | result1 = service.delete_book(book["id"]) 263 | assert result1 is True 264 | assert len(service.books) == 0 265 | 266 | # Second deletion 267 | result2 = service.delete_book(book["id"]) 268 | assert result2 is False 269 | assert len(service.books) == 0 270 | 271 | def test_delete_specific_book(self): 272 | """Test that deleting one book doesn't affect others.""" 273 | service = BookService() 274 | book1 = service.create_book("Book 1", "Author 1") 275 | book2 = service.create_book("Book 2", "Author 2") 276 | 277 | service.delete_book(book1["id"]) 278 | 279 | assert len(service.books) == 1 280 | assert service.get_book(book2["id"]) == book2 281 | assert service.get_book(book1["id"]) is None 282 | 283 | 284 | class TestBookServiceIntegration: 285 | """Test integration scenarios and complex workflows.""" 286 | 287 | def test_full_crud_workflow(self): 288 | """Test complete CRUD workflow.""" 289 | service = BookService() 290 | 291 | # Create 292 | book = service.create_book("Test Book", "Test Author") 293 | assert book["id"] == "1" 294 | 295 | # Read 296 | retrieved = service.get_book(book["id"]) 297 | assert retrieved == book 298 | 299 | # List 300 | books = service.list_books() 301 | assert len(books) == 1 302 | assert book in books 303 | 304 | # Update (simulated by delete + create) 305 | service.delete_book(book["id"]) 306 | updated_book = service.create_book("Updated Book", "Updated Author") 307 | assert updated_book["id"] == "2" 308 | 309 | # Delete 310 | result = service.delete_book(updated_book["id"]) 311 | assert result is True 312 | assert len(service.books) == 0 313 | 314 | def test_large_dataset(self): 315 | """Test with a larger dataset.""" 316 | service = BookService() 317 | 318 | # Create many books 319 | books = [] 320 | for i in range(100): 321 | book = service.create_book(f"Book {i}", f"Author {i}") 322 | books.append(book) 323 | 324 | assert len(service.books) == 100 325 | 326 | # Test search performance 327 | search_results = service.list_books(search="Book 5") 328 | assert len(search_results) == 1 329 | 330 | # Test sort performance 331 | sorted_books = service.list_books(sort_by="title", ascending=True) 332 | assert len(sorted_books) == 100 333 | assert sorted_books[0]["title"] == "Book 0" 334 | assert sorted_books[-1]["title"] == "Book 99" 335 | 336 | # Test delete performance 337 | service.delete_book(books[50]["id"]) 338 | assert len(service.books) == 99 339 | 340 | def test_unicode_handling(self): 341 | """Test comprehensive Unicode handling.""" 342 | service = BookService() 343 | 344 | unicode_books = [ 345 | ("Café", "José María"), 346 | ("naïve", "François"), 347 | ("résumé", "André"), 348 | ("中文", "作者"), 349 | ("العربية", "كاتب"), 350 | ("русский", "автор"), 351 | ] 352 | 353 | for title, author in unicode_books: 354 | book = service.create_book(title, author) 355 | assert book["title"] == title 356 | assert book["author"] == author 357 | 358 | # Test search with Unicode 359 | results = service.list_books(search="café") 360 | assert len(results) == 1 361 | assert results[0]["title"] == "Café" 362 | 363 | # Test sort with Unicode 364 | sorted_books = service.list_books(sort_by="title", ascending=True) 365 | assert len(sorted_books) == len(unicode_books) 366 | 367 | def test_edge_case_whitespace(self): 368 | """Test various whitespace edge cases.""" 369 | service = BookService() 370 | 371 | # Test whitespace in search 372 | service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 373 | results = service.list_books(search=" gatsby ") 374 | assert len(results) == 1 375 | 376 | # Test empty search 377 | results = service.list_books(search="") 378 | assert len(results) == 1 379 | 380 | # Test whitespace-only search 381 | results = service.list_books(search=" ") 382 | assert len(results) == 0 383 | 384 | def test_id_generation_sequence(self): 385 | """Test ID generation sequence and uniqueness.""" 386 | service = BookService() 387 | 388 | # Create and delete books to test ID sequence 389 | book1 = service.create_book("Book 1", "Author 1") 390 | book2 = service.create_book("Book 2", "Author 2") 391 | book3 = service.create_book("Book 3", "Author 3") 392 | 393 | assert book1["id"] == "1" 394 | assert book2["id"] == "2" 395 | assert book3["id"] == "3" 396 | 397 | # Delete middle book 398 | service.delete_book(book2["id"]) 399 | 400 | # Create new book - should get next ID 401 | book4 = service.create_book("Book 4", "Author 4") 402 | assert book4["id"] == "4" 403 | 404 | # Verify all IDs are unique 405 | all_books = service.list_books() 406 | ids = [book["id"] for book in all_books] 407 | assert len(set(ids)) == len(ids) # All unique 408 | 409 | 410 | if __name__ == "__main__": 411 | pytest.main([__file__, "-v"]) 412 | -------------------------------------------------------------------------------- /02_11_CodeProject_refactor/beginState/test_book_service.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from book_service import BookService 3 | 4 | 5 | class TestBookServiceCreate: 6 | """Test create_book method edge cases.""" 7 | 8 | def test_create_valid_books(self): 9 | """Test creating valid books.""" 10 | service = BookService() 11 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 12 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 13 | 14 | assert book1["title"] == "The Great Gatsby" 15 | assert book1["author"] == "F. Scott Fitzgerald" 16 | assert book1["id"] == "1" 17 | assert book2["id"] == "2" 18 | assert len(service.books) == 2 19 | 20 | @pytest.mark.parametrize("title,author", [ 21 | ("", "Author"), 22 | ("Title", ""), 23 | (" ", "Author"), 24 | ("Title", " "), 25 | ("", ""), 26 | (" ", " "), 27 | ]) 28 | def test_create_empty_strings(self, title, author): 29 | """Test creating books with empty or whitespace-only strings.""" 30 | service = BookService() 31 | with pytest.raises(ValueError, match="Title and author must be non-empty strings"): 32 | service.create_book(title, author) 33 | 34 | @pytest.mark.parametrize("title,author", [ 35 | (None, "Author"), 36 | ("Title", None), 37 | (123, "Author"), 38 | ("Title", 456), 39 | ([], "Author"), 40 | ("Title", {}), 41 | ]) 42 | def test_create_invalid_types(self, title, author): 43 | """Test creating books with invalid input types.""" 44 | service = BookService() 45 | with pytest.raises((TypeError, ValueError)): 46 | service.create_book(title, author) 47 | 48 | def test_create_whitespace_stripping(self): 49 | """Test that whitespace is properly stripped.""" 50 | service = BookService() 51 | book = service.create_book(" The Great Gatsby ", " F. Scott Fitzgerald ") 52 | 53 | assert book["title"] == "The Great Gatsby" 54 | assert book["author"] == "F. Scott Fitzgerald" 55 | 56 | def test_create_unicode_characters(self): 57 | """Test creating books with Unicode characters.""" 58 | service = BookService() 59 | book = service.create_book("Café", "José María") 60 | 61 | assert book["title"] == "Café" 62 | assert book["author"] == "José María" 63 | 64 | def test_create_duplicate_titles_authors(self): 65 | """Test creating books with duplicate titles and authors.""" 66 | service = BookService() 67 | book1 = service.create_book("The Book", "Author") 68 | book2 = service.create_book("The Book", "Author") 69 | 70 | assert book1["id"] == "1" 71 | assert book2["id"] == "2" 72 | assert len(service.books) == 2 73 | 74 | 75 | class TestBookServiceGet: 76 | """Test get_book method edge cases.""" 77 | 78 | def test_get_existing_book(self): 79 | """Test getting an existing book.""" 80 | service = BookService() 81 | book = service.create_book("Test Book", "Test Author") 82 | retrieved = service.get_book(book["id"]) 83 | 84 | assert retrieved == book 85 | 86 | @pytest.mark.parametrize("book_id", [ 87 | "999", 88 | "0", 89 | "-1", 90 | "", 91 | " ", 92 | None, 93 | 123, 94 | [], 95 | {}, 96 | ]) 97 | def test_get_nonexistent_book(self, book_id): 98 | """Test getting non-existent books.""" 99 | service = BookService() 100 | service.create_book("Test Book", "Test Author") 101 | result = service.get_book(book_id) 102 | 103 | assert result is None 104 | 105 | def test_get_after_deletion(self): 106 | """Test getting a book after it's been deleted.""" 107 | service = BookService() 108 | book = service.create_book("Test Book", "Test Author") 109 | service.delete_book(book["id"]) 110 | result = service.get_book(book["id"]) 111 | 112 | assert result is None 113 | 114 | 115 | class TestBookServiceList: 116 | """Test list_books method edge cases.""" 117 | 118 | def test_list_empty_service(self): 119 | """Test listing books from empty service.""" 120 | service = BookService() 121 | books = service.list_books() 122 | 123 | assert books == [] 124 | 125 | def test_list_all_books(self): 126 | """Test listing all books.""" 127 | service = BookService() 128 | book1 = service.create_book("Book A", "Author A") 129 | book2 = service.create_book("Book B", "Author B") 130 | books = service.list_books() 131 | 132 | assert len(books) == 2 133 | assert book1 in books 134 | assert book2 in books 135 | 136 | @pytest.mark.parametrize("search_term,expected_count", [ 137 | ("book", 2), 138 | ("BOOK", 2), 139 | ("Book A", 1), 140 | ("Author A", 1), 141 | ("nonexistent", 0), 142 | ("", 2), 143 | (" ", 0), 144 | ]) 145 | def test_list_search(self, search_term, expected_count): 146 | """Test searching books.""" 147 | service = BookService() 148 | service.create_book("Book A", "Author A") 149 | service.create_book("Book B", "Author B") 150 | books = service.list_books(search=search_term) 151 | 152 | assert len(books) == expected_count 153 | 154 | def test_list_search_unicode(self): 155 | """Test searching with Unicode characters.""" 156 | service = BookService() 157 | service.create_book("Café", "José") 158 | books = service.list_books(search="café") 159 | 160 | assert len(books) == 1 161 | assert books[0]["title"] == "Café" 162 | 163 | @pytest.mark.parametrize("sort_by,ascending,expected_order", [ 164 | ("id", True, ["1", "2", "3"]), 165 | ("id", False, ["3", "2", "1"]), 166 | ("title", True, ["A Book", "B Book", "C Book"]), 167 | ("title", False, ["C Book", "B Book", "A Book"]), 168 | ("author", True, ["Author A", "Author B", "Author C"]), 169 | ("author", False, ["Author C", "Author B", "Author A"]), 170 | ]) 171 | def test_list_sorting(self, sort_by, ascending, expected_order): 172 | """Test sorting books by different fields and directions.""" 173 | service = BookService() 174 | service.create_book("C Book", "Author C") 175 | service.create_book("A Book", "Author A") 176 | service.create_book("B Book", "Author B") 177 | 178 | books = service.list_books(sort_by=sort_by, ascending=ascending) 179 | actual_order = [book[sort_by] for book in books] 180 | 181 | assert actual_order == expected_order 182 | 183 | def test_list_sort_stability(self): 184 | """Test that sorting is stable for identical values.""" 185 | service = BookService() 186 | book1 = service.create_book("Same Title", "Author A") 187 | book2 = service.create_book("Same Title", "Author B") 188 | 189 | books = service.list_books(sort_by="title", ascending=True) 190 | # Should maintain original order for identical titles 191 | assert books[0]["author"] == "Author A" 192 | assert books[1]["author"] == "Author B" 193 | 194 | @pytest.mark.parametrize("sort_by", [ 195 | "invalid_field", 196 | "price", 197 | "year", 198 | "", 199 | None, 200 | 123, 201 | ]) 202 | def test_list_invalid_sort_field(self, sort_by): 203 | """Test listing with invalid sort fields.""" 204 | service = BookService() 205 | service.create_book("Test Book", "Test Author") 206 | 207 | with pytest.raises(ValueError): 208 | service.list_books(sort_by=sort_by) 209 | 210 | def test_list_combined_search_sort(self): 211 | """Test combining search and sort.""" 212 | service = BookService() 213 | service.create_book("Book A", "Author A") 214 | service.create_book("Book B", "Author B") 215 | service.create_book("Book C", "Author C") 216 | 217 | books = service.list_books(search="Book", sort_by="title", ascending=False) 218 | titles = [book["title"] for book in books] 219 | 220 | assert titles == ["Book C", "Book B", "Book A"] 221 | 222 | 223 | class TestBookServiceDelete: 224 | """Test delete_book method edge cases.""" 225 | 226 | def test_delete_existing_book(self): 227 | """Test deleting an existing book.""" 228 | service = BookService() 229 | book = service.create_book("Test Book", "Test Author") 230 | result = service.delete_book(book["id"]) 231 | 232 | assert result is True 233 | assert len(service.books) == 0 234 | assert service.get_book(book["id"]) is None 235 | 236 | @pytest.mark.parametrize("book_id", [ 237 | "999", 238 | "0", 239 | "-1", 240 | "", 241 | " ", 242 | None, 243 | 123, 244 | [], 245 | {}, 246 | ]) 247 | def test_delete_nonexistent_book(self, book_id): 248 | """Test deleting non-existent books.""" 249 | service = BookService() 250 | service.create_book("Test Book", "Test Author") 251 | result = service.delete_book(book_id) 252 | 253 | assert result is False 254 | assert len(service.books) == 1 255 | 256 | def test_delete_idempotency(self): 257 | """Test that deleting the same book twice is idempotent.""" 258 | service = BookService() 259 | book = service.create_book("Test Book", "Test Author") 260 | 261 | # First deletion 262 | result1 = service.delete_book(book["id"]) 263 | assert result1 is True 264 | assert len(service.books) == 0 265 | 266 | # Second deletion 267 | result2 = service.delete_book(book["id"]) 268 | assert result2 is False 269 | assert len(service.books) == 0 270 | 271 | def test_delete_specific_book(self): 272 | """Test that deleting one book doesn't affect others.""" 273 | service = BookService() 274 | book1 = service.create_book("Book 1", "Author 1") 275 | book2 = service.create_book("Book 2", "Author 2") 276 | 277 | service.delete_book(book1["id"]) 278 | 279 | assert len(service.books) == 1 280 | assert service.get_book(book2["id"]) == book2 281 | assert service.get_book(book1["id"]) is None 282 | 283 | 284 | class TestBookServiceIntegration: 285 | """Test integration scenarios and complex workflows.""" 286 | 287 | def test_full_crud_workflow(self): 288 | """Test complete CRUD workflow.""" 289 | service = BookService() 290 | 291 | # Create 292 | book = service.create_book("Test Book", "Test Author") 293 | assert book["id"] == "1" 294 | 295 | # Read 296 | retrieved = service.get_book(book["id"]) 297 | assert retrieved == book 298 | 299 | # List 300 | books = service.list_books() 301 | assert len(books) == 1 302 | assert book in books 303 | 304 | # Update (simulated by delete + create) 305 | service.delete_book(book["id"]) 306 | updated_book = service.create_book("Updated Book", "Updated Author") 307 | assert updated_book["id"] == "2" 308 | 309 | # Delete 310 | result = service.delete_book(updated_book["id"]) 311 | assert result is True 312 | assert len(service.books) == 0 313 | 314 | def test_large_dataset(self): 315 | """Test with a larger dataset.""" 316 | service = BookService() 317 | 318 | # Create many books 319 | books = [] 320 | for i in range(100): 321 | book = service.create_book(f"Book {i}", f"Author {i}") 322 | books.append(book) 323 | 324 | assert len(service.books) == 100 325 | 326 | # Test search performance 327 | search_results = service.list_books(search="Book 5") 328 | assert len(search_results) == 1 329 | 330 | # Test sort performance 331 | sorted_books = service.list_books(sort_by="title", ascending=True) 332 | assert len(sorted_books) == 100 333 | assert sorted_books[0]["title"] == "Book 0" 334 | assert sorted_books[-1]["title"] == "Book 99" 335 | 336 | # Test delete performance 337 | service.delete_book(books[50]["id"]) 338 | assert len(service.books) == 99 339 | 340 | def test_unicode_handling(self): 341 | """Test comprehensive Unicode handling.""" 342 | service = BookService() 343 | 344 | unicode_books = [ 345 | ("Café", "José María"), 346 | ("naïve", "François"), 347 | ("résumé", "André"), 348 | ("中文", "作者"), 349 | ("العربية", "كاتب"), 350 | ("русский", "автор"), 351 | ] 352 | 353 | for title, author in unicode_books: 354 | book = service.create_book(title, author) 355 | assert book["title"] == title 356 | assert book["author"] == author 357 | 358 | # Test search with Unicode 359 | results = service.list_books(search="café") 360 | assert len(results) == 1 361 | assert results[0]["title"] == "Café" 362 | 363 | # Test sort with Unicode 364 | sorted_books = service.list_books(sort_by="title", ascending=True) 365 | assert len(sorted_books) == len(unicode_books) 366 | 367 | def test_edge_case_whitespace(self): 368 | """Test various whitespace edge cases.""" 369 | service = BookService() 370 | 371 | # Test whitespace in search 372 | service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 373 | results = service.list_books(search=" gatsby ") 374 | assert len(results) == 1 375 | 376 | # Test empty search 377 | results = service.list_books(search="") 378 | assert len(results) == 1 379 | 380 | # Test whitespace-only search 381 | results = service.list_books(search=" ") 382 | assert len(results) == 0 383 | 384 | def test_id_generation_sequence(self): 385 | """Test ID generation sequence and uniqueness.""" 386 | service = BookService() 387 | 388 | # Create and delete books to test ID sequence 389 | book1 = service.create_book("Book 1", "Author 1") 390 | book2 = service.create_book("Book 2", "Author 2") 391 | book3 = service.create_book("Book 3", "Author 3") 392 | 393 | assert book1["id"] == "1" 394 | assert book2["id"] == "2" 395 | assert book3["id"] == "3" 396 | 397 | # Delete middle book 398 | service.delete_book(book2["id"]) 399 | 400 | # Create new book - should get next ID 401 | book4 = service.create_book("Book 4", "Author 4") 402 | assert book4["id"] == "4" 403 | 404 | # Verify all IDs are unique 405 | all_books = service.list_books() 406 | ids = [book["id"] for book in all_books] 407 | assert len(set(ids)) == len(ids) # All unique 408 | 409 | 410 | if __name__ == "__main__": 411 | pytest.main([__file__, "-v"]) 412 | -------------------------------------------------------------------------------- /02_11_CodeProject_refactor/endState/test_book_service.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from book_service import BookService 3 | 4 | 5 | class TestBookServiceCreate: 6 | """Test create_book method edge cases.""" 7 | 8 | def test_create_valid_books(self): 9 | """Test creating valid books.""" 10 | service = BookService() 11 | book1 = service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 12 | book2 = service.create_book("To Kill a Mockingbird", "Harper Lee") 13 | 14 | assert book1["title"] == "The Great Gatsby" 15 | assert book1["author"] == "F. Scott Fitzgerald" 16 | assert book1["id"] == "1" 17 | assert book2["id"] == "2" 18 | assert len(service.books) == 2 19 | 20 | @pytest.mark.parametrize("title,author", [ 21 | ("", "Author"), 22 | ("Title", ""), 23 | (" ", "Author"), 24 | ("Title", " "), 25 | ("", ""), 26 | (" ", " "), 27 | ]) 28 | def test_create_empty_strings(self, title, author): 29 | """Test creating books with empty or whitespace-only strings.""" 30 | service = BookService() 31 | with pytest.raises(ValueError, match="Title and author must be non-empty strings"): 32 | service.create_book(title, author) 33 | 34 | @pytest.mark.parametrize("title,author", [ 35 | (None, "Author"), 36 | ("Title", None), 37 | (123, "Author"), 38 | ("Title", 456), 39 | ([], "Author"), 40 | ("Title", {}), 41 | ]) 42 | def test_create_invalid_types(self, title, author): 43 | """Test creating books with invalid input types.""" 44 | service = BookService() 45 | with pytest.raises((TypeError, ValueError)): 46 | service.create_book(title, author) 47 | 48 | def test_create_whitespace_stripping(self): 49 | """Test that whitespace is properly stripped.""" 50 | service = BookService() 51 | book = service.create_book(" The Great Gatsby ", " F. Scott Fitzgerald ") 52 | 53 | assert book["title"] == "The Great Gatsby" 54 | assert book["author"] == "F. Scott Fitzgerald" 55 | 56 | def test_create_unicode_characters(self): 57 | """Test creating books with Unicode characters.""" 58 | service = BookService() 59 | book = service.create_book("Café", "José María") 60 | 61 | assert book["title"] == "Café" 62 | assert book["author"] == "José María" 63 | 64 | def test_create_duplicate_titles_authors(self): 65 | """Test creating books with duplicate titles and authors.""" 66 | service = BookService() 67 | book1 = service.create_book("The Book", "Author") 68 | book2 = service.create_book("The Book", "Author") 69 | 70 | assert book1["id"] == "1" 71 | assert book2["id"] == "2" 72 | assert len(service.books) == 2 73 | 74 | 75 | class TestBookServiceGet: 76 | """Test get_book method edge cases.""" 77 | 78 | def test_get_existing_book(self): 79 | """Test getting an existing book.""" 80 | service = BookService() 81 | book = service.create_book("Test Book", "Test Author") 82 | retrieved = service.get_book(book["id"]) 83 | 84 | assert retrieved == book 85 | 86 | @pytest.mark.parametrize("book_id", [ 87 | "999", 88 | "0", 89 | "-1", 90 | "", 91 | " ", 92 | None, 93 | 123, 94 | [], 95 | {}, 96 | ]) 97 | def test_get_nonexistent_book(self, book_id): 98 | """Test getting non-existent books.""" 99 | service = BookService() 100 | service.create_book("Test Book", "Test Author") 101 | result = service.get_book(book_id) 102 | 103 | assert result is None 104 | 105 | def test_get_after_deletion(self): 106 | """Test getting a book after it's been deleted.""" 107 | service = BookService() 108 | book = service.create_book("Test Book", "Test Author") 109 | service.delete_book(book["id"]) 110 | result = service.get_book(book["id"]) 111 | 112 | assert result is None 113 | 114 | 115 | class TestBookServiceList: 116 | """Test list_books method edge cases.""" 117 | 118 | def test_list_empty_service(self): 119 | """Test listing books from empty service.""" 120 | service = BookService() 121 | books = service.list_books() 122 | 123 | assert books == [] 124 | 125 | def test_list_all_books(self): 126 | """Test listing all books.""" 127 | service = BookService() 128 | book1 = service.create_book("Book A", "Author A") 129 | book2 = service.create_book("Book B", "Author B") 130 | books = service.list_books() 131 | 132 | assert len(books) == 2 133 | assert book1 in books 134 | assert book2 in books 135 | 136 | @pytest.mark.parametrize("search_term,expected_count", [ 137 | ("book", 2), 138 | ("BOOK", 2), 139 | ("Book A", 1), 140 | ("Author A", 1), 141 | ("nonexistent", 0), 142 | ("", 2), 143 | (" ", 0), 144 | ]) 145 | def test_list_search(self, search_term, expected_count): 146 | """Test searching books.""" 147 | service = BookService() 148 | service.create_book("Book A", "Author A") 149 | service.create_book("Book B", "Author B") 150 | books = service.list_books(search=search_term) 151 | 152 | assert len(books) == expected_count 153 | 154 | def test_list_search_unicode(self): 155 | """Test searching with Unicode characters.""" 156 | service = BookService() 157 | service.create_book("Café", "José") 158 | books = service.list_books(search="café") 159 | 160 | assert len(books) == 1 161 | assert books[0]["title"] == "Café" 162 | 163 | @pytest.mark.parametrize("sort_by,ascending,expected_order", [ 164 | ("id", True, ["1", "2", "3"]), 165 | ("id", False, ["3", "2", "1"]), 166 | ("title", True, ["A Book", "B Book", "C Book"]), 167 | ("title", False, ["C Book", "B Book", "A Book"]), 168 | ("author", True, ["Author A", "Author B", "Author C"]), 169 | ("author", False, ["Author C", "Author B", "Author A"]), 170 | ]) 171 | def test_list_sorting(self, sort_by, ascending, expected_order): 172 | """Test sorting books by different fields and directions.""" 173 | service = BookService() 174 | service.create_book("C Book", "Author C") 175 | service.create_book("A Book", "Author A") 176 | service.create_book("B Book", "Author B") 177 | 178 | books = service.list_books(sort_by=sort_by, ascending=ascending) 179 | actual_order = [book[sort_by] for book in books] 180 | 181 | assert actual_order == expected_order 182 | 183 | def test_list_sort_stability(self): 184 | """Test that sorting is stable for identical values.""" 185 | service = BookService() 186 | book1 = service.create_book("Same Title", "Author A") 187 | book2 = service.create_book("Same Title", "Author B") 188 | 189 | books = service.list_books(sort_by="title", ascending=True) 190 | # Should maintain original order for identical titles 191 | assert books[0]["author"] == "Author A" 192 | assert books[1]["author"] == "Author B" 193 | 194 | @pytest.mark.parametrize("sort_by", [ 195 | "invalid_field", 196 | "price", 197 | "year", 198 | "", 199 | None, 200 | 123, 201 | ]) 202 | def test_list_invalid_sort_field(self, sort_by): 203 | """Test listing with invalid sort fields.""" 204 | service = BookService() 205 | service.create_book("Test Book", "Test Author") 206 | 207 | with pytest.raises(ValueError): 208 | service.list_books(sort_by=sort_by) 209 | 210 | def test_list_combined_search_sort(self): 211 | """Test combining search and sort.""" 212 | service = BookService() 213 | service.create_book("Book A", "Author A") 214 | service.create_book("Book B", "Author B") 215 | service.create_book("Book C", "Author C") 216 | 217 | books = service.list_books(search="Book", sort_by="title", ascending=False) 218 | titles = [book["title"] for book in books] 219 | 220 | assert titles == ["Book C", "Book B", "Book A"] 221 | 222 | 223 | class TestBookServiceDelete: 224 | """Test delete_book method edge cases.""" 225 | 226 | def test_delete_existing_book(self): 227 | """Test deleting an existing book.""" 228 | service = BookService() 229 | book = service.create_book("Test Book", "Test Author") 230 | result = service.delete_book(book["id"]) 231 | 232 | assert result is True 233 | assert len(service.books) == 0 234 | assert service.get_book(book["id"]) is None 235 | 236 | @pytest.mark.parametrize("book_id", [ 237 | "999", 238 | "0", 239 | "-1", 240 | "", 241 | " ", 242 | None, 243 | 123, 244 | [], 245 | {}, 246 | ]) 247 | def test_delete_nonexistent_book(self, book_id): 248 | """Test deleting non-existent books.""" 249 | service = BookService() 250 | service.create_book("Test Book", "Test Author") 251 | result = service.delete_book(book_id) 252 | 253 | assert result is False 254 | assert len(service.books) == 1 255 | 256 | def test_delete_idempotency(self): 257 | """Test that deleting the same book twice is idempotent.""" 258 | service = BookService() 259 | book = service.create_book("Test Book", "Test Author") 260 | 261 | # First deletion 262 | result1 = service.delete_book(book["id"]) 263 | assert result1 is True 264 | assert len(service.books) == 0 265 | 266 | # Second deletion 267 | result2 = service.delete_book(book["id"]) 268 | assert result2 is False 269 | assert len(service.books) == 0 270 | 271 | def test_delete_specific_book(self): 272 | """Test that deleting one book doesn't affect others.""" 273 | service = BookService() 274 | book1 = service.create_book("Book 1", "Author 1") 275 | book2 = service.create_book("Book 2", "Author 2") 276 | 277 | service.delete_book(book1["id"]) 278 | 279 | assert len(service.books) == 1 280 | assert service.get_book(book2["id"]) == book2 281 | assert service.get_book(book1["id"]) is None 282 | 283 | 284 | class TestBookServiceIntegration: 285 | """Test integration scenarios and complex workflows.""" 286 | 287 | def test_full_crud_workflow(self): 288 | """Test complete CRUD workflow.""" 289 | service = BookService() 290 | 291 | # Create 292 | book = service.create_book("Test Book", "Test Author") 293 | assert book["id"] == "1" 294 | 295 | # Read 296 | retrieved = service.get_book(book["id"]) 297 | assert retrieved == book 298 | 299 | # List 300 | books = service.list_books() 301 | assert len(books) == 1 302 | assert book in books 303 | 304 | # Update (simulated by delete + create) 305 | service.delete_book(book["id"]) 306 | updated_book = service.create_book("Updated Book", "Updated Author") 307 | assert updated_book["id"] == "2" 308 | 309 | # Delete 310 | result = service.delete_book(updated_book["id"]) 311 | assert result is True 312 | assert len(service.books) == 0 313 | 314 | def test_large_dataset(self): 315 | """Test with a larger dataset.""" 316 | service = BookService() 317 | 318 | # Create many books 319 | books = [] 320 | for i in range(100): 321 | book = service.create_book(f"Book {i}", f"Author {i}") 322 | books.append(book) 323 | 324 | assert len(service.books) == 100 325 | 326 | # Test search performance 327 | search_results = service.list_books(search="Book 5") 328 | assert len(search_results) == 1 329 | 330 | # Test sort performance 331 | sorted_books = service.list_books(sort_by="title", ascending=True) 332 | assert len(sorted_books) == 100 333 | assert sorted_books[0]["title"] == "Book 0" 334 | assert sorted_books[-1]["title"] == "Book 99" 335 | 336 | # Test delete performance 337 | service.delete_book(books[50]["id"]) 338 | assert len(service.books) == 99 339 | 340 | def test_unicode_handling(self): 341 | """Test comprehensive Unicode handling.""" 342 | service = BookService() 343 | 344 | unicode_books = [ 345 | ("Café", "José María"), 346 | ("naïve", "François"), 347 | ("résumé", "André"), 348 | ("中文", "作者"), 349 | ("العربية", "كاتب"), 350 | ("русский", "автор"), 351 | ] 352 | 353 | for title, author in unicode_books: 354 | book = service.create_book(title, author) 355 | assert book["title"] == title 356 | assert book["author"] == author 357 | 358 | # Test search with Unicode 359 | results = service.list_books(search="café") 360 | assert len(results) == 1 361 | assert results[0]["title"] == "Café" 362 | 363 | # Test sort with Unicode 364 | sorted_books = service.list_books(sort_by="title", ascending=True) 365 | assert len(sorted_books) == len(unicode_books) 366 | 367 | def test_edge_case_whitespace(self): 368 | """Test various whitespace edge cases.""" 369 | service = BookService() 370 | 371 | # Test whitespace in search 372 | service.create_book("The Great Gatsby", "F. Scott Fitzgerald") 373 | results = service.list_books(search=" gatsby ") 374 | assert len(results) == 1 375 | 376 | # Test empty search 377 | results = service.list_books(search="") 378 | assert len(results) == 1 379 | 380 | # Test whitespace-only search 381 | results = service.list_books(search=" ") 382 | assert len(results) == 0 383 | 384 | def test_id_generation_sequence(self): 385 | """Test ID generation sequence and uniqueness.""" 386 | service = BookService() 387 | 388 | # Create and delete books to test ID sequence 389 | book1 = service.create_book("Book 1", "Author 1") 390 | book2 = service.create_book("Book 2", "Author 2") 391 | book3 = service.create_book("Book 3", "Author 3") 392 | 393 | assert book1["id"] == "1" 394 | assert book2["id"] == "2" 395 | assert book3["id"] == "3" 396 | 397 | # Delete middle book 398 | service.delete_book(book2["id"]) 399 | 400 | # Create new book - should get next ID 401 | book4 = service.create_book("Book 4", "Author 4") 402 | assert book4["id"] == "4" 403 | 404 | # Verify all IDs are unique 405 | all_books = service.list_books() 406 | ids = [book["id"] for book in all_books] 407 | assert len(set(ids)) == len(ids) # All unique 408 | 409 | 410 | if __name__ == "__main__": 411 | pytest.main([__file__, "-v"]) 412 | --------------------------------------------------------------------------------