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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,57 @@
# Copilot Instructions (project-specific)

Purpose
- Short guide for GitHub Copilot CLI sessions working on this repository (courseware, not a production product).

Build, test, and lint commands
- Python (primary sample: samples/book-app-project):
- Run all tests: python -m pytest samples/book-app-project -q
- Run a single test: python -m pytest samples/book-app-project/tests/test_books.py::test_name -q
- Install deps: follow pyproject.toml (e.g., `poetry install`), or `python -m pip install -r samples/book-app-project/requirements.txt` if present
- Lint (if available): python -m flake8 samples/book-app-project

- JavaScript (samples/book-app-project-js):
- Install & test: cd samples/book-app-project-js && npm install && npm test
- Run a single test (Jest): npm test -- -t "test name" (or use npx jest <path>)
- Lint: npm run lint (if script exists)

- C# (samples/book-app-project-cs):
- Run tests: dotnet test samples/book-app-project-cs
- Run a single test: dotnet test samples/book-app-project-cs --filter "FullyQualifiedName~TestName"

High-level architecture
- Course chapters live in top-level folders 00-07; each chapter is self-contained with a README following the same structure.
- Primary sample: samples/book-app-project (Python). JS and C# variants exist under samples/book-app-project-js and samples/book-app-project-cs.
- Agents and skills:
- .github/agents and .github/skills store agent & skill templates used by course demos
- samples/agents and samples/skills contain example implementations
- CI / automation: .github/workflows and .github/scripts contain helper scripts for building demo assets and translations.
- MCP examples: samples/mcp-configs/ and chapter 06 contain MCP server configs and docs.

Key conventions (repo-specific)
- Primary-sample-first: Use samples/book-app-project for Python-centric examples in chapters.
- Test file naming: pytest tests are under samples/book-app-project/tests/ and use test_*.py naming.
- Python version: 3.10+ (see samples/book-app-project/pyproject.toml).
- Intentional-bugs: Do NOT fix files in samples/book-app-buggy/ or samples/buggy-code/ — they are exercises.
- Chapter README format: Every chapter README must include: Real-World Analogy; Core Concepts; Hands-On Examples; Assignment; What's Next.
- Filenames & identifiers: prefer kebab-case for session names and examples used in text and commands.
- Command flag style: use `--flag=value` when a value is required and `--flag` for booleans in examples.
- Docs-first edits: When changing sample behavior, update corresponding chapter README and tests.

Files & places to check for AI sessions
- Primary sample code: samples/book-app-project/
- Tests: samples/book-app-project/tests/
- Agent templates: .github/agents/ and samples/agents/
- Skill templates: .github/skills/ and samples/skills/
- MCP server examples: samples/mcp-configs/ and 06-mcp-servers/

MCP servers
- This repo includes MCP examples (samples/mcp-configs/) and a chapter (06-mcp-servers). Would you like assistance configuring an MCP server for local testing? (Ask to configure: Playwright or other demo servers.)

Summary
- This file adds actionable build/test/lint commands, a concise architecture overview, and repo-specific conventions to help future Copilot sessions behave usefully and avoid common mistakes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Copilot Instructions

These instructions guide GitHub Copilot when working in this repository.
Expand Down
24 changes: 19 additions & 5 deletions samples/book-app-project/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,34 @@ It can add, remove, and list books. Also mark them as read.

## Running the App

Available commands (run from samples/book-app-project/):

```bash
python book_app.py list
python book_app.py add
python book_app.py find
python book_app.py remove
python book_app.py list # Show all books
python book_app.py add # Interactive add
python book_app.py find # Find books by author (exact match)
python book_app.py remove # Remove a book by title
python book_app.py search # Interactive search prompt
python book_app.py search "dune" title # Search titles containing "dune"
python book_app.py search "gibson" author # Search authors containing "gibson"
python book_app.py help
```

## Running Tests

Run the full test suite for the sample:

