Pipelinen parannuksia building blockeilla

This commit is contained in:
Jaakko Vanhala
2026-04-12 18:48:14 +03:00
parent c1a5f8aff5
commit b2ee8b9031
175 changed files with 13311 additions and 237 deletions

View File

@@ -0,0 +1,11 @@
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
ENV UV_CACHE_DIR=/tmp/uv-cache
WORKDIR /app
COPY pyproject.toml .
RUN uv sync
COPY *.py .
RUN useradd -m appuser
USER appuser
EXPOSE 8000
CMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,74 @@
from fastapi import FastAPI, HTTPException, Depends, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from pydantic.v1 import model_dump
from models import Task, User, Base, engine
from schemas import TaskCreate, TaskResponse
from sqlalchemy.exc import SQLAlchemyError
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/tasks/", response_model=TaskResponse, status_code=201)
async def create_task(task: TaskCreate, db: Session = Depends(get_db)):
db_task = Task(**model_dump(task))
db.add(db_task)
try:
db.commit()
db.refresh(db_task)
except SQLAlchemyError as e:
raise HTTPException(status_code=400, detail=str(e))
return db_task
@app.get("/tasks/", response_model=List[TaskResponse])
async def read_tasks(
status: Optional[str] = Query(None),
db: Session = Depends(get_db)
):
query = db.query(Task)
if status:
query = query.filter(Task.status == status)
tasks = query.all()
return tasks
@app.get("/tasks/{task_id}", response_model=TaskResponse)
async def read_task(task_id: int, db: Session = Depends(get_db)):
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
@app.put("/tasks/{task_id}", response_model=TaskResponse)
async def update_task(task_id: int, task: TaskCreate, db: Session = Depends(get_db)):
db_task = db.query(Task).filter(Task.id == task_id).first()
if not db_task:
raise HTTPException(status_code=404, detail="Task not found")
for key, value in model_dump(task).items():
setattr(db_task, key, value)
try:
db.commit()
db.refresh(db_task)
except SQLAlchemyError as e:
raise HTTPException(status_code=400, detail=str(e))
return db_task
@app.delete("/tasks/{task_id}", status_code=204)
async def delete_task(task_id: int, db: Session = Depends(get_db)):
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
try:
db.delete(task)
db.commit()
except SQLAlchemyError as e:
raise HTTPException(status_code=400, detail=str(e))

View File

@@ -0,0 +1,25 @@
from sqlalchemy import create_engine, Column, Integer, String, Text, 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 Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
description = Column(Text, nullable=True)
due_date = Column(Date, nullable=True)
status = Column(Enum('pending', 'completed'), default='pending')
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(255), unique=True, nullable=False)
email = Column(String(255), unique=True, nullable=False)
password_hash = Column(String(255), nullable=False)
Base.metadata.create_all(bind=engine)

View File

@@ -0,0 +1,13 @@
[project]
name = "todo-app"
version = "0.1.0"
description = "A simple task management API"
authors = [
{ name="Your Name", email="your.email@example.com" }
]
dependencies = [
"fastapi",
"uvicorn[standard]",
"sqlalchemy"
]

View File

