Molemmat esimerkit (single entity + FK relaatio) yhdessä tiedostossa. 1699 tokenia, 10.4% kontekstista. 8b näkee konkreettisen FK-patternen.
205 lines
6.6 KiB
Markdown
205 lines
6.6 KiB
Markdown
# Example 1: Todo App (single entity)
|
|
|
|
## models.py
|
|
|
|
```python
|
|
"""Tietokantamallit — SQLAlchemy 2.0, Mapped-tyypitys, SQLite."""
|
|
from datetime import date
|
|
from sqlalchemy import String, Text, Date, create_engine
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, sessionmaker
|
|
|
|
DATABASE_URL = "sqlite:///./app.db"
|
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
class Todo(Base):
|
|
__tablename__ = "todos"
|
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
title: Mapped[str] = mapped_column(String(255))
|
|
description: Mapped[str | None] = mapped_column(Text, default=None)
|
|
due_date: Mapped[date | None] = mapped_column(Date, default=None)
|
|
priority: Mapped[int] = mapped_column(default=1)
|
|
status: Mapped[str] = mapped_column(String(20), default="pending")
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
```
|
|
|
|
## schemas.py
|
|
|
|
```python
|
|
from datetime import date
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
class TodoCreate(BaseModel):
|
|
title: str
|
|
description: str | None = None
|
|
due_date: date | None = None
|
|
priority: int = 1
|
|
status: str = "pending"
|
|
|
|
class TodoResponse(TodoCreate):
|
|
id: int
|
|
model_config = ConfigDict(from_attributes=True)
|
|
```
|
|
|
|
## test_main.py — exactly 6 tests per entity
|
|
|
|
```python
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
from main import app, get_db
|
|
from models import Base
|
|
|
|
test_engine = create_engine("sqlite:///./test.db", connect_args={"check_same_thread": False})
|
|
TestSession = sessionmaker(autocommit=False, autoflush=False, bind=test_engine)
|
|
Base.metadata.create_all(bind=test_engine)
|
|
|
|
def override_get_db():
|
|
db = TestSession()
|
|
try: yield db
|
|
finally: db.close()
|
|
|
|
app.dependency_overrides[get_db] = override_get_db
|
|
client = TestClient(app)
|
|
|
|
def test_create_todo():
|
|
response = client.post("/todos/", json={"title": "Osta maitoa", "priority": 2})
|
|
assert response.status_code == 201
|
|
assert "id" in response.json()
|
|
|
|
def test_list_todos():
|
|
client.post("/todos/", json={"title": "Listattava"})
|
|
response = client.get("/todos/")
|
|
assert response.status_code == 200
|
|
assert len(response.json()) >= 1
|
|
|
|
def test_get_todo_by_id():
|
|
created = client.post("/todos/", json={"title": "Haettava"}).json()
|
|
response = client.get(f"/todos/{created['id']}")
|
|
assert response.status_code == 200
|
|
|
|
def test_get_todo_not_found():
|
|
response = client.get("/todos/99999")
|
|
assert response.status_code == 404
|
|
|
|
def test_update_todo():
|
|
created = client.post("/todos/", json={"title": "Vanha"}).json()
|
|
response = client.put(f"/todos/{created['id']}", json={"title": "Uusi"})
|
|
assert response.status_code == 200
|
|
|
|
def test_delete_todo():
|
|
created = client.post("/todos/", json={"title": "Poistettava"}).json()
|
|
response = client.delete(f"/todos/{created['id']}")
|
|
assert response.status_code == 204
|
|
```
|
|
|
|
# Example 2: Blog (two entities with ForeignKey)
|
|
|
|
NOTE: ForeignKey is imported from sqlalchemy, NOT from sqlalchemy.orm!
|
|
|
|
## models.py
|
|
|
|
```python
|
|
from datetime import datetime
|
|
from sqlalchemy import String, Text, DateTime, ForeignKey, create_engine
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker
|
|
|
|
DATABASE_URL = "sqlite:///./app.db"
|
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
class Author(Base):
|
|
__tablename__ = "authors"
|
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
name: Mapped[str] = mapped_column(String(255))
|
|
email: Mapped[str] = mapped_column(String(255), unique=True)
|
|
bio: Mapped[str | None] = mapped_column(Text, default=None)
|
|
posts: Mapped[list["Post"]] = relationship(back_populates="author")
|
|
|
|
class Post(Base):
|
|
__tablename__ = "posts"
|
|
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
|
title: Mapped[str] = mapped_column(String(255))
|
|
content: Mapped[str] = mapped_column(Text)
|
|
author_id: Mapped[int] = mapped_column(ForeignKey("authors.id"))
|
|
published_at: Mapped[datetime | None] = mapped_column(DateTime, default=None)
|
|
status: Mapped[str] = mapped_column(String(20), default="draft")
|
|
author: Mapped["Author"] = relationship(back_populates="posts")
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
```
|
|
|
|
## schemas.py
|
|
|
|
```python
|
|
from datetime import datetime
|
|
from pydantic import BaseModel, ConfigDict
|
|
|
|
class AuthorCreate(BaseModel):
|
|
name: str
|
|
email: str
|
|
bio: str | None = None
|
|
|
|
class AuthorResponse(AuthorCreate):
|
|
id: int
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
class PostCreate(BaseModel):
|
|
title: str
|
|
content: str
|
|
author_id: int
|
|
published_at: datetime | None = None
|
|
status: str = "draft"
|
|
|
|
class PostResponse(PostCreate):
|
|
id: int
|
|
model_config = ConfigDict(from_attributes=True)
|
|
```
|
|
|
|
## test_main.py — 6 tests per entity, create parent FIRST for child tests
|
|
|
|
```python
|
|
client = TestClient(app) # same setup as above
|
|
|
|
def _create_author(name="Kirjailija", email=None):
|
|
if email is None:
|
|
email = f"{name.lower().replace(' ', '.')}@example.com"
|
|
return client.post("/authors/", json={"name": name, "email": email}).json()
|
|
|
|
def test_create_author():
|
|
response = client.post("/authors/", json={"name": "Aleksis Kivi", "email": "aleksis@example.com"})
|
|
assert response.status_code == 201
|
|
|
|
def test_list_authors():
|
|
_create_author("Minna Canth", "minna@example.com")
|
|
response = client.get("/authors/")
|
|
assert response.status_code == 200
|
|
assert len(response.json()) >= 1
|
|
|
|
# ... (same pattern: get_by_id, not_found, update, delete)
|
|
|
|
def test_create_post():
|
|
author = _create_author("Tove Jansson", "tove@example.com")
|
|
response = client.post("/posts/", json={"title": "Artikkeli", "content": "Sisältö", "author_id": author["id"]})
|
|
assert response.status_code == 201
|
|
|
|
def test_update_post():
|
|
author = _create_author("Joel Lehtonen", "joel@example.com")
|
|
created = client.post("/posts/", json={"title": "Vanha", "content": "Teksti", "author_id": author["id"]}).json()
|
|
response = client.put(f"/posts/{created['id']}", json={"title": "Uusi", "content": "Muokattu", "author_id": author["id"]})
|
|
assert response.status_code == 200
|
|
|
|
def test_delete_post():
|
|
author = _create_author("Aino Kallas", "aino@example.com")
|
|
created = client.post("/posts/", json={"title": "Poistettava", "content": "Poistetaan", "author_id": author["id"]}).json()
|
|
response = client.delete(f"/posts/{created['id']}")
|
|
assert response.status_code == 204
|
|
```
|