```bash
python -m pytest tests/
python -m pytest samples/book-app-project -q
```

Run a single test by node (example):

```bash
python -m pytest samples/book-app-project/tests/test_books.py::test_search_by_title_exact -q
```


---

## Notes
Expand Down
185 changes: 127 additions & 58 deletions samples/book-app-project/book_app.py
Original file line number Diff line number Diff line change
@@ -1,97 +1,166 @@
import sys
from books import BookCollection
from abc import ABC, abstractmethod
from books import BookCollection, Book
from typing import List


# Global collection instance
collection = BookCollection()


def show_books(books):
def prompt_required(label: str) -> str:
"""Prompt user for required input, retrying if empty."""
while True:
value = input(f"{label}: ").strip()
if value:
return value
print(f" {label} cannot be empty. Please try again.")


def show_books(books: List[Book]) -> None:
"""Display books in a user-friendly format."""
if not books:
print("No books found.")
return

print("\nYour Book Collection:\n")

for index, book in enumerate(books, start=1):
status = "✓" if book.read else " "
print(f"{index}. [{status}] {book.title} by {book.author} ({book.year})")

print()


def handle_list():
books = collection.list_books()
show_books(books)
class Command(ABC):
"""Base class for all commands."""

@abstractmethod
def execute(self) -> None:
"""Execute the command."""
pass

def handle_add():
print("\nAdd a New Book\n")
@abstractmethod
def help(self) -> str:
"""Return help text for this command."""
pass

title = input("Title: ").strip()
author = input("Author: ").strip()
year_str = input("Year: ").strip()

try:
year = int(year_str) if year_str else 0
collection.add_book(title, author, year)
print("\nBook added successfully.\n")
except ValueError as e:
print(f"\nError: {e}\n")
class ListCommand(Command):
def execute(self) -> None:
books = collection.list_books()
show_books(books)

def help(self) -> str:
return "Show all books"

def handle_remove():
print("\nRemove a Book\n")

title = input("Enter the title of the book to remove: ").strip()
collection.remove_book(title)
class AddCommand(Command):
def execute(self) -> None:
print("\nAdd a New Book\n")
title = prompt_required("Title")
author = prompt_required("Author")

print("\nBook removed if it existed.\n")
year_str = input("Year (optional): ").strip()
try:
year = int(year_str) if year_str else 0
collection.add_book(title, author, year)
print("\n✓ Book added successfully.\n")
except ValueError:
print("\n✗ Error: Year must be a valid number.\n")

def help(self) -> str:
return "Add a new book"

def handle_find():
print("\nFind Books by Author\n")

author = input("Author name: ").strip()
books = collection.find_by_author(author)
class RemoveCommand(Command):
def execute(self) -> None:
print("\nRemove a Book\n")
title = prompt_required("Enter the title of the book to remove")
if collection.remove_book(title):
print(f"\n✓ '{title}' removed.\n")
else:
print(f"\n✗ '{title}' not found.\n")

show_books(books)
def help(self) -> str:
return "Remove a book by title"


def show_help():
print("""
Book Collection Helper
class FindCommand(Command):
def execute(self) -> None:
print("\nFind Books by Author\n")
author = prompt_required("Author name")
books = collection.find_by_author(author)
show_books(books)

Commands:
list - Show all books
add - Add a new book
remove - Remove a book by title
find - Find books by author
help - Show this help message
""")
def help(self) -> str:
return "Find books by author (exact match)"


def main():
if len(sys.argv) < 2:
show_help()
return
class SearchCommand(Command):
def execute(self) -> None:
query = sys.argv[2] if len(sys.argv) >= 3 else prompt_required("Query")
field = sys.argv[3].lower() if len(sys.argv) >= 4 else "title"

if field not in ("title", "author"):
print(f"\n✗ Field must be 'title' or 'author', not '{field}'.\n")
return

books = collection.search_books(query, field)
show_books(books)

