Siirrä kipina-codebench projektin päätasolle
This commit is contained in:
84
kipina-codebench/golden-examples/DOCUMENTATION.md
Normal file
84
kipina-codebench/golden-examples/DOCUMENTATION.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Dokumentointiohjeet — Zensical
|
||||
|
||||
Hyvä dokumentointi kertoo **mitä asia ON**, ei mitä se tekee. Se on kuin zen-koan: lyhyt, tarkka, riittävä.
|
||||
|
||||
## Periaatteet
|
||||
|
||||
1. **Yksi rivi riittää.** Jos tarvitset kappaleen, koodi on liian monimutkainen.
|
||||
2. **Kerro mitä, älä miten.** `"""Tietokantamallit — SQLAlchemy 2.0, SQLite."""` ei `"""This module creates database models using SQLAlchemy..."""`
|
||||
3. **Älä toista koodia.** Jos funktio on `create_todo`, docstring ei ole "Creates a todo".
|
||||
4. **Suomi tai englanti, ei molempia.** Valitse yksi kieli per projekti.
|
||||
5. **Ei täytesanoja.** "This module provides functionality for" → poista.
|
||||
|
||||
## Mitä dokumentoidaan
|
||||
|
||||
| Kohde | Dokumentointi | Esimerkki |
|
||||
|-------|--------------|-----------|
|
||||
| **Moduuli** (.py) | Aina. Yksi rivi: mitä tiedosto sisältää. | `"""Pydantic v2 -skeemat — Create ja Response."""` |
|
||||
| **Luokka** | Aina. Mitä entiteetti edustaa. | `"""Tehtävä — otsikko, deadline, prioriteetti."""` |
|
||||
| **Funktio** | Vain jos nimi ei kerro kaikkea. | `get_db` → `"""Tietokantasessio per pyyntö."""` |
|
||||
| **CRUD-endpoint** | Ei. Nimi + HTTP-metodi riittää. | `create_todo`, `list_todos` — itsedokumentoivia |
|
||||
| **Testi** | Ei. Testin nimi on dokumentaatio. | `test_get_todo_not_found` — selvä |
|
||||
| **Konfiguraatio** | Kommentti vain jos arvo yllättää. | `check_same_thread: False # SQLite + FastAPI` |
|
||||
|
||||
## Mitä EI dokumentoida
|
||||
|
||||
- Importteja
|
||||
- Ilmeisiä parametreja (`item_id: int`)
|
||||
- Tyyppivihjeitä jotka kertovat saman asian
|
||||
- Geneerisiä "boilerplate"-docstringejä
|
||||
|
||||
## Esimerkkejä
|
||||
|
||||
### Hyvä (zensical)
|
||||
|
||||
```python
|
||||
"""Tietokantamallit — SQLAlchemy 2.0, Mapped-tyypitys, SQLite."""
|
||||
|
||||
class Todo(Base):
|
||||
"""Tehtävä — otsikko, kuvaus, deadline, prioriteetti ja status."""
|
||||
...
|
||||
|
||||
def get_db():
|
||||
"""Tietokantasessio per pyyntö."""
|
||||
...
|
||||
```
|
||||
|
||||
### Huono (verbose)
|
||||
|
||||
```python
|
||||
"""
|
||||
This module defines the database models for the Todo application.
|
||||
It uses SQLAlchemy ORM to create the database tables and provides
|
||||
the session factory for database connections.
|
||||
"""
|
||||
|
||||
class Todo(Base):
|
||||
"""
|
||||
Represents a todo item in the database.
|
||||
|
||||
Attributes:
|
||||
id: The unique identifier for the todo item.
|
||||
title: The title of the todo item.
|
||||
...
|
||||
"""
|
||||
...
|
||||
```
|
||||
|
||||
### Huono (tyhjä)
|
||||
|
||||
```python
|
||||
# Ei docstringejä ollenkaan — lukija ei tiedä mikä tiedoston rooli on
|
||||
class Todo(Base):
|
||||
__tablename__ = "todos"
|
||||
...
|
||||
```
|
||||
|
||||
## Tarkistuslista
|
||||
|
||||
Generoitu koodi on hyvin dokumentoitu kun:
|
||||
- [ ] Jokainen .py-tiedosto alkaa yksirivisellä docstringillä
|
||||
- [ ] Jokainen luokka kertoo mitä entiteetti edustaa
|
||||
- [ ] Docstringit ovat saman kielen kuin muu koodi
|
||||
- [ ] CRUD-endpointeilla ei ole turhia docstringejä
|
||||
- [ ] Kommentteja on vain siellä missä koodi yllättää
|
||||
123
kipina-codebench/golden-examples/README.md
Normal file
123
kipina-codebench/golden-examples/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Golden Examples — referenssitoteutukset
|
||||
|
||||
Kultaiset esimerkit ovat **täydellisiä, testattuja** FastAPI-projekteja joita LLM käyttää mallina koodigeneroinnissa. Malli näkee esimerkin ja tuottaa vastaavan rakenteen uudelle projektille.
|
||||
|
||||
## Uuden esimerkin luominen
|
||||
|
||||
### 1. Luo hakemisto
|
||||
|
||||
```bash
|
||||
mkdir golden-examples/shop
|
||||
```
|
||||
|
||||
Nimeä hakemisto skenaarion mukaan (todo, blog, shop, booking...).
|
||||
|
||||
### 2. Luo 4 tiedostoa
|
||||
|
||||
| Tiedosto | Sisältö |
|
||||
|----------|---------|
|
||||
| `models.py` | SQLAlchemy 2.0 -mallit (DeclarativeBase, Mapped, mapped_column) |
|
||||
| `schemas.py` | Pydantic v2 -skeemat (ConfigDict, `str \| None` -syntaksi) |
|
||||
| `main.py` | FastAPI CRUD -endpointit (POST 201, GET, GET/:id 404, PUT, DELETE 204) |
|
||||
| `test_main.py` | Pytest + TestClient, erillinen test.db, uniikki data per testi |
|
||||
|
||||
### 3. Noudata konventioita
|
||||
|
||||
**Python-versio:** >=3.14
|
||||
|
||||
**SQLAlchemy 2.0** (ei legacy):
|
||||
```python
|
||||
# Oikein
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class Todo(Base):
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
title: Mapped[str] = mapped_column(String(255))
|
||||
status: Mapped[str] = mapped_column(String(20), default="pending")
|
||||
|
||||
# Väärin
|
||||
Base = declarative_base()
|
||||
id = Column(Integer, primary_key=True)
|
||||
```
|
||||
|
||||
**Pydantic v2** (ei v1):
|
||||
```python
|
||||
# Oikein
|
||||
class TodoResponse(TodoCreate):
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# Väärin
|
||||
class Config:
|
||||
orm_mode = True
|
||||
```
|
||||
|
||||
**Tyypitys:**
|
||||
```python
|
||||
# Oikein
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None)
|
||||
|
||||
# Väärin
|
||||
description: Mapped[Optional[str]]
|
||||
```
|
||||
|
||||
**Dokumentointi (zensical):**
|
||||
```python
|
||||
"""Tietokantamallit — SQLAlchemy 2.0, Mapped-tyypitys, SQLite."""
|
||||
|
||||
class Todo(Base):
|
||||
"""Tehtävä — otsikko, kuvaus, deadline, prioriteetti ja status."""
|
||||
```
|
||||
|
||||
Yksi rivi riittää. Kerro mitä asia ON, älä mitä se tekee. Katso [DOCUMENTATION.md](DOCUMENTATION.md).
|
||||
|
||||
**Testidata — uniikki ja kuvaava:**
|
||||
```python
|
||||
# Oikein
|
||||
def test_create_todo():
|
||||
response = client.post("/todos/", json={"title": "Osta maitoa", "priority": 2})
|
||||
|
||||
def test_update_todo():
|
||||
created = client.post("/todos/", json={"title": "Vanha otsikko"}).json()
|
||||
|
||||
# Väärin — geneerinen data
|
||||
def test_create_todo():
|
||||
response = client.post("/todos/", json={"title": "test", "priority": 1})
|
||||
```
|
||||
|
||||
### 4. Testaa Docker-kontissa
|
||||
|
||||
```bash
|
||||
rm -rf /tmp/golden-test && mkdir /tmp/golden-test
|
||||
cp golden-examples/shop/*.py /tmp/golden-test/
|
||||
docker run --rm -v /tmp/golden-test:/src:ro kipina-pytest
|
||||
```
|
||||
|
||||
**Kaikkien testien pitää mennä läpi.** Ei varoituksia, ei deprecation-viestejä.
|
||||
|
||||
### 5. Vaikeustasot
|
||||
|
||||
| Taso | Esimerkit | Haaste |
|
||||
|------|-----------|--------|
|
||||
| 1 — Perus-CRUD | `todo/`, `users/`, `notes/` | Yksi entiteetti |
|
||||
| 2 — Relaatiot | `blog/`, `library/`, `school/` | Foreign key, 2–3 entiteettiä |
|
||||
| 3 — Liiketoimintalogiikka | `shop/`, `booking/` | Custom endpointit, validointi |
|
||||
|
||||
Aloita tasosta 1 ja etene. Tason 1 esimerkkien pitää olla yksinkertaisia — ne opettavat mallille perusrakenteen.
|
||||
|
||||
## Miten esimerkit vaikuttavat
|
||||
|
||||
Benchmark lataa `todo/`-esimerkin ja syöttää sen LLM:lle osana koodingenerointipromptia:
|
||||
|
||||
```
|
||||
REFERENCE IMPLEMENTATION (todo project — follow this exact structure):
|
||||
|
||||
=== models.py ===
|
||||
<todo/models.py sisältö>
|
||||
|
||||
=== schemas.py ===
|
||||
...
|
||||
```
|
||||
|
||||
Malli näkee tarkan esimerkin ja tuottaa vastaavan rakenteen uudelle projektille. Mitä parempi esimerkki, sitä parempi tulos.
|
||||
110
kipina-codebench/golden-examples/blog/main.py
Normal file
110
kipina-codebench/golden-examples/blog/main.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""FastAPI CRUD — kaksi endpoint-settiä, Author ja Post."""
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import SessionLocal, Author, Post
|
||||
from schemas import AuthorCreate, AuthorResponse, PostCreate, PostResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Tietokantasessio per pyyntö."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# --- Author ---
|
||||
|
||||
|
||||
@app.post("/authors/", response_model=AuthorResponse, status_code=201)
|
||||
def create_author(item: AuthorCreate, db: Session = Depends(get_db)):
|
||||
db_item = Author(**item.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@app.get("/authors/", response_model=list[AuthorResponse])
|
||||
def list_authors(db: Session = Depends(get_db)):
|
||||
return db.query(Author).all()
|
||||
|
||||
|
||||
@app.get("/authors/{item_id}", response_model=AuthorResponse)
|
||||
def get_author(item_id: int, db: Session = Depends(get_db)):
|
||||
item = db.query(Author).filter(Author.id == item_id).first()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
return item
|
||||
|
||||
|
||||
@app.put("/authors/{item_id}", response_model=AuthorResponse)
|
||||
def update_author(item_id: int, item: AuthorCreate, db: Session = Depends(get_db)):
|
||||
db_item = db.query(Author).filter(Author.id == item_id).first()
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
for key, value in item.model_dump().items():
|
||||
setattr(db_item, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@app.delete("/authors/{item_id}", status_code=204)
|
||||
def delete_author(item_id: int, db: Session = Depends(get_db)):
|
||||
db_item = db.query(Author).filter(Author.id == item_id).first()
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Author not found")
|
||||
db.delete(db_item)
|
||||
db.commit()
|
||||
|
||||
|
||||
# --- Post ---
|
||||
|
||||
|
||||
@app.post("/posts/", response_model=PostResponse, status_code=201)
|
||||
def create_post(item: PostCreate, db: Session = Depends(get_db)):
|
||||
db_item = Post(**item.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@app.get("/posts/", response_model=list[PostResponse])
|
||||
def list_posts(db: Session = Depends(get_db)):
|
||||
return db.query(Post).all()
|
||||
|
||||
|
||||
@app.get("/posts/{item_id}", response_model=PostResponse)
|
||||
def get_post(item_id: int, db: Session = Depends(get_db)):
|
||||
item = db.query(Post).filter(Post.id == item_id).first()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
return item
|
||||
|
||||
|
||||
@app.put("/posts/{item_id}", response_model=PostResponse)
|
||||
def update_post(item_id: int, item: PostCreate, db: Session = Depends(get_db)):
|
||||
db_item = db.query(Post).filter(Post.id == item_id).first()
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
for key, value in item.model_dump().items():
|
||||
setattr(db_item, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@app.delete("/posts/{item_id}", status_code=204)
|
||||
def delete_post(item_id: int, db: Session = Depends(get_db)):
|
||||
db_item = db.query(Post).filter(Post.id == item_id).first()
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
db.delete(db_item)
|
||||
db.commit()
|
||||
45
kipina-codebench/golden-examples/blog/models.py
Normal file
45
kipina-codebench/golden-examples/blog/models.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Tietokantamallit — SQLAlchemy 2.0, Mapped-tyypitys, ForeignKey-relaatiot."""
|
||||
|
||||
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):
|
||||
"""Kirjoittaja — nimi, sähköposti ja bio."""
|
||||
|
||||
__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):
|
||||
"""Blogipostaus — otsikko, sisältö, kirjoittaja, julkaisuaika ja tila."""
|
||||
|
||||
__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)
|
||||
37
kipina-codebench/golden-examples/blog/schemas.py
Normal file
37
kipina-codebench/golden-examples/blog/schemas.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Pydantic v2 -skeemat — Create sisääntulolle, Response vastaukselle."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class AuthorCreate(BaseModel):
|
||||
"""Uuden kirjoittajan luonti. Pakolliset: name, email."""
|
||||
|
||||
name: str
|
||||
email: str
|
||||
bio: str | None = None
|
||||
|
||||
|
||||
class AuthorResponse(AuthorCreate):
|
||||
"""Palautettava kirjoittaja — sisältää id:n."""
|
||||
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PostCreate(BaseModel):
|
||||
"""Uuden postauksen luonti. Pakolliset: title, content, author_id."""
|
||||
|
||||
title: str
|
||||
content: str
|
||||
author_id: int
|
||||
published_at: datetime | None = None
|
||||
status: str = "draft"
|
||||
|
||||
|
||||
class PostResponse(PostCreate):
|
||||
"""Palautettava postaus — sisältää id:n."""
|
||||
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
164
kipina-codebench/golden-examples/blog/test_main.py
Normal file
164
kipina-codebench/golden-examples/blog/test_main.py
Normal file
@@ -0,0 +1,164 @@
|
||||
"""Pytest — TestClient, erillinen test.db, uniikki data per testi."""
|
||||
|
||||
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 _create_author(name="Eino Leino", email=None):
|
||||
"""Apufunktio kirjoittajan luomiseen testeissä."""
|
||||
if email is None:
|
||||
email = f"{name.lower().replace(' ', '.')}@example.com"
|
||||
return client.post(
|
||||
"/authors/", json={"name": name, "email": email}
|
||||
).json()
|
||||
|
||||
|
||||
# --- Author-testit ---
|
||||
|
||||
|
||||
def test_create_author():
|
||||
response = client.post(
|
||||
"/authors/",
|
||||
json={"name": "Aleksis Kivi", "email": "aleksis@example.com", "bio": "Suomen kansalliskirjailija"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["name"] == "Aleksis Kivi"
|
||||
assert response.json()["bio"] == "Suomen kansalliskirjailija"
|
||||
assert "id" in response.json()
|
||||
|
||||
|
||||
def test_list_authors():
|
||||
_create_author("Minna Canth", "minna.canth@example.com")
|
||||
response = client.get("/authors/")
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) >= 1
|
||||
|
||||
|
||||
def test_get_author_by_id():
|
||||
created = _create_author("Väinö Linna", "vaino.linna@example.com")
|
||||
response = client.get(f"/authors/{created['id']}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == created["id"]
|
||||
|
||||
|
||||
def test_get_author_not_found():
|
||||
response = client.get("/authors/99999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_update_author():
|
||||
created = _create_author("Vanha Nimi", "vanha.nimi@example.com")
|
||||
response = client.put(
|
||||
f"/authors/{created['id']}",
|
||||
json={"name": "Uusi Nimi", "email": "uusi.nimi@example.com"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Uusi Nimi"
|
||||
|
||||
|
||||
def test_delete_author():
|
||||
created = _create_author("Poistettava Kirjailija", "poistettava@example.com")
|
||||
response = client.delete(f"/authors/{created['id']}")
|
||||
assert response.status_code == 204
|
||||
response = client.get(f"/authors/{created['id']}")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# --- Post-testit ---
|
||||
|
||||
|
||||
def test_create_post():
|
||||
author = _create_author("Tove Jansson", "tove.jansson@example.com")
|
||||
response = client.post(
|
||||
"/posts/",
|
||||
json={"title": "Muumipeikko ja pyrstötähti", "content": "Eräänä aamuna...", "author_id": author["id"]},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["title"] == "Muumipeikko ja pyrstötähti"
|
||||
assert response.json()["author_id"] == author["id"]
|
||||
assert response.json()["status"] == "draft"
|
||||
|
||||
|
||||
def test_list_posts():
|
||||
author = _create_author("Juhani Aho", "juhani.aho@example.com")
|
||||
client.post(
|
||||
"/posts/",
|
||||
json={"title": "Rautatie", "content": "Junasta kertova novelli.", "author_id": author["id"]},
|
||||
)
|
||||
response = client.get("/posts/")
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) >= 1
|
||||
|
||||
|
||||
def test_get_post_by_id():
|
||||
author = _create_author("Elias Lönnrot", "elias.lonnrot@example.com")
|
||||
created = client.post(
|
||||
"/posts/",
|
||||
json={"title": "Kalevala", "content": "Vaka vanha Väinämöinen.", "author_id": author["id"]},
|
||||
).json()
|
||||
response = client.get(f"/posts/{created['id']}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == created["id"]
|
||||
|
||||
|
||||
def test_get_post_not_found():
|
||||
response = client.get("/posts/99999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_update_post():
|
||||
author = _create_author("Joel Lehtonen", "joel.lehtonen@example.com")
|
||||
created = client.post(
|
||||
"/posts/",
|
||||
json={"title": "Vanha otsikko", "content": "Alkuperäinen teksti.", "author_id": author["id"]},
|
||||
).json()
|
||||
response = client.put(
|
||||
f"/posts/{created['id']}",
|
||||
json={"title": "Päivitetty otsikko", "content": "Muokattu teksti.", "author_id": author["id"], "status": "published"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "Päivitetty otsikko"
|
||||
assert response.json()["status"] == "published"
|
||||
|
||||
|
||||
def test_delete_post():
|
||||
author = _create_author("Aino Kallas", "aino.kallas@example.com")
|
||||
created = client.post(
|
||||
"/posts/",
|
||||
json={"title": "Poistettava postaus", "content": "Tämä poistetaan.", "author_id": author["id"]},
|
||||
).json()
|
||||
response = client.delete(f"/posts/{created['id']}")
|
||||
assert response.status_code == 204
|
||||
response = client.get(f"/posts/{created['id']}")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_post_belongs_to_author():
|
||||
author = _create_author("Sofi Oksanen", "sofi.oksanen@example.com")
|
||||
post = client.post(
|
||||
"/posts/",
|
||||
json={"title": "Puhdistus", "content": "Romaani Virosta.", "author_id": author["id"]},
|
||||
).json()
|
||||
assert post["author_id"] == author["id"]
|
||||
61
kipina-codebench/golden-examples/todo/main.py
Normal file
61
kipina-codebench/golden-examples/todo/main.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""FastAPI CRUD — yksi endpoint-setti per entiteetti."""
|
||||
|
||||
from fastapi import FastAPI, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models import SessionLocal, Todo
|
||||
from schemas import TodoCreate, TodoResponse
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Tietokantasessio per pyyntö."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
||||
def create_todo(item: TodoCreate, db: Session = Depends(get_db)):
|
||||
db_item = Todo(**item.model_dump())
|
||||
db.add(db_item)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@app.get("/todos/", response_model=list[TodoResponse])
|
||||
def list_todos(db: Session = Depends(get_db)):
|
||||
return db.query(Todo).all()
|
||||
|
||||
|
||||
@app.get("/todos/{item_id}", response_model=TodoResponse)
|
||||
def get_todo(item_id: int, db: Session = Depends(get_db)):
|
||||
item = db.query(Todo).filter(Todo.id == item_id).first()
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Todo not found")
|
||||
return item
|
||||
|
||||
|
||||
@app.put("/todos/{item_id}", response_model=TodoResponse)
|
||||
def update_todo(item_id: int, item: TodoCreate, db: Session = Depends(get_db)):
|
||||
db_item = db.query(Todo).filter(Todo.id == item_id).first()
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Todo not found")
|
||||
for key, value in item.model_dump().items():
|
||||
setattr(db_item, key, value)
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
return db_item
|
||||
|
||||
|
||||
@app.delete("/todos/{item_id}", status_code=204)
|
||||
def delete_todo(item_id: int, db: Session = Depends(get_db)):
|
||||
db_item = db.query(Todo).filter(Todo.id == item_id).first()
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Todo not found")
|
||||
db.delete(db_item)
|
||||
db.commit()
|
||||
30
kipina-codebench/golden-examples/todo/models.py
Normal file
30
kipina-codebench/golden-examples/todo/models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""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):
|
||||
"""Tehtävä — otsikko, kuvaus, deadline, prioriteetti ja status."""
|
||||
|
||||
__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)
|
||||
11
kipina-codebench/golden-examples/todo/pyproject.toml
Normal file
11
kipina-codebench/golden-examples/todo/pyproject.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[project]
|
||||
name = "todo-app"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"fastapi",
|
||||
"uvicorn[standard]",
|
||||
"sqlalchemy",
|
||||
"pytest",
|
||||
"httpx",
|
||||
]
|
||||
22
kipina-codebench/golden-examples/todo/schemas.py
Normal file
22
kipina-codebench/golden-examples/todo/schemas.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Pydantic v2 -skeemat — Create sisääntulolle, Response vastaukselle."""
|
||||
|
||||
from datetime import date
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class TodoCreate(BaseModel):
|
||||
"""Uuden tehtävän luonti. Pakolliset: title."""
|
||||
|
||||
title: str
|
||||
description: str | None = None
|
||||
due_date: date | None = None
|
||||
priority: int = 1
|
||||
status: str = "pending"
|
||||
|
||||
|
||||
class TodoResponse(TodoCreate):
|
||||
"""Palautettava tehtävä — sisältää id:n."""
|
||||
|
||||
id: int
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
69
kipina-codebench/golden-examples/todo/test_main.py
Normal file
69
kipina-codebench/golden-examples/todo/test_main.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Pytest — TestClient, erillinen test.db, uniikki data per testi."""
|
||||
|
||||
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 response.json()["title"] == "Osta maitoa"
|
||||
assert "id" in response.json()
|
||||
|
||||
|
||||
def test_list_todos():
|
||||
client.post("/todos/", json={"title": "Listattava tehtävä"})
|
||||
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 tehtävä"}).json()
|
||||
response = client.get(f"/todos/{created['id']}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == created["id"]
|
||||
|
||||
|
||||
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 otsikko"}).json()
|
||||
response = client.put(
|
||||
f"/todos/{created['id']}", json={"title": "Uusi otsikko"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "Uusi otsikko"
|
||||
|
||||
|
||||
def test_delete_todo():
|
||||
created = client.post("/todos/", json={"title": "Poistettava"}).json()
|
||||
response = client.delete(f"/todos/{created['id']}")
|
||||
assert response.status_code == 204
|
||||
response = client.get(f"/todos/{created['id']}")
|
||||
assert response.status_code == 404
|
||||
Reference in New Issue
Block a user