Agent turn complete
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
"""Database configuration and session management."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker
|
||||
|
||||
# Ensure data directory exists
|
||||
data_dir = Path("./data")
|
||||
data_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Use SQLite database
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./data/app.db")
|
||||
|
||||
# Create engine with SQLite-specific settings
|
||||
engine = create_engine(
|
||||
DATABASE_URL,
|
||||
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {},
|
||||
echo=False,
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Dependency for getting database session."""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize database tables."""
|
||||
Base.metadata.create_all(bind=engine)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""FastAPI entry point. Deploy imports ``app.main:app`` — keep both names."""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.routes import api_router
|
||||
from app.database import init_db
|
||||
|
||||
app = FastAPI(title=os.getenv("APP_NAME", "app"))
|
||||
|
||||
# Initialize database tables on startup
|
||||
init_db()
|
||||
|
||||
# Allowed origins come from the environment so the deployed app and local dev
|
||||
# use the same code path. "*" only when nothing is configured (dev default).
|
||||
_origins = [o.strip() for o in os.getenv("CORS_ORIGINS", "*").split(",") if o.strip()]
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# The one place the /api prefix is applied. Route modules must not repeat it.
|
||||
app.include_router(api_router, prefix="/api")
|
||||
@@ -0,0 +1,23 @@
|
||||
"""SQLAlchemy models for the kanban board."""
|
||||
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Float
|
||||
from app.database import Base
|
||||
|
||||
|
||||
class Task(Base):
|
||||
"""Task model for kanban board."""
|
||||
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(255), nullable=False, index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
priority = Column(String(50), nullable=False, default="medium") # low, medium, high, urgent
|
||||
status = Column(String(50), nullable=False, default="TODO", index=True) # TODO, IN PROGRESS, QA, UAT, PROD
|
||||
assignee = Column(String(255), nullable=True)
|
||||
tags = Column(String(500), nullable=True) # Comma-separated tags
|
||||
time_estimate = Column(Float, nullable=True) # Hours
|
||||
due_date = Column(DateTime, nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Route barrel — every router the app serves is mounted here.
|
||||
|
||||
Adding a resource is two steps: create ``app/routes/<name>.py`` with a
|
||||
module-level ``router``, then append one ``include_router`` line below. A route
|
||||
file that is never mounted 404s silently.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.routes import health, items, tasks
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
api_router.include_router(health.router, tags=["health"])
|
||||
api_router.include_router(items.router, tags=["items"])
|
||||
api_router.include_router(tasks.router)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Liveness endpoint. The shape every other route module copies."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class Health(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
@router.get("/health", response_model=Health)
|
||||
def health() -> Health:
|
||||
return Health(status="ok")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""One demo resource. Its rows mirror the frontend seed, so Preview and the
|
||||
deployed app render the same screen. Replace both together."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class Item(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
status: str
|
||||
|
||||
|
||||
@router.get("/items", response_model=list[Item])
|
||||
def list_items() -> list[Item]:
|
||||
return [
|
||||
Item(id=1, name="Draft the launch checklist", status="in progress"),
|
||||
Item(id=2, name="Review onboarding copy", status="todo"),
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Task management routes for the kanban board."""
|
||||
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import Task
|
||||
from app.schemas import TaskCreate, TaskUpdate, TaskResponse, TaskListResponse
|
||||
|
||||
router = APIRouter(prefix="/tasks", tags=["tasks"])
|
||||
|
||||
|
||||
@router.post("", response_model=TaskResponse)
|
||||
def create_task(task: TaskCreate, db: Session = Depends(get_db)):
|
||||
"""Create a new task."""
|
||||
db_task = Task(**task.model_dump())
|
||||
db.add(db_task)
|
||||
db.commit()
|
||||
db.refresh(db_task)
|
||||
return db_task
|
||||
|
||||
|
||||
@router.get("", response_model=TaskListResponse)
|
||||
def list_tasks(
|
||||
status: Optional[str] = Query(None),
|
||||
priority: Optional[str] = Query(None),
|
||||
tags: Optional[str] = Query(None),
|
||||
search: Optional[str] = Query(None),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""List tasks with optional filtering."""
|
||||
query = db.query(Task)
|
||||
|
||||
# Filter by status
|
||||
if status:
|
||||
query = query.filter(Task.status == status)
|
||||
|
||||
# Filter by priority
|
||||
if priority:
|
||||
query = query.filter(Task.priority == priority)
|
||||
|
||||
# Filter by tags (partial match)
|
||||
if tags:
|
||||
query = query.filter(Task.tags.ilike(f"%{tags}%"))
|
||||
|
||||
# Search in title and description
|
||||
if search:
|
||||
query = query.filter(
|
||||
(Task.title.ilike(f"%{search}%")) | (Task.description.ilike(f"%{search}%"))
|
||||
)
|
||||
|
||||
tasks = query.order_by(Task.created_at.desc()).all()
|
||||
return TaskListResponse(tasks=tasks, total=len(tasks))
|
||||
|
||||
|
||||
@router.get("/{task_id}", response_model=TaskResponse)
|
||||
def get_task(task_id: int, db: Session = Depends(get_db)):
|
||||
"""Get a specific task by ID."""
|
||||
task = db.query(Task).filter(Task.id == task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return task
|
||||
|
||||
|
||||
@router.put("/{task_id}", response_model=TaskResponse)
|
||||
def update_task(task_id: int, task_update: TaskUpdate, db: Session = Depends(get_db)):
|
||||
"""Update a task."""
|
||||
task = db.query(Task).filter(Task.id == task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
# Update only provided fields
|
||||
update_data = task_update.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(task, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
return task
|
||||
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
def delete_task(task_id: int, db: Session = Depends(get_db)):
|
||||
"""Delete a task."""
|
||||
task = db.query(Task).filter(Task.id == task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
return {"message": "Task deleted successfully"}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Pydantic schemas for request/response validation."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TaskBase(BaseModel):
|
||||
"""Base task schema with common fields."""
|
||||
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
priority: str = "medium"
|
||||
status: str = "TODO"
|
||||
assignee: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
time_estimate: Optional[float] = None
|
||||
due_date: Optional[datetime] = None
|
||||
|
||||
|
||||
class TaskCreate(TaskBase):
|
||||
"""Schema for creating a task."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TaskUpdate(BaseModel):
|
||||
"""Schema for updating a task."""
|
||||
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
priority: Optional[str] = None
|
||||
status: Optional[str] = None
|
||||
assignee: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
time_estimate: Optional[float] = None
|
||||
due_date: Optional[datetime] = None
|
||||
|
||||
|
||||
class TaskResponse(TaskBase):
|
||||
"""Schema for task response."""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TaskListResponse(BaseModel):
|
||||
"""Schema for task list response."""
|
||||
|
||||
tasks: List[TaskResponse]
|
||||
total: int
|
||||
@@ -0,0 +1,6 @@
|
||||
# Exact pins — this file is the version contract for the backend.
|
||||
fastapi==0.115.6
|
||||
uvicorn[standard]==0.34.0
|
||||
pydantic==2.10.4
|
||||
sqlalchemy==2.0.36
|
||||
|
||||
Reference in New Issue
Block a user