@@ -0,0 +1,163 @@
{
"run_id": "v5_feedback",
"steps": [
{
"step": "requirements",
"agent": "client",
"attempts": [
{
"attempt": 1,
"elapsed": 9.1,
"errors": [],
"code_length": 1560
}
],
"final_errors": [],
"passed": true
},
{
"step": "models.py",
"agent": "data",
"attempts": [
{
"attempt": 1,
"elapsed": 7.5,
"errors": [
"SQLite create_engine puuttuu connect_args={'check_same_thread': False}"
],
"code_length": 982
},
{
"attempt": 2,
"elapsed": 7.6,
"errors": [],
"code_length": 1025
}
],
"final_errors": [],
"passed": true
},
{
"step": "schemas.py",
"agent": "coder",
"attempts": [
{
"attempt": 1,
"elapsed": 6.2,
"errors": [
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
],
"code_length": 494
},
{
"attempt": 2,
"elapsed": 4.6,
"errors": [
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
],
"code_length": 494
},
{
"attempt": 3,
"elapsed": 4.0,
"errors": [
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
],
"code_length": 494
}
],
"final_errors": [
"Schema puuttuu kenttiä jotka ovat modelissa: {'password_hash', 'email', 'username'}"
],
"passed": false
},
{
"step": "main.py",
"agent": "coder",
"attempts": [
{
"attempt": 1,
"elapsed": 18.9,
"errors": [
"Kutsutaan 'create_engine()' mutta sitä ei ole importattu eikä määritelty",
"Kutsutaan 'sessionmaker()' mutta sitä ei ole importattu eikä määritelty"
],
"code_length": 2531
},
{
"attempt": 2,
"elapsed": 18.7,
"errors": [
"Kutsutaan 'SessionLocal()' mutta sitä ei ole importattu eikä määritelty"
],
"code_length": 2337
},
{
"attempt": 3,
"elapsed": 18.4,
"errors": [
"Kutsutaan 'SessionLocal()' mutta sitä ei ole importattu eikä määritelty"
],
"code_length": 2337
}
],
"final_errors": [
"Kutsutaan 'SessionLocal()' mutta sitä ei ole importattu eikä määritelty"
],
"passed": false
},
{
"step": "pyproject.toml",
"agent": "coder",
"attempts": [
{
"attempt": 1,
"elapsed": 7.4,
"errors": [
"Sisältää poetry-konfiguraation — käytä VAIN [project] (PEP 621) + uv",
"build-backend käyttää poetryä — poista tai vaihda"
],
"code_length": 647
},
{
"attempt": 2,
"elapsed": 2.9,
"errors": [],
"code_length": 238
}
],
"final_errors": [],
"passed": true
},
{
"step": "test_main.py",
"agent": "qa",
"attempts": [
{
"attempt": 1,
"elapsed": 14.1,
"errors": [],
"code_length": 1567
}
],
"final_errors": [],
"passed": true
},
{
"step": "Dockerfile",
"agent": "tester",
"attempts": [
{
"attempt": 1,
"elapsed": 4.1,
"errors": [],
"code_length": 291
}
],
"final_errors": [],
"passed": true
}
],
"model": "qwen2.5-coder:7b-instruct-q4_K_M",
"summary": "5/7 passed"
}

View File

@@ -0,0 +1,40 @@
**PROJECT NAME:** Todo-sovellus
**GOAL:** Create a simple task management application for individuals to keep track of their daily tasks and manage them efficiently.
**CORE FEATURES:**
1. Add new tasks with title, description, due date, and status.
2. View all tasks with filtering by status (e.g., pending, completed).
3. Update existing tasks including changing the title, description, due date, and status.
4. Delete tasks that are no longer needed.
5. Mark tasks as completed or pending.
6. Search for tasks by keywords in the title or description.
7. Export task list to a CSV file.
**DATA MODEL:**
1. **Task**
- id (integer, primary key)
- title (string, required)
- description (text, optional)
- due_date (date, optional)
- status (enum ['pending', 'completed'], default 'pending')
2. **User**
- id (integer, primary key)
- username (string, unique, required)
- email (string, unique, required)
- password_hash (string, required)
**API ENDPOINTS:**
1. **GET /tasks** - Retrieve all tasks with optional filters by status.
2. **POST /tasks** - Create a new task.
3. **GET /tasks/{task_id}** - Retrieve a specific task by ID.
4. **PUT /tasks/{task_id}** - Update an existing task.
5. **DELETE /tasks/{task_id}** - Delete a task.
6. **POST /users/register** - Register a new user.
7. **POST /users/login** - Authenticate and return a token for authorized access.
**CONSTRAINTS:**
- Must use SQLite as the database.
- No authentication is required for accessing CRUD operations.
- Implement unit tests for all API endpoints.

View File

@@ -0,0 +1,18 @@
from pydantic import BaseModel, Field
from datetime import date
class TaskCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=255)
description: str | None = Field(None, max_length=255)
due_date: date | None = Field(None)
status: str = Field('pending', regex='^(pending|completed)$')
class TaskResponse(BaseModel):
id: int
title: str
description: str | None
due_date: date | None
status: str
class Config:
from_attributes = True

View File

@@ -0,0 +1,51 @@
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from main import app
from models import Base
engine = create_engine("sqlite:///:memory:", connect_args={"check_same_thread": False})
TestSession = sessionmaker(bind=engine)
Base.metadata.create_all(bind=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_task():
response = client.post("/tasks/", json={"title": "Test Task", "description": "This is a test task"})
assert response.status_code == 201
assert response.json()["title"] == "Test Task"
assert response.json()["description"] == "This is a test task"
def test_read_tasks():
response = client.get("/tasks/")
assert response.status_code == 200
assert len(response.json()) == 1
def test_get_task_by_id():
response = client.get("/tasks/1")
assert response.status_code == 200
assert response.json()["id"] == 1
def test_get_task_by_id_not_found():
response = client.get("/tasks/999")
assert response.status_code == 404
def test_update_task():
response = client.put("/tasks/1", json={"title": "Updated Task"})
assert response.status_code == 200
assert response.json()["title"] == "Updated Task"
def test_delete_task():
response = client.delete("/tasks/1")
assert response.status_code == 204
response = client.get("/tasks/1")
assert response.status_code == 404