def help(self) -> str:
return "Search books by title or author (partial). Usage: search <query> [title|author]"


class HelpCommand(Command):
def __init__(self, commands: dict):
self.commands = commands

def execute(self) -> None:
self._print_help()

def _print_help(self) -> None:
print("\nBook Collection Helper\n")
print("Commands:")
for name, cmd in self.commands.items():
print(f" {name:<8} - {cmd.help()}")
print()

def help(self) -> str:
return "Show this help message"


class CommandDispatcher:
"""Manages command registration and execution."""

def __init__(self):
self.commands = {
"list": ListCommand(),
"add": AddCommand(),
"remove": RemoveCommand(),
"find": FindCommand(),
"search": SearchCommand(),
}
self.commands["help"] = HelpCommand(self.commands)

command = sys.argv[1].lower()

if command == "list":
handle_list()
elif command == "add":
handle_add()
elif command == "remove":
handle_remove()
elif command == "find":
handle_find()
elif command == "help":
show_help()
else:
print("Unknown command.\n")
show_help()
def dispatch(self, command_name: str) -> None:
"""Execute a command by name."""
command = self.commands.get(command_name.lower())
if command:
command.execute()
else:
print(f"✗ Unknown command: '{command_name}'.\n")
self.commands["help"].execute()

def run(self) -> None:
"""Main entry point."""
if len(sys.argv) < 2:
self.commands["help"].execute()
else:
self.dispatch(sys.argv[1])


def main() -> None:
dispatcher = CommandDispatcher()
dispatcher.run()


if __name__ == "__main__":
Expand Down
17 changes: 16 additions & 1 deletion samples/book-app-project/books.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,20 @@ def remove_book(self, title: str) -> bool:
return False

def find_by_author(self, author: str) -> List[Book]:
"""Find all books by a given author."""
"""Find all books by a given author (exact match, case-insensitive)."""
return [b for b in self.books if b.author.lower() == author.lower()]

def search_books(self, query: str, field: str = 'title') -> List[Book]:
"""Search books by title or author.

Args:
query: substring to search for (case-insensitive).
field: 'title' or 'author'. Defaults to 'title'.
Returns:
List[Book]: matching books (partial, case-insensitive).
"""
q = query.lower()
if field == 'author':
return [b for b in self.books if q in b.author.lower()]
# default to title
return [b for b in self.books if q in b.title.lower()]
30 changes: 30 additions & 0 deletions samples/book-app-project/tests/test_books.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,33 @@ def test_remove_book_invalid():
collection = BookCollection()
result = collection.remove_book("Nonexistent Book")
assert result is False


def test_search_by_title_exact():
collection = BookCollection()
collection.add_book("Dune", "Frank Herbert", 1965)
collection.add_book("Dune Messiah", "Frank Herbert", 1969)
results = collection.search_books("Dune", field='title')
assert any(b.title == "Dune" for b in results)


def test_search_by_title_partial_case_insensitive():
collection = BookCollection()
collection.add_book("The Hobbit", "J.R.R. Tolkien", 1937)
results = collection.search_books("hob", field='title')
assert any("hob" in b.title.lower() for b in results)


def test_search_by_author_partial_case_insensitive():
collection = BookCollection()
collection.add_book("Neuromancer", "William Gibson", 1984)
collection.add_book("Count Zero", "William Gibson", 1986)
results = collection.search_books("gibson", field='author')
assert len(results) == 2


def test_search_no_results():
collection = BookCollection()
collection.add_book("Frankenstein", "Mary Shelley", 1818)
results = collection.search_books("notfound", field='title')
assert results == []
10 changes: 10 additions & 0 deletions samples/mcp-configs/playwright-mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"mcpServers": {
"playwright-demo": {
"type": "local",
"command": "npx",
"args": ["-y", "http-server", "samples/src", "-p", "3000"],
"tools": ["browser"]
}
}
}
Loading