405 lines
12 KiB
Markdown
405 lines
12 KiB
Markdown
# 8 — Observer (observer) — README.md
|
|
|
|
**Malli:** `qwen-coder`
|
|
|
|
## System Prompt
|
|
|
|
```
|
|
You are an independent technical observer and risk analyst.
|
|
|
|
EVALUATE THE PROJECT FOR:
|
|
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
|
|
2. SECURITY: SQL injection risks? Input validation? Authentication?
|
|
3. RELIABILITY: Error handling? Database connection management? Edge cases?
|
|
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?
|
|
|
|
OUTPUT FORMAT:
|
|
- RISK: [critical/high/medium/low] Description
|
|
- List max 3-5 most important findings
|
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"
|
|
```
|
|
|
|
## Syöte
|
|
|
|
```
|
|
You are an independent technical observer and risk analyst.
|
|
|
|
EVALUATE THE PROJECT FOR:
|
|
1. ARCHITECTURE: Is the file structure logical? Are responsibilities separated?
|
|
2. SECURITY: SQL injection risks? Input validation? Authentication?
|
|
3. RELIABILITY: Error handling? Database connection management? Edge cases?
|
|
4. MAINTAINABILITY: Consistent naming? Clear code structure? Would a new developer understand this?
|
|
|
|
OUTPUT FORMAT:
|
|
- RISK: [critical/high/medium/low] Description
|
|
- List max 3-5 most important findings
|
|
- End with overall assessment: "SHIP IT" or "NEEDS WORK: reason"
|
|
|
|
Write a project report in clean markdown for: Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
|
|
|
FIRST LINE must be exactly one of:
|
|
VERDICT: GREEN
|
|
VERDICT: ORANGE
|
|
VERDICT: RED
|
|
|
|
Then write this report:
|
|
|
|
# Todo-sovellus FastAPI + SQLite, CRUD-endpointit ja testit
|
|
|
|
## Overview
|
|
One paragraph describing what this project does.
|
|
|
|
## Files
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| models.py | ... |
|
|
| schemas.py | ... |
|
|
| main.py | ... |
|
|
| pyproject.toml | ... |
|
|
| test_main.py | ... |
|
|
| Dockerfile | ... |
|
|
|
|
## Quick Start
|
|
```bash
|
|
git clone <repo>
|
|
cd project
|
|
uv sync
|
|
uv run uvicorn main:app --reload
|
|
```
|
|
|
|
## Docker
|
|
```bash
|
|
docker build -t todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit .
|
|
docker run -p 8000:8000 todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit
|
|
```
|
|
|
|
## API Endpoints
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| POST | /items/ | Create |
|
|
| GET | /items/ | List all |
|
|
| GET | /items/{id} | Get by ID |
|
|
| PUT | /items/{id} | Update |
|
|
| DELETE | /items/{id} | Delete |
|
|
(Adapt paths and descriptions to match the actual code)
|
|
|
|
## Architecture
|
|
Describe the project structure and design decisions.
|
|
|
|
## Risk Assessment
|
|
| Severity | Issue |
|
|
|----------|-------|
|
|
| ... | ... |
|
|
|
|
Project code:
|
|
--- models.py ---
|
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
DATABASE_URL = "sqlite:///./todo.db"
|
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
Base = declarative_base()
|
|
|
|
class Todo(Base):
|
|
__tablename__ = "todos"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
title = Column(String(100), nullable=False)
|
|
description = Column(String(500))
|
|
due_date = Column(Date, nullable=False)
|
|
status = Column(Enum('pending', 'completed'), default='pending')
|
|
|
|
--- schemas.py ---
|
|
from pydantic import BaseModel
|
|
|
|
class TodoCreate(BaseModel):
|
|
title: str
|
|
description: str | None = None
|
|
due_date: date
|
|
status: str = 'pending'
|
|
|
|
class TodoResponse(TodoCreate):
|
|
id: int
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
--- main.py ---
|
|
from fastapi import FastAPI, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from models import Base, engine, SessionLocal, Todo
|
|
from schemas import TodoCreate, TodoResponse
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
app = FastAPI()
|
|
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
@app.post("/todos/", response_model=TodoResponse, status_code=201)
|
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
|
db_todo = Todo(**todo.model_dump())
|
|
db.add(db_todo)
|
|
db.commit()
|
|
db.refresh(db_todo)
|
|
return db_todo
|
|
|
|
@app.get("/todos/", response_model=list[TodoResponse])
|
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
|
if status:
|
|
query = db.query(Todo).filter_by(status=status)
|
|
else:
|
|
query = db.query(Todo)
|
|
return query.all()
|
|
|
|
@app.get("/todos/{todo_id}", response_model=TodoResponse)
|
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
if not todo:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
return todo
|
|
|
|
@app.put("/todos/{todo_id}", response_model=TodoResponse)
|
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
if not db_todo:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
for key, value in todo.model_dump().items():
|
|
setattr(db_todo, key, value)
|
|
db.commit()
|
|
db.refresh(db_todo)
|
|
return db_todo
|
|
|
|
@app.delete("/todos/{todo_id}", status_code=204)
|
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
if not db_todo:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
db.delete(db_todo)
|
|
db.commit()
|
|
|
|
--- pyproject.toml ---
|
|
[project]
|
|
name = "todo-sovellus"
|
|
version = "0.1.0"
|
|
requires-python = ">=3.11"
|
|
dependencies = [
|
|
"fastapi",
|
|
"uvicorn[standard]",
|
|
"sqlalchemy",
|
|
]
|
|
|
|
[project.scripts]
|
|
dev = "uvicorn main:app --reload"
|
|
|
|
--- test_main.py ---
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy import create_engine, Column, Integer, String, Date, Enum
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
DATABASE_URL = "sqlite:///./test_todo.db"
|
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
|
Base = declarative_base()
|
|
|
|
class Todo(Base):
|
|
__tablename__ = "todos"
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
title = Column(String(100), nullable=False)
|
|
description = Column(String(500))
|
|
due_date = Column(Date, nullable=False)
|
|
status = Column(Enum('pending', 'completed'), default='pending')
|
|
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
app = FastAPI()
|
|
|
|
@app.post("/todos/", response_model=Todo, status_code=201)
|
|
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
|
|
db_todo = Todo(**todo.model_dump())
|
|
db.add(db_todo)
|
|
db.commit()
|
|
db.refresh(db_todo)
|
|
return db_todo
|
|
|
|
@app.get("/todos/", response_model=list[Todo])
|
|
def list_todos(status: str | None = None, db: Session = Depends(get_db)):
|
|
if status:
|
|
query = db.query(Todo).filter_by(status=status)
|
|
else:
|
|
query = db.query(Todo)
|
|
return query.all()
|
|
|
|
@app.get("/todos/{todo_id}", response_model=Todo)
|
|
def get_todo(todo_id: int, db: Session = Depends(get_db)):
|
|
todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
if not todo:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
return todo
|
|
|
|
@app.put("/todos/{todo_id}", response_model=Todo)
|
|
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
|
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
if not db_todo:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
for key, value in todo.model_dump().items():
|
|
setattr(db_todo, key, value)
|
|
db.commit()
|
|
db.refresh(db_todo)
|
|
return db_todo
|
|
|
|
@app.delete("/todos/{todo_id}", status_code=204)
|
|
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
|
|
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
|
|
if not db_todo:
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
db.delete(db_todo)
|
|
db.commit()
|
|
|
|
client = TestClient(app)
|
|
|
|
def test_create_todo():
|
|
response = client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023-12-31"})
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert data["title"] == "Test Todo"
|
|
assert data["description"] == "This is a test todo."
|
|
assert data["due_date"] == "2023-12-31"
|
|
assert data["status"] == "pending"
|
|
|
|
def test_list_todos():
|
|
response = client.get("/todos/")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data) > 0
|
|
|
|
def test_get_todo_by_id():
|
|
todo_id = create_test_todo().json()["id"]
|
|
response = client.get(f"/todos/{todo_id}")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert data["title"] == "Test Todo"
|
|
|
|
def test_get_nonexistent_todo():
|
|
response = client.get("/todos/999")
|
|
assert response.status_code == 404
|
|
|
|
def test_update_todo():
|
|
todo_id = create_test_todo().json()["id"]
|
|
response = client.put(f"/todos/{todo_id}", json={"title": "Updated Todo"})
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "id" in data
|
|
assert data["title"] == "Updated Todo"
|
|
|
|
def test_delete_todo():
|
|
todo_id = create_test_todo().json()["id"]
|
|
response = client.delete(f"/todos/{todo_id}")
|
|
assert response.status_code == 204
|
|
|
|
def create_test_todo():
|
|
return client.post("/todos/", json={"title": "Test Todo", "description": "This is a test todo.", "due_date": "2023
|
|
|
|
--- Dockerfile ---
|
|
# Stage 1: Build
|
|
FROM python:3.12-slim as builder
|
|
|
|
WORKDIR /app
|
|
|
|
COPY pyproject.toml .
|
|
RUN pip install --no-cache-dir poetry && \
|
|
poetry config virtualenvs.create false && \
|
|
poetry install --no-interaction --no-ansi
|
|
|
|
COPY models.py schemas.py main.py test_main.py .
|
|
|
|
# Stage 2: Production
|
|
FROM python:3.12-slim
|
|
|
|
WORKDIR /app
|
|
|
|
COPY --from=builder /root/.local/share/python-poetry/cache /root/.local/share/python-poetry/cache
|
|
COPY --from=builder /app /app
|
|
|
|
RUN useradd -m appuser && \
|
|
chown -R appuser:appuser /app
|
|
|
|
USER appuser
|
|
|
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
|
|
|
|
EXPOSE 8000
|
|
|
|
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
```
|
|
|
|
## Tulos
|
|
|
|
```
|
|
VERDICT: GREEN
|
|
|
|
## Overview
|
|
This project is a simple Todo application using FastAPI for the backend and SQLite as the database. It includes CRUD endpoints for managing tasks.
|
|
|
|
## Files
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| models.py | Defines the data model for the todo items using SQLAlchemy ORM. |
|
|
| schemas.py | Contains Pydantic schemas for validating request and response data. |
|
|
| main.py | The FastAPI application, defining routes and business logic. |
|
|
| pyproject.toml | Project metadata and dependencies. |
|
|
| test_main.py | Unit tests for the FastAPI endpoints. |
|
|
| Dockerfile | Docker configuration to build and run the application. |
|
|
|
|
## Quick Start
|
|
git clone <repo>
|
|
cd project
|
|
uv sync
|
|
uv run uvicorn main:app --reload
|
|
|
|
## Docker
|
|
docker build -t todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit .
|
|
docker run -p 8000:8000 todo-sovellus-fastapi---sqlite--crud-endpointit-ja-testit
|
|
|
|
## API Endpoints
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| POST | /todos/ | Create a new todo item. |
|
|
| GET | /todos/ | List all todos or filter by status. |
|
|
| GET | /todos/{id} | Retrieve a specific todo item by ID. |
|
|
| PUT | /todos/{id} | Update an existing todo item. |
|
|
| DELETE | /todos/{id} | Delete a todo item. |
|
|
|
|
## Architecture
|
|
The project follows a clean separation of concerns:
|
|
- **models.py**: Defines the data model using SQLAlchemy ORM.
|
|
- **schemas.py**: Contains Pydantic models for data validation and serialization.
|
|
- **main.py**: Implements FastAPI routes, business logic, and database interactions.
|
|
|
|
This logical structure makes it easy to understand and maintain each component independently.
|
|
|
|
## Risk Assessment
|
|
| Severity | Issue |
|
|
|----------|-------|
|
|
| LOW | The project uses parameterized queries in the SQLAlchemy ORM which helps prevent SQL injection. However, input validation is minimal (e.g., no checks for `due_date` format). |
|
|
| MEDIUM | Error handling and database connection management are basic but adequate. Database connections are managed via context managers, which ensures proper closure of sessions. |
|
|
| LOW | The project uses consistent naming conventions for variables and functions, making the code easy to understand. However, there's room for improving documentation around API endpoints and models. |
|
|
|
|
Overall assessment: "SHIP IT"
|
|
```
|