feat: Introduce several new Python projects, unit testing fundamentals, and update project dependencies.

This commit is contained in:
blshaer 2026-03-11 10:41:17 +02:00
parent d1c6102700
commit 63dc2ff15a
11 changed files with 1404 additions and 9 deletions

View File

@ -0,0 +1,36 @@
"""
================================================================================
File: 01_test_basics.py
Topic: Introduction to Unit Testing with pytest
================================================================================
This file introduces unit testing, a professional practice used to ensure that
code behaves as expected. We use 'pytest', the industry standard for Python.
Key Concepts:
- Writing test functions (starting with test_)
- Assertions (checking if conditions are True)
- Running tests from the terminal
================================================================================
"""
import pytest
# A simple function we want to test
def add(a: int, b: int) -> int:
return a + b
def test_add_positive():
"""Test addition of positive numbers."""
assert add(2, 3) == 5
def test_add_negative():
"""Test addition of negative numbers."""
assert add(-1, -1) == -2
def test_add_zero():
"""Test addition with zero."""
assert add(5, 0) == 5
if __name__ == "__main__":
print("Run this file using: pytest 11_testing/01_test_basics.py")

View File

@ -10,9 +10,6 @@ A practical, example-driven Python repository designed to help you learn Python
Each file is self-contained and focuses on a single concept, making the repository suitable for structured learning, revision, or reference.
### 🌐 **Visit the Interactive Website**
Want to follow your progress? Visit the website at [pythonbyexample.page.gd](https://pythonbyexample.page.gd/)
> **If you like this project, please hit the ⭐ Star button and follow me on GitHub [@blshaer](https://github.com/blshaer)!**
---
@ -25,6 +22,7 @@ Want to follow your progress? Visit the website at [pythonbyexample.page.gd](htt
- [Prerequisites](#-prerequisites)
- [File Structure](#-file-structure)
- [Learning Path](#-learning-path)
- [Projects](#-projects)
---
@ -119,21 +117,50 @@ This repository contains a complete Python tutorial designed for both beginners
## Getting Started
Follow these steps to clone the repository and set it up in your local editor:
1. **Clone the repository**
Open your terminal and run:
```bash
git clone https://github.com/blshaer/python_review.git
cd python_review
git clone https://github.com/blshaer/python-by-example.git
cd python-by-example
```
2. **Start from basics**
2. **Set up a Virtual Environment (Recommended)**
It's best practice to keep your projects isolated. Run these commands:
```bash
python -m venv venv
# On Windows:
.\venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate
```
3. **Install Dependencies**
Install the necessary tools for testing and formatting:
```bash
pip install -r requirements.txt
```
4. **Open in your code editor**
We recommend [Visual Studio Code](https://code.visualstudio.com/). Open the project directly:
```bash
code .
```
5. **Explore and Run**
Navigate to any example, like `01_basics`, and run the file:
```bash
cd 01_basics
python 01_print.py
```
3. **Read the comments** — Each section is thoroughly explained
6. **Read the comments** — Detailed explanations are provided inline for every concept!
4. **Experiment** — Modify the code and observe the results
7. **Try the Tests** — Go to the new testing module to see how professional code is verified:
```bash
pytest 11_testing/01_test_basics.py
```
---
@ -219,6 +246,7 @@ Key Concepts:
<td>
10. [Best Practices](./10_best_practices/)
11. [Testing](./11_testing/)
</td>
</tr>
@ -229,11 +257,26 @@ Key Concepts:
Beginner → 01_basics → 02_control_flow → 03_loops
Intermediate → 04_data_structures → 05_functions → 06_modules
Advanced → 07_error_handling → 08_oop → 09_advanced
Professional → 10_best_practices
Professional → 10_best_practices → 11_testing
```
---
## Projects
Put your skills to the test! Each project includes a **challenge description** (`README.md`) so you can try building it yourself before looking at the solution.
| # | Project | Difficulty | Concepts Applied |
|:--|:--------|:-----------|:-----------------|
| 01 | [**Number Guessing Game**](./projects/01_number_guessing_game/) | 🟢 Beginner | Loops, Conditionals, Random module |
| 02 | [**Expense Tracker**](./projects/02_expense_tracker/) | 🟡 Intermediate | Dicts, File I/O (JSON), Functions |
| 03 | [**Library Management System**](./projects/03_library_management/) | 🟠 Advanced | OOP, Inheritance, Custom Exceptions |
| 04 | [**Real-time Weather CLI**](./projects/04_weather_cli/) | 🚀 Professional | API Requests, Decorators, Type Hints |
> 💡 **Tip:** Read the project `README.md` first and try to build it on your own before looking at `solution.py`!
---
## Progress Tracker
Use this checklist to track your learning progress:
@ -248,6 +291,13 @@ Use this checklist to track your learning progress:
- [ ] **08. OOP** — Classes, Init, Inheritance, Polymorphism
- [ ] **09. Advanced Python** — Comprehensions, Generators, Decorators
- [ ] **10. Best Practices** — PEP8, Type Hints, Virtual Environments
- [ ] **11. Testing** — Unit Tests, Pytest, Assertions
### 🏗️ Projects
- [ ] **Project 01** — Number Guessing Game (Beginner)
- [ ] **Project 02** — Expense Tracker (Intermediate)
- [ ] **Project 03** — Library Management System (Advanced)
- [ ] **Project 04** — Real-time Weather CLI (Professional)
---

View File

@ -0,0 +1,58 @@
# 🎯 Project 01: Number Guessing Game
## Difficulty: 🟢 Beginner
## Description
Build a command-line number guessing game where the computer picks a random number
and the player tries to guess it. The game should provide hints like "Too high!" or
"Too low!" after each guess.
## Requirements
1. Generate a random number between 1 and 100
2. Ask the user for guesses until they get the correct number
3. After each guess, tell the user if their guess is too high, too low, or correct
4. Track the number of attempts and display it at the end
5. Handle invalid input gracefully (non-numeric input)
6. Ask the player if they want to play again after winning
## Concepts You'll Practice
- `while` loops
- `if/elif/else` conditionals
- `random` module
- `try/except` error handling
- `input()` function
- f-strings
## Example Output
```
🎯 Welcome to the Number Guessing Game!
I'm thinking of a number between 1 and 100...
Enter your guess: 50
📉 Too low! Try again.
Enter your guess: 75
📈 Too high! Try again.
Enter your guess: 63
🎉 Congratulations! You guessed it in 3 attempts!
Play again? (yes/no): no
Thanks for playing! Goodbye! 👋
```
## How to Run
```bash
cd projects/01_number_guessing_game
python solution.py
```
## Bonus Challenges
- [ ] Add difficulty levels (Easy: 1-50, Medium: 1-100, Hard: 1-500)
- [ ] Add a scoreboard that tracks best scores across games
- [ ] Set a maximum number of attempts based on difficulty

View File

@ -0,0 +1,159 @@
"""
================================================================================
Project 01: Number Guessing Game
Difficulty: 🟢 Beginner
================================================================================
A command-line number guessing game where the computer picks a random number
and the player tries to guess it with hints provided after each attempt.
Concepts Used:
- while loops
- if/elif/else conditionals
- random module
- try/except error handling
- input() function
- f-strings
================================================================================
"""
import random
# -----------------------------------------------------------------------------
# Game Configuration
# -----------------------------------------------------------------------------
DIFFICULTIES = {
"easy": {"min": 1, "max": 50, "attempts": 10},
"medium": {"min": 1, "max": 100, "attempts": 7},
"hard": {"min": 1, "max": 500, "attempts": 10},
}
# -----------------------------------------------------------------------------
# Helper Functions
# -----------------------------------------------------------------------------
def display_welcome():
"""Display the welcome banner."""
print("""
🎯 NUMBER GUESSING GAME 🎯
""")
def choose_difficulty() -> dict:
"""Let the player choose a difficulty level."""
print("Choose your difficulty:")
print(" [1] 🟢 Easy (1-50, 10 attempts)")
print(" [2] 🟡 Medium (1-100, 7 attempts)")
print(" [3] 🔴 Hard (1-500, 10 attempts)")
while True:
choice = input("\nYour choice (1/2/3): ").strip()
if choice == "1":
return DIFFICULTIES["easy"]
elif choice == "2":
return DIFFICULTIES["medium"]
elif choice == "3":
return DIFFICULTIES["hard"]
else:
print("❌ Invalid choice. Please enter 1, 2, or 3.")
def get_guess(min_val: int, max_val: int) -> int:
"""Get a valid integer guess from the player."""
while True:
try:
guess = int(input(f"\nEnter your guess ({min_val}-{max_val}): "))
if min_val <= guess <= max_val:
return guess
else:
print(f"⚠️ Please enter a number between {min_val} and {max_val}.")
except ValueError:
print("⚠️ That's not a valid number. Try again!")
def play_round():
"""Play a single round of the guessing game."""
# Setup
config = choose_difficulty()
min_val = config["min"]
max_val = config["max"]
max_attempts = config["attempts"]
secret = random.randint(min_val, max_val)
attempts = 0
print(f"\n🤔 I'm thinking of a number between {min_val} and {max_val}...")
print(f" You have {max_attempts} attempts. Good luck!\n")
# Game loop
while attempts < max_attempts:
guess = get_guess(min_val, max_val)
attempts += 1
remaining = max_attempts - attempts
if guess < secret:
print(f"📉 Too low! ({remaining} attempts remaining)")
elif guess > secret:
print(f"📈 Too high! ({remaining} attempts remaining)")
else:
print(f"\n🎉 Congratulations! You guessed it in {attempts} attempt(s)!")
if attempts <= 3:
print("🏆 Amazing! You're a natural!")
elif attempts <= 5:
print("👏 Well done!")
else:
print("😅 That was close!")
return True
# Out of attempts
print(f"\n💀 Game over! The number was {secret}.")
return False
def play_again() -> bool:
"""Ask the player if they want to play again."""
while True:
answer = input("\n🔄 Play again? (yes/no): ").strip().lower()
if answer in ("yes", "y"):
print()
return True
elif answer in ("no", "n"):
return False
else:
print("Please enter 'yes' or 'no'.")
# -----------------------------------------------------------------------------
# Main Game Loop
# -----------------------------------------------------------------------------
def main():
"""Main entry point for the game."""
display_welcome()
wins = 0
total = 0
while True:
total += 1
if play_round():
wins += 1
print(f"\n📊 Score: {wins} wins out of {total} games")
if not play_again():
break
print(f"\n{'='*40}")
print(f" Final Score: {wins}/{total} games won")
print(f" Thanks for playing! Goodbye! 👋")
print(f"{'='*40}\n")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,68 @@
# 💰 Project 02: Expense Tracker
## Difficulty: 🟡 Intermediate
## Description
Build a command-line expense tracker that allows users to add, view, and analyze
their expenses. Data is saved to a JSON file so expenses persist between sessions.
## Requirements
1. Add new expenses with a description, amount, and category
2. View all expenses in a formatted table
3. Filter expenses by category
4. Show a summary with total spending and spending per category
5. Save and load expenses from a JSON file
6. Delete an expense by its ID
## Concepts You'll Practice
- Dictionaries and Lists
- File I/O (JSON)
- Functions with multiple parameters
- String formatting
- `datetime` module
- Data filtering and aggregation
## Example Output
```
💰 EXPENSE TRACKER
==================
1. Add Expense
2. View All Expenses
3. View by Category
4. Summary
5. Delete Expense
6. Exit
Choose an option: 1
Enter description: Coffee
Enter amount: 4.50
Enter category (food/transport/entertainment/bills/other): food
✅ Expense added successfully!
Choose an option: 4
📊 EXPENSE SUMMARY
Total Expenses: $127.50
🍔 Food: $45.00 (35.3%)
🚗 Transport: $32.50 (25.5%)
🎬 Entertainment: $25.00 (19.6%)
📄 Bills: $25.00 (19.6%)
```
## How to Run
```bash
cd projects/02_expense_tracker
python solution.py
```
## Bonus Challenges
- [ ] Add monthly budget limits with warnings
- [ ] Export expenses to CSV
- [ ] Add date range filtering

View File

@ -0,0 +1,244 @@
"""
================================================================================
Project 02: Expense Tracker
Difficulty: 🟡 Intermediate
================================================================================
A command-line expense tracker that lets users add, view, filter, and analyze
their expenses. Data is persisted to a JSON file between sessions.
Concepts Used:
- Dictionaries and Lists
- File I/O (JSON)
- Functions with parameters
- String formatting
- datetime module
- Data filtering and aggregation
================================================================================
"""
import json
import os
from datetime import datetime
# -----------------------------------------------------------------------------
# Constants
# -----------------------------------------------------------------------------
DATA_FILE = os.path.join(os.path.dirname(__file__), "expenses.json")
CATEGORIES = {
"food": "🍔",
"transport": "🚗",
"entertainment": "🎬",
"bills": "📄",
"other": "📦",
}
# -----------------------------------------------------------------------------
# Data Management
# -----------------------------------------------------------------------------
def load_expenses() -> list:
"""Load expenses from the JSON file."""
if os.path.exists(DATA_FILE):
with open(DATA_FILE, "r") as f:
return json.load(f)
return []
def save_expenses(expenses: list) -> None:
"""Save expenses to the JSON file."""
with open(DATA_FILE, "w") as f:
json.dump(expenses, f, indent=2)
def generate_id(expenses: list) -> int:
"""Generate a unique ID for a new expense."""
if not expenses:
return 1
return max(exp["id"] for exp in expenses) + 1
# -----------------------------------------------------------------------------
# Core Features
# -----------------------------------------------------------------------------
def add_expense(expenses: list) -> None:
"""Add a new expense."""
description = input(" Enter description: ").strip()
if not description:
print(" ❌ Description cannot be empty.")
return
try:
amount = float(input(" Enter amount: $"))
if amount <= 0:
print(" ❌ Amount must be positive.")
return
except ValueError:
print(" ❌ Invalid amount.")
return
print(f" Categories: {', '.join(CATEGORIES.keys())}")
category = input(" Enter category: ").strip().lower()
if category not in CATEGORIES:
print(f" ⚠️ Unknown category. Defaulting to 'other'.")
category = "other"
expense = {
"id": generate_id(expenses),
"description": description,
"amount": amount,
"category": category,
"date": datetime.now().strftime("%Y-%m-%d %H:%M"),
}
expenses.append(expense)
save_expenses(expenses)
print(f" ✅ Expense added: {description} — ${amount:.2f} [{category}]")
def view_expenses(expenses: list, filter_category: str = None) -> None:
"""Display expenses in a formatted table."""
filtered = expenses
if filter_category:
filtered = [e for e in expenses if e["category"] == filter_category]
if not filtered:
print(" 📭 No expenses found.")
return
# Header
print(f"\n {'ID':<4} {'Date':<18} {'Category':<15} {'Description':<20} {'Amount':>10}")
print(f" {''*4} {''*18} {''*15} {''*20} {''*10}")
# Rows
for exp in filtered:
icon = CATEGORIES.get(exp["category"], "📦")
print(
f" {exp['id']:<4} "
f"{exp['date']:<18} "
f"{icon} {exp['category']:<12} "
f"{exp['description']:<20} "
f"${exp['amount']:>8.2f}"
)
total = sum(e["amount"] for e in filtered)
print(f"\n {'Total:':<60} ${total:>8.2f}")
def view_by_category(expenses: list) -> None:
"""Filter and view expenses by category."""
print(f" Categories: {', '.join(CATEGORIES.keys())}")
category = input(" Enter category to filter: ").strip().lower()
if category not in CATEGORIES:
print(" ❌ Invalid category.")
return
view_expenses(expenses, filter_category=category)
def show_summary(expenses: list) -> None:
"""Display spending summary with category breakdown."""
if not expenses:
print(" 📭 No expenses to summarize.")
return
total = sum(e["amount"] for e in expenses)
# Group by category
by_category = {}
for exp in expenses:
cat = exp["category"]
by_category[cat] = by_category.get(cat, 0) + exp["amount"]
print(f"\n 📊 EXPENSE SUMMARY")
print(f" {'='*40}")
print(f" Total Expenses: ${total:,.2f}")
print(f" Total Items: {len(expenses)}")
print(f" {''*40}")
# Sort categories by amount (descending)
sorted_cats = sorted(by_category.items(), key=lambda x: x[1], reverse=True)
for cat, amount in sorted_cats:
icon = CATEGORIES.get(cat, "📦")
percentage = (amount / total) * 100
bar = "" * int(percentage / 5)
print(f" {icon} {cat:<15} ${amount:>8.2f} ({percentage:>5.1f}%) {bar}")
def delete_expense(expenses: list) -> None:
"""Delete an expense by its ID."""
view_expenses(expenses)
if not expenses:
return
try:
exp_id = int(input("\n Enter expense ID to delete: "))
except ValueError:
print(" ❌ Invalid ID.")
return
for i, exp in enumerate(expenses):
if exp["id"] == exp_id:
removed = expenses.pop(i)
save_expenses(expenses)
print(f" 🗑️ Deleted: {removed['description']} — ${removed['amount']:.2f}")
return
print(f" ❌ Expense with ID {exp_id} not found.")
# -----------------------------------------------------------------------------
# Main Menu
# -----------------------------------------------------------------------------
def display_menu():
"""Display the main menu."""
print("""
💰 EXPENSE TRACKER 💰
1. Add Expense
2. View All Expenses
3. View by Category
4. Summary
5. Delete Expense
6. Exit
""")
def main():
"""Main entry point."""
expenses = load_expenses()
while True:
display_menu()
choice = input(" Choose an option (1-6): ").strip()
if choice == "1":
add_expense(expenses)
elif choice == "2":
view_expenses(expenses)
elif choice == "3":
view_by_category(expenses)
elif choice == "4":
show_summary(expenses)
elif choice == "5":
delete_expense(expenses)
elif choice == "6":
print("\n 👋 Goodbye! Keep tracking your expenses!")
break
else:
print(" ❌ Invalid option. Please choose 1-6.")
input("\n Press Enter to continue...")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,65 @@
# 📚 Project 03: Library Management System
## Difficulty: 🟠 Advanced
## Description
Build an object-oriented Library Management System where users can manage books,
members, and borrowing/returning of books. This project emphasizes classes,
inheritance, encapsulation, and error handling.
## Requirements
1. Create a `Book` class with title, author, ISBN, and availability status
2. Create a `Member` class with name, member ID, and borrowed books list
3. Create a `Library` class that manages books and members
4. Allow adding/removing books from the library catalog
5. Allow registering new members
6. Implement borrow and return functionality with proper validation
7. Handle edge cases: book already borrowed, member limit reached, book not found
8. Display library catalog and member details
## Concepts You'll Practice
- Classes and Objects
- Inheritance and Encapsulation
- Properties and Methods
- Custom Exceptions
- `__str__` and `__repr__` magic methods
- Composition (Library has Books and Members)
## Example Output
```
📚 LIBRARY MANAGEMENT SYSTEM
=============================
1. View Catalog
2. Add Book
3. Register Member
4. Borrow Book
5. Return Book
6. View Member Info
7. Exit
Choose: 4
Enter Member ID: M001
Enter Book ISBN: 978-0-13-110362-7
✅ "The C Programming Language" has been borrowed by Alice.
Due date: 2025-02-15
Books borrowed: 1/3
```
## How to Run
```bash
cd projects/03_library_management
python solution.py
```
## Bonus Challenges
- [ ] Add due dates and overdue book tracking
- [ ] Save library data to JSON for persistence
- [ ] Add search functionality (by title, author, or ISBN)

View File

@ -0,0 +1,389 @@
"""
================================================================================
Project 03: Library Management System
Difficulty: 🟠 Advanced
================================================================================
An object-oriented Library Management System for managing books, members,
and borrow/return operations with proper validation and error handling.
Concepts Used:
- Classes and Objects
- Inheritance and Encapsulation
- Properties and Methods
- Custom Exceptions
- __str__ and __repr__ magic methods
- Composition (Library has Books and Members)
================================================================================
"""
from datetime import datetime, timedelta
# -----------------------------------------------------------------------------
# Custom Exceptions
# -----------------------------------------------------------------------------
class LibraryError(Exception):
"""Base exception for library errors."""
pass
class BookNotFoundError(LibraryError):
"""Raised when a book is not found."""
pass
class BookNotAvailableError(LibraryError):
"""Raised when a book is already borrowed."""
pass
class MemberNotFoundError(LibraryError):
"""Raised when a member is not found."""
pass
class BorrowLimitError(LibraryError):
"""Raised when a member has reached their borrow limit."""
pass
# -----------------------------------------------------------------------------
# Book Class
# -----------------------------------------------------------------------------
class Book:
"""Represents a book in the library."""
def __init__(self, title: str, author: str, isbn: str):
self._title = title
self._author = author
self._isbn = isbn
self._is_available = True
self._borrowed_by = None
self._due_date = None
@property
def title(self) -> str:
return self._title
@property
def author(self) -> str:
return self._author
@property
def isbn(self) -> str:
return self._isbn
@property
def is_available(self) -> bool:
return self._is_available
@property
def due_date(self):
return self._due_date
def borrow(self, member_id: str, days: int = 14) -> None:
"""Mark the book as borrowed."""
if not self._is_available:
raise BookNotAvailableError(
f'"{self._title}" is already borrowed.'
)
self._is_available = False
self._borrowed_by = member_id
self._due_date = datetime.now() + timedelta(days=days)
def return_book(self) -> None:
"""Mark the book as returned."""
self._is_available = True
self._borrowed_by = None
self._due_date = None
def __str__(self) -> str:
status = "✅ Available" if self._is_available else f"❌ Borrowed (due: {self._due_date.strftime('%Y-%m-%d')})"
return f'"{self._title}" by {self._author} [ISBN: {self._isbn}] — {status}'
def __repr__(self) -> str:
return f"Book(title='{self._title}', author='{self._author}', isbn='{self._isbn}')"
# -----------------------------------------------------------------------------
# Member Class
# -----------------------------------------------------------------------------
class Member:
"""Represents a library member."""
MAX_BORROW_LIMIT = 3
def __init__(self, name: str, member_id: str):
self._name = name
self._member_id = member_id
self._borrowed_books: list[str] = [] # List of ISBNs
@property
def name(self) -> str:
return self._name
@property
def member_id(self) -> str:
return self._member_id
@property
def borrowed_books(self) -> list:
return self._borrowed_books.copy()
@property
def borrow_count(self) -> int:
return len(self._borrowed_books)
def can_borrow(self) -> bool:
"""Check if the member can borrow more books."""
return self.borrow_count < self.MAX_BORROW_LIMIT
def add_book(self, isbn: str) -> None:
"""Add a book ISBN to the borrowed list."""
if not self.can_borrow():
raise BorrowLimitError(
f"{self._name} has reached the borrow limit ({self.MAX_BORROW_LIMIT} books)."
)
self._borrowed_books.append(isbn)
def remove_book(self, isbn: str) -> None:
"""Remove a book ISBN from the borrowed list."""
if isbn in self._borrowed_books:
self._borrowed_books.remove(isbn)
def __str__(self) -> str:
return (
f"👤 {self._name} (ID: {self._member_id}) "
f"— Books borrowed: {self.borrow_count}/{self.MAX_BORROW_LIMIT}"
)
def __repr__(self) -> str:
return f"Member(name='{self._name}', member_id='{self._member_id}')"
# -----------------------------------------------------------------------------
# Library Class (Composition)
# -----------------------------------------------------------------------------
class Library:
"""Manages books and members."""
def __init__(self, name: str):
self.name = name
self._books: dict[str, Book] = {} # ISBN -> Book
self._members: dict[str, Member] = {} # member_id -> Member
# --- Book Management ---
def add_book(self, title: str, author: str, isbn: str) -> Book:
"""Add a new book to the catalog."""
if isbn in self._books:
print(f" ⚠️ Book with ISBN {isbn} already exists.")
return self._books[isbn]
book = Book(title, author, isbn)
self._books[isbn] = book
return book
def find_book(self, isbn: str) -> Book:
"""Find a book by ISBN."""
if isbn not in self._books:
raise BookNotFoundError(f"No book found with ISBN: {isbn}")
return self._books[isbn]
def search_books(self, query: str) -> list[Book]:
"""Search books by title or author."""
query_lower = query.lower()
return [
book for book in self._books.values()
if query_lower in book.title.lower() or query_lower in book.author.lower()
]
# --- Member Management ---
def register_member(self, name: str, member_id: str) -> Member:
"""Register a new library member."""
if member_id in self._members:
print(f" ⚠️ Member ID {member_id} already exists.")
return self._members[member_id]
member = Member(name, member_id)
self._members[member_id] = member
return member
def find_member(self, member_id: str) -> Member:
"""Find a member by ID."""
if member_id not in self._members:
raise MemberNotFoundError(f"No member found with ID: {member_id}")
return self._members[member_id]
# --- Borrow / Return ---
def borrow_book(self, member_id: str, isbn: str) -> None:
"""Allow a member to borrow a book."""
member = self.find_member(member_id)
book = self.find_book(isbn)
if not member.can_borrow():
raise BorrowLimitError(
f"{member.name} has reached the borrow limit."
)
book.borrow(member_id)
member.add_book(isbn)
print(f'"{book.title}" has been borrowed by {member.name}.')
print(f" Due date: {book.due_date.strftime('%Y-%m-%d')}")
print(f" Books borrowed: {member.borrow_count}/{Member.MAX_BORROW_LIMIT}")
def return_book(self, member_id: str, isbn: str) -> None:
"""Allow a member to return a book."""
member = self.find_member(member_id)
book = self.find_book(isbn)
book.return_book()
member.remove_book(isbn)
print(f'"{book.title}" has been returned by {member.name}.')
# --- Display ---
def display_catalog(self) -> None:
"""Display the full library catalog."""
if not self._books:
print(" 📭 No books in the catalog.")
return
print(f"\n 📚 {self.name} — Catalog ({len(self._books)} books)")
print(f" {'='*65}")
for book in self._books.values():
print(f" {book}")
def display_members(self) -> None:
"""Display all members."""
if not self._members:
print(" 📭 No registered members.")
return
print(f"\n 👥 Registered Members ({len(self._members)})")
print(f" {'='*50}")
for member in self._members.values():
print(f" {member}")
# -----------------------------------------------------------------------------
# Interactive Menu
# -----------------------------------------------------------------------------
def display_menu():
"""Display the main menu."""
print("""
📚 LIBRARY MANAGEMENT SYSTEM 📚
1. View Catalog
2. Add Book
3. Search Books
4. Register Member
5. Borrow Book
6. Return Book
7. View Member Info
8. Exit
""")
def main():
"""Main entry point."""
library = Library("City Public Library")
# Pre-load some sample data
library.add_book("The C Programming Language", "Kernighan & Ritchie", "978-0-13-110362-7")
library.add_book("Clean Code", "Robert C. Martin", "978-0-13-235088-4")
library.add_book("Python Crash Course", "Eric Matthes", "978-1-59327-603-4")
library.add_book("Design Patterns", "Gang of Four", "978-0-20-163361-0")
library.add_book("The Pragmatic Programmer", "Hunt & Thomas", "978-0-20-161622-4")
library.register_member("Alice", "M001")
library.register_member("Bob", "M002")
while True:
display_menu()
choice = input(" Choose an option (1-8): ").strip()
try:
if choice == "1":
library.display_catalog()
elif choice == "2":
title = input(" Enter title: ").strip()
author = input(" Enter author: ").strip()
isbn = input(" Enter ISBN: ").strip()
if title and author and isbn:
library.add_book(title, author, isbn)
print(f'"{title}" added to the catalog.')
else:
print(" ❌ All fields are required.")
elif choice == "3":
query = input(" Search (title or author): ").strip()
results = library.search_books(query)
if results:
print(f"\n 🔍 Found {len(results)} result(s):")
for book in results:
print(f" {book}")
else:
print(" 📭 No matching books found.")
elif choice == "4":
name = input(" Enter member name: ").strip()
member_id = input(" Enter member ID: ").strip()
if name and member_id:
library.register_member(name, member_id)
print(f"{name} registered as member {member_id}.")
else:
print(" ❌ All fields are required.")
elif choice == "5":
library.display_members()
member_id = input("\n Enter Member ID: ").strip()
library.display_catalog()
isbn = input("\n Enter Book ISBN: ").strip()
library.borrow_book(member_id, isbn)
elif choice == "6":
member_id = input(" Enter Member ID: ").strip()
isbn = input(" Enter Book ISBN: ").strip()
library.return_book(member_id, isbn)
elif choice == "7":
library.display_members()
member_id = input("\n Enter Member ID for details: ").strip()
member = library.find_member(member_id)
print(f"\n {member}")
if member.borrowed_books:
print(" Borrowed books:")
for isbn in member.borrowed_books:
book = library.find_book(isbn)
print(f" 📖 {book}")
else:
print(" No books currently borrowed.")
elif choice == "8":
print("\n 👋 Thank you for using the Library Management System!")
break
else:
print(" ❌ Invalid option. Please choose 1-8.")
except LibraryError as e:
print(f"{e}")
input("\n Press Enter to continue...")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,62 @@
# 🌤️ Project 04: Real-time Weather CLI
## Difficulty: 🚀 Professional
## Description
Build a professional command-line weather application that fetches real-time weather
data from the Open-Meteo API (free, no API key needed!) and displays it beautifully
in the terminal. This project combines API consumption, decorators, type hinting,
error handling, and testing.
## Requirements
1. Fetch current weather data for any city using the Open-Meteo geocoding + weather API
2. Display temperature, humidity, wind speed, and weather condition
3. Support multiple cities in a single query
4. Cache API responses using a decorator to avoid repeated requests
5. Use type hints throughout the codebase
6. Handle network errors and invalid city names gracefully
7. Display weather data in a formatted, visually appealing way
## Concepts You'll Practice
- HTTP Requests (`urllib` — no external dependencies!)
- Decorators (caching)
- Type Hinting
- Error Handling (network, API, data)
- JSON parsing
- String formatting and CLI design
## Example Output
```
🌤️ WEATHER CLI
===============
Enter city name: Gaza
╔══════════════════════════════════════╗
║ 📍 Gaza, Palestinian Territory ║
╠══════════════════════════════════════╣
║ 🌡️ Temperature: 28.3°C ║
║ 💧 Humidity: 65% ║
║ 💨 Wind Speed: 12.5 km/h ║
║ ☀️ Condition: Clear sky ║
╚══════════════════════════════════════╝
```
## How to Run
```bash
cd projects/04_weather_cli
python solution.py
```
> **Note:** This project uses the free [Open-Meteo API](https://open-meteo.com/) —
> no API key or sign-up required!
## Bonus Challenges
- [ ] Add 5-day forecast display
- [ ] Add temperature unit conversion (°C / °F)
- [ ] Save favorite cities to a config file

View File

@ -0,0 +1,259 @@
"""
================================================================================
Project 04: Real-time Weather CLI
Difficulty: 🚀 Professional
================================================================================
A professional command-line weather application that fetches real-time data
from the Open-Meteo API (free, no API key needed) and displays it beautifully.
Concepts Used:
- HTTP Requests (urllib no external dependencies)
- Decorators (caching)
- Type Hinting
- Error Handling (network, API, data)
- JSON parsing
- String formatting and CLI design
================================================================================
"""
import json
import urllib.request
import urllib.parse
from typing import Optional
from functools import wraps
# -----------------------------------------------------------------------------
# Constants
# -----------------------------------------------------------------------------
GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search"
WEATHER_URL = "https://api.open-meteo.com/v1/forecast"
# WMO Weather interpretation codes
WMO_CODES: dict[int, tuple[str, str]] = {
0: ("☀️", "Clear sky"),
1: ("🌤️", "Mainly clear"),
2: ("", "Partly cloudy"),
3: ("☁️", "Overcast"),
45: ("🌫️", "Fog"),
48: ("🌫️", "Depositing rime fog"),
51: ("🌦️", "Light drizzle"),
53: ("🌦️", "Moderate drizzle"),
55: ("🌧️", "Dense drizzle"),
61: ("🌧️", "Slight rain"),
63: ("🌧️", "Moderate rain"),
65: ("🌧️", "Heavy rain"),
71: ("🌨️", "Slight snow"),
73: ("🌨️", "Moderate snow"),
75: ("❄️", "Heavy snow"),
80: ("🌧️", "Slight rain showers"),
81: ("🌧️", "Moderate rain showers"),
82: ("⛈️", "Violent rain showers"),
95: ("⛈️", "Thunderstorm"),
96: ("⛈️", "Thunderstorm with slight hail"),
99: ("⛈️", "Thunderstorm with heavy hail"),
}
# -----------------------------------------------------------------------------
# Caching Decorator
# -----------------------------------------------------------------------------
def cache_response(func):
"""
Decorator that caches function responses based on arguments.
This avoids making redundant API calls for the same city
within a single session.
"""
_cache: dict[str, any] = {}
@wraps(func)
def wrapper(*args, **kwargs):
# Create a cache key from the arguments
key = str(args) + str(sorted(kwargs.items()))
if key not in _cache:
_cache[key] = func(*args, **kwargs)
else:
print(" ⚡ Using cached data...")
return _cache[key]
# Expose cache for testing / inspection
wrapper.cache = _cache
return wrapper
# -----------------------------------------------------------------------------
# API Functions
# -----------------------------------------------------------------------------
def fetch_json(url: str) -> dict:
"""Fetch JSON data from a URL using urllib (no external deps)."""
try:
req = urllib.request.Request(url, headers={"User-Agent": "PythonByExample/1.0"})
with urllib.request.urlopen(req, timeout=10) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.URLError as e:
raise ConnectionError(f"Network error: {e}")
except json.JSONDecodeError:
raise ValueError("Failed to parse API response.")
@cache_response
def geocode_city(city_name: str) -> Optional[dict]:
"""
Look up a city's coordinates using the Open-Meteo Geocoding API.
Returns dict with name, country, latitude, longitude or None if not found.
"""
params = urllib.parse.urlencode({"name": city_name, "count": 1, "language": "en"})
url = f"{GEOCODING_URL}?{params}"
data = fetch_json(url)
if "results" not in data or not data["results"]:
return None
result = data["results"][0]
return {
"name": result.get("name", city_name),
"country": result.get("country", "Unknown"),
"latitude": result["latitude"],
"longitude": result["longitude"],
}
@cache_response
def get_weather(latitude: float, longitude: float) -> dict:
"""
Fetch current weather data for given coordinates.
Returns dict with temperature, humidity, wind_speed, and weather_code.
"""
params = urllib.parse.urlencode({
"latitude": latitude,
"longitude": longitude,
"current_weather": "true",
"hourly": "relativehumidity_2m",
"timezone": "auto",
})
url = f"{WEATHER_URL}?{params}"
data = fetch_json(url)
current = data.get("current_weather", {})
hourly = data.get("hourly", {})
# Get current hour's humidity
humidity = None
if "relativehumidity_2m" in hourly and hourly["relativehumidity_2m"]:
from datetime import datetime
try:
current_time = datetime.fromisoformat(current.get("time", ""))
times = hourly.get("time", [])
for i, t in enumerate(times):
if datetime.fromisoformat(t) >= current_time:
humidity = hourly["relativehumidity_2m"][i]
break
except (ValueError, IndexError):
humidity = hourly["relativehumidity_2m"][0] if hourly["relativehumidity_2m"] else None
return {
"temperature": current.get("temperature", 0),
"wind_speed": current.get("windspeed", 0),
"weather_code": current.get("weathercode", 0),
"humidity": humidity,
}
# -----------------------------------------------------------------------------
# Display Functions
# -----------------------------------------------------------------------------
def get_weather_description(code: int) -> tuple[str, str]:
"""Get weather icon and description from WMO code."""
return WMO_CODES.get(code, ("", "Unknown"))
def display_weather(city_info: dict, weather: dict) -> None:
"""Display weather data in a beautiful formatted box."""
icon, condition = get_weather_description(weather["weather_code"])
location = f"{city_info['name']}, {city_info['country']}"
# Calculate box width based on content
box_width = max(38, len(location) + 8)
inner = box_width - 4
print()
print(f"{'' * (box_width - 2)}")
print(f" ║ 📍 {location:<{inner - 4}}")
print(f"{'' * (box_width - 2)}")
print(f" ║ 🌡️ Temperature: {weather['temperature']:>6.1f}°C{' ' * (inner - 26)}")
if weather["humidity"] is not None:
print(f" ║ 💧 Humidity: {weather['humidity']:>5}%{' ' * (inner - 25)}")
else:
print(f" ║ 💧 Humidity: N/A{' ' * (inner - 23)}")
print(f" ║ 💨 Wind Speed: {weather['wind_speed']:>6.1f} km/h{' ' * (inner - 29)}")
print(f"{icon} Condition: {condition:<{inner - 17}}")
print(f"{'' * (box_width - 2)}")
def display_welcome() -> None:
"""Display the welcome banner."""
print("""
🌤 WEATHER CLI 🌤
Real-time weather at your terminal
Powered by Open-Meteo API (free, no API key needed!)
Type 'quit' to exit.
""")
# -----------------------------------------------------------------------------
# Main Application
# -----------------------------------------------------------------------------
def main() -> None:
"""Main entry point for the Weather CLI."""
display_welcome()
while True:
city_name = input("\n 🔍 Enter city name: ").strip()
if not city_name:
print(" ⚠️ Please enter a city name.")
continue
if city_name.lower() in ("quit", "exit", "q"):
print("\n 👋 Goodbye! Stay dry out there!")
break
try:
# Step 1: Geocode the city
print(f" 🔄 Looking up '{city_name}'...")
city_info = geocode_city(city_name)
if city_info is None:
print(f" ❌ City '{city_name}' not found. Try a different name.")
continue
# Step 2: Fetch weather
weather = get_weather(city_info["latitude"], city_info["longitude"])
# Step 3: Display
display_weather(city_info, weather)
except ConnectionError as e:
print(f" ❌ Connection error: {e}")
print(" 💡 Check your internet connection and try again.")
except Exception as e:
print(f" ❌ An unexpected error occurred: {e}")
if __name__ == "__main__":
main()

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
requirements.txt
pytest
mypy
black
isort