diff --git a/agents-md/AGENTS.md b/agents-md/AGENTS.md new file mode 100644 index 0000000000..dae9775653 --- /dev/null +++ b/agents-md/AGENTS.md @@ -0,0 +1,66 @@ +## Project Domain + +This is a REST API for browsing and managing a collection of cars. +Each car is a complete record with a unique, server-assigned `id` +and the fields `make`, `model`, `year`, `horsepower`, `engine_cc`, +and `transmission`. + +The `engine_cc` field can be `0` for fully electric cars. Every +car you return or store must include all fields. + +## Project Setup and Management + +- Python version: 3.14. Don't use newer syntax. +- Dependency management: `uv` and `pyproject.toml`. Never use `pip` + or a `requirements.txt` file. +- Add a dependency with `uv add `. Never use `uv pip` + for dependencies. +- Run the app with `uv run fastapi dev main.py`. +- Branch from `main` as `feature/` and use Conventional Commits. +- Stage changes for review. Don't commit to `main` or push without + being asked. + +## Coding Conventions + +- Type-hint public functions and methods, including their return types. +- Use `pathlib` for path management. Don't use `os.path`. +- Prefer f-strings over `str.format()` or `%` formatting. +- Follow EAFP: handle exceptions rather than checking conditions up front. +- Write Google-style docstrings for every public function and method. +- Validate request bodies with Pydantic models. +- Embrace idiomatic Python like comprehensions, generators, and decorators. + +## Project Structure + +- The project is flat: `main.py` holds the app and `cars.json` holds + the data. +- The `main.py` file is the only module to edit when adding features. +- Put tests in `tests/test_main.py`. +- Don't create new packages or new files without being asked. + +## Quality Gates + +A task is done only when all of these pass: + +- `uv run ruff format` leaves the code unchanged. +- `uv run ruff check` reports no errors. +- `uv run mypy main.py` reports no errors. +- `uv run pytest` passes, with a test added for every new endpoint. + +## Constraints + +- Ask before adding any external dependency. +- Preserve the signature and response shape of existing endpoints. +- Don't use blocking I/O inside `async` functions. +- Keep existing tests intact, and fix the code to make them pass. +- Declare a task done only after the gates pass and docstrings are + updated. + +## Ignore + +Treat everything in `.gitignore` as off-limits to read or edit. On top of +that, never open: + +- Secrets and `.env` files +- Large data files unrelated to the current task +- Vendored or generated code diff --git a/agents-md/README.md b/agents-md/README.md new file mode 100644 index 0000000000..59bf2a19ed --- /dev/null +++ b/agents-md/README.md @@ -0,0 +1,3 @@ +# How to Write an AGENTS.md File for a Python Project + +This folder provides the code examples for the Real Python tutorial [How to Write an AGENTS.md File for a Python Project](https://realpython.com/agents-md/) diff --git a/agents-md/cars.json b/agents-md/cars.json new file mode 100644 index 0000000000..3e50a522ee --- /dev/null +++ b/agents-md/cars.json @@ -0,0 +1,92 @@ +[ + { + "id": 1, + "make": "Ford", + "model": "Mustang", + "year": 1969, + "horsepower": 290, + "engine_cc": 5752, + "transmission": "Manual" + }, + { + "id": 2, + "make": "Chevrolet", + "model": "Corvette", + "year": 2020, + "horsepower": 490, + "engine_cc": 6162, + "transmission": "Automatic" + }, + { + "id": 3, + "make": "Dodge", + "model": "Charger", + "year": 2023, + "horsepower": 370, + "engine_cc": 5654, + "transmission": "Automatic" + }, + { + "id": 4, + "make": "Tesla", + "model": "Model S", + "year": 2022, + "horsepower": 670, + "engine_cc": 0, + "transmission": "Automatic" + }, + { + "id": 5, + "make": "Jeep", + "model": "Wrangler", + "year": 2021, + "horsepower": 285, + "engine_cc": 3604, + "transmission": "Automatic" + }, + { + "id": 6, + "make": "Ford", + "model": "F-150", + "year": 2024, + "horsepower": 400, + "engine_cc": 3496, + "transmission": "Automatic" + }, + { + "id": 7, + "make": "Cadillac", + "model": "Escalade", + "year": 2023, + "horsepower": 420, + "engine_cc": 6162, + "transmission": "Automatic" + }, + { + "id": 8, + "make": "Chevrolet", + "model": "Camaro", + "year": 2018, + "horsepower": 455, + "engine_cc": 6162, + "transmission": "Manual" + }, + { + "id": 9, + "make": "GMC", + "model": "Sierra", + "year": 2022, + "horsepower": 355, + "engine_cc": 5328, + "transmission": "Automatic" + }, + { + "id": 10, + "make": "Chrysler", + "model": "300", + "year": 2019, + "horsepower": 292, + "engine_cc": 3604, + "transmission": "Automatic" + } +] diff --git a/agents-md/original_main.py b/agents-md/original_main.py new file mode 100644 index 0000000000..9d51c27a60 --- /dev/null +++ b/agents-md/original_main.py @@ -0,0 +1,22 @@ +import json +from pathlib import Path + +from fastapi import FastAPI, HTTPException + +app = FastAPI() +cars: list[dict] = json.loads(Path("cars.json").read_text()) + + +@app.get("/cars") +def list_cars() -> list[dict]: + """Return a list of all cars.""" + return cars + + +@app.get("/cars/{car_id}") +def get_car(car_id: int) -> dict: + """Return a single car by its id, or raise 404 if it doesn't exist.""" + for car in cars: + if car["id"] == car_id: + return car + raise HTTPException(status_code=404, detail="Car not found") diff --git a/agents-md/run1_main.py b/agents-md/run1_main.py new file mode 100644 index 0000000000..7428905333 --- /dev/null +++ b/agents-md/run1_main.py @@ -0,0 +1,41 @@ +import json +import os + +from fastapi import FastAPI, HTTPException + +app = FastAPI() + +data_file = os.path.join(os.path.dirname(__file__), "cars.json") +with open(data_file) as f: + cars = json.load(f) + + +@app.get("/cars") +def list_cars() -> list[dict]: + """Return a list of all cars.""" + return cars + + +@app.get("/cars/{car_id}") +def get_car(car_id: int) -> dict: + """Return a single car by its id, or raise 404 if it doesn't exist.""" + for car in cars: + if car["id"] == car_id: + return car + raise HTTPException(status_code=404, detail="Car not found") + + +@app.post("/cars") +def create_car(car: dict): + car["id"] = len(cars) + 1 + cars.append(car) + return {"message": "Car created successfully", "car": car} + + +@app.delete("/cars/{car_id}") +def delete_car(car_id: int): + for i in range(len(cars)): + if cars[i]["id"] == car_id: + cars.pop(i) + return {"message": "Car deleted"} + return {"error": "Car not found"} diff --git a/agents-md/run2_main.py b/agents-md/run2_main.py new file mode 100644 index 0000000000..65cd6188b6 --- /dev/null +++ b/agents-md/run2_main.py @@ -0,0 +1,51 @@ +import json +from pathlib import Path + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +app = FastAPI() +cars: list[dict] = json.loads(Path("cars.json").read_text()) + + +class NewCar(BaseModel): + make: str + model: str + year: int + horsepower: int + engine_cc: int + transmission: str + + +@app.get("/cars") +def list_cars() -> list[dict]: + """Return a list of all cars.""" + return cars + + +@app.get("/cars/{car_id}") +def get_car(car_id: int) -> dict: + """Return a single car by its id, or raise 404 if it doesn't exist.""" + for car in cars: + if car["id"] == car_id: + return car + raise HTTPException(status_code=404, detail="Car not found") + + +@app.post("/cars", status_code=201) +def create_car(new_car: NewCar) -> dict: + """Add a new car and return it with a server-assigned id.""" + car = new_car.model_dump() + car["id"] = max((existing["id"] for existing in cars), default=0) + 1 + cars.append(car) + return car + + +@app.delete("/cars/{car_id}", status_code=204) +def delete_car(car_id: int) -> None: + """Delete a car by its id, or raise 404 if it doesn't exist.""" + for index, car in enumerate(cars): + if car["id"] == car_id: + del cars[index] + return + raise HTTPException(status_code=404, detail="Car not found") diff --git a/agents-md/test_main.py b/agents-md/test_main.py new file mode 100644 index 0000000000..9a63199b2d --- /dev/null +++ b/agents-md/test_main.py @@ -0,0 +1,26 @@ +from fastapi.testclient import TestClient + +from run2_main import app, cars + +client = TestClient(app) + + +def test_create_car_assigns_unique_id(): + new_car = { + "make": "Honda", + "model": "Civic", + "year": 2021, + "horsepower": 158, + "engine_cc": 1996, + "transmission": "Manual", + } + response = client.post("/cars", json=new_car) + + assert response.status_code == 201 + assert response.json()["id"] not in {car["id"] for car in cars[:-1]} + + +def test_delete_missing_car_returns_404(): + response = client.delete("/cars/999") + + assert response.status_code == 404