Enhance Todo application with advanced features

- Add due dates and priority level to todos
- Add tags/categories for better organization
- Implement advanced search and filtering
- Create database migrations for model changes
- Add endpoints for managing tags
- Update documentation

generated with BackendIM... (backend.im)
This commit is contained in:
Automated Action 2025-05-12 23:29:41 +00:00
parent 959ebdf9a5
commit 8557c1426e
6 changed files with 330 additions and 24 deletions

View File

@ -1,10 +1,15 @@
# Todo API Application
# Enhanced Todo API Application
This is a simple Todo API application built with FastAPI and SQLite.
This is an improved Todo API application built with FastAPI and SQLite, featuring advanced task management capabilities.
## Features
- Create, Read, Update, and Delete todos
- Todo prioritization (low, medium, high)
- Due dates for deadline tracking
- Tag/category support for organizing todos
- Advanced search and filtering capabilities
- Upcoming tasks view for planning
- Health endpoint for monitoring application status
- SQLite database for data persistence
- Alembic for database migrations
@ -20,7 +25,9 @@ simpletodoapplication/
│ └── schemas.py
├── migrations/
│ ├── versions/
│ │ └── 001_create_todos_table.py
│ │ ├── 001_create_todos_table.py
│ │ ├── 002_add_priority_and_due_date.py
│ │ └── 003_add_tags_table_and_associations.py
│ ├── env.py
│ ├── README
│ └── script.py.mako
@ -31,13 +38,35 @@ simpletodoapplication/
## API Endpoints
### Health
- `GET /health` - Check API health
- `GET /todos` - Get all todos
### Todo Management
- `GET /todos` - Get all todos with optional filtering
- `POST /todos` - Create a new todo
- `POST /todos/with-tags` - Create a new todo with tags (creates tags if they don't exist)
- `GET /todos/{todo_id}` - Get a specific todo
- `PUT /todos/{todo_id}` - Update a todo
- `DELETE /todos/{todo_id}` - Delete a todo
### Search & Filtering
- `GET /todos/search` - Search todos by title, description, or tag
- `GET /todos/upcoming` - Get todos due in the next N days (defaults to 7)
- `GET /todos` - With query parameters for advanced filtering:
- `title` - Filter by title (partial match)
- `description` - Filter by description (partial match)
- `completed` - Filter by completion status
- `priority` - Filter by priority level
- `tag` - Filter by tag name
- `due_before` - Filter todos due before a specific date
- `due_after` - Filter todos due after a specific date
- `overdue` - Filter overdue todos
### Tag Management
- `GET /tags` - Get all tags
- `POST /tags` - Create a new tag
- `GET /tags/{tag_id}` - Get a specific tag
## Requirements
- Python 3.8+

View File

@ -1,8 +1,33 @@
from sqlalchemy import Boolean, Column, Integer, String, Text, DateTime
from sqlalchemy import Boolean, Column, Integer, String, Text, DateTime, Enum, Table, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
import enum
from .database import Base
# Association table for the many-to-many relationship between Todo and Tag
todo_tag = Table(
"todo_tag",
Base.metadata,
Column("todo_id", Integer, ForeignKey("todos.id"), primary_key=True),
Column("tag_id", Integer, ForeignKey("tags.id"), primary_key=True)
)
class PriorityLevel(str, enum.Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(50), nullable=False, unique=True, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Relationship to todos
todos = relationship("Todo", secondary=todo_tag, back_populates="tags")
class Todo(Base):
__tablename__ = "todos"
@ -10,5 +35,10 @@ class Todo(Base):
title = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
completed = Column(Boolean, default=False)
priority = Column(Enum(PriorityLevel), default=PriorityLevel.MEDIUM)
due_date = Column(DateTime(timezone=True), nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationship to tags
tags = relationship("Tag", secondary=todo_tag, back_populates="todos")

View File

@ -1,19 +1,52 @@
from pydantic import BaseModel, Field
from typing import Optional
from typing import Optional, List
from datetime import datetime
from enum import Enum
class PriorityLevel(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class TagBase(BaseModel):
name: str = Field(..., min_length=1, max_length=50)
class TagCreate(TagBase):
pass
class TagResponse(TagBase):
id: int
created_at: datetime
class Config:
from_attributes = True
class TodoBase(BaseModel):
title: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
completed: bool = False
priority: PriorityLevel = PriorityLevel.MEDIUM
due_date: Optional[datetime] = None
class TodoCreate(TodoBase):
pass
tag_ids: Optional[List[int]] = []
class TodoCreateWithTags(TodoBase):
tags: Optional[List[str]] = []
class TodoUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=100)
description: Optional[str] = None
completed: Optional[bool] = None
priority: Optional[PriorityLevel] = None
due_date: Optional[datetime] = None
tag_ids: Optional[List[int]] = None
class TodoResponse(TodoBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
tags: List[TagResponse] = []
class Config:
from_attributes = True

171
main.py
View File

@ -1,17 +1,25 @@
from fastapi import FastAPI, Depends, HTTPException
from fastapi import FastAPI, Depends, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import or_, and_, func
from datetime import datetime, timedelta
from app.database import get_db
from app.models import Todo
from app.schemas import TodoCreate, TodoResponse, HealthResponse
from app.database import get_db, engine
from app.models import Todo, Tag, Base
from app.schemas import (
TodoCreate, TodoCreateWithTags, TodoUpdate, TodoResponse,
TagCreate, TagResponse, HealthResponse
)
# Create tables if they don't exist
Base.metadata.create_all(bind=engine)
app = FastAPI(
title="Todo API",
description="A simple API for managing todos",
version="0.1.0",
title="Enhanced Todo API",
description="An improved API for managing todos with tags, priority levels, due dates, and search functionality",
version="0.2.0",
)
app.add_middleware(
@ -26,18 +34,136 @@ app.add_middleware(
def health():
return {"status": "healthy"}
# Tag endpoints
@app.post("/tags/", response_model=TagResponse)
def create_tag(tag: TagCreate, db: Session = Depends(get_db)):
# Check if tag already exists
existing_tag = db.query(Tag).filter(Tag.name == tag.name).first()
if existing_tag:
return existing_tag
db_tag = Tag(**tag.model_dump())
db.add(db_tag)
db.commit()
db.refresh(db_tag)
return db_tag
@app.get("/tags/", response_model=List[TagResponse])
def get_tags(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
return db.query(Tag).offset(skip).limit(limit).all()
@app.get("/tags/{tag_id}", response_model=TagResponse)
def get_tag(tag_id: int, db: Session = Depends(get_db)):
db_tag = db.query(Tag).filter(Tag.id == tag_id).first()
if db_tag is None:
raise HTTPException(status_code=404, detail="Tag not found")
return db_tag
# Todo endpoints
@app.post("/todos/", response_model=TodoResponse)
def create_todo(todo: TodoCreate, db: Session = Depends(get_db)):
db_todo = Todo(**todo.model_dump())
todo_data = todo.model_dump(exclude={"tag_ids"})
db_todo = Todo(**todo_data)
# Add tags if provided
if todo.tag_ids:
for tag_id in todo.tag_ids:
tag = db.query(Tag).filter(Tag.id == tag_id).first()
if tag is None:
raise HTTPException(status_code=404, detail=f"Tag with id {tag_id} not found")
db_todo.tags.append(tag)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
@app.post("/todos/with-tags/", response_model=TodoResponse)
def create_todo_with_tags(todo: TodoCreateWithTags, db: Session = Depends(get_db)):
# Extract and create todo
todo_data = todo.model_dump(exclude={"tags"})
db_todo = Todo(**todo_data)
# Process tags
if todo.tags:
for tag_name in todo.tags:
# Find existing tag or create new one
tag = db.query(Tag).filter(Tag.name == tag_name).first()
if not tag:
tag = Tag(name=tag_name)
db.add(tag)
db.flush()
db_todo.tags.append(tag)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
@app.get("/todos/", response_model=List[TodoResponse])
def get_todos(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
todos = db.query(Todo).offset(skip).limit(limit).all()
return todos
def get_todos(
skip: int = 0,
limit: int = 100,
title: Optional[str] = None,
description: Optional[str] = None,
completed: Optional[bool] = None,
priority: Optional[str] = None,
tag: Optional[str] = None,
due_before: Optional[datetime] = None,
due_after: Optional[datetime] = None,
overdue: Optional[bool] = None,
db: Session = Depends(get_db)
):
query = db.query(Todo)
# Apply filters if provided
if title:
query = query.filter(Todo.title.ilike(f"%{title}%"))
if description:
query = query.filter(Todo.description.ilike(f"%{description}%"))
if completed is not None:
query = query.filter(Todo.completed == completed)
if priority:
query = query.filter(Todo.priority == priority)
if tag:
query = query.join(Todo.tags).filter(Tag.name == tag)
if due_before:
query = query.filter(Todo.due_date <= due_before)
if due_after:
query = query.filter(Todo.due_date >= due_after)
if overdue is not None and overdue:
query = query.filter(and_(
Todo.due_date < datetime.now(),
Todo.completed == False
))
return query.offset(skip).limit(limit).all()
@app.get("/todos/search/", response_model=List[TodoResponse])
def search_todos(
q: str = Query(..., min_length=1, description="Search query"),
db: Session = Depends(get_db)
):
"""Search todos by title, description or tag name"""
return db.query(Todo).filter(
or_(
Todo.title.ilike(f"%{q}%"),
Todo.description.ilike(f"%{q}%"),
Todo.tags.any(Tag.name.ilike(f"%{q}%"))
)
).all()
@app.get("/todos/upcoming/", response_model=List[TodoResponse])
def get_upcoming_todos(days: int = 7, db: Session = Depends(get_db)):
"""Get todos with due dates in the next N days"""
future_date = datetime.now() + timedelta(days=days)
return db.query(Todo).filter(
and_(
Todo.due_date <= future_date,
Todo.due_date >= datetime.now(),
Todo.completed == False
)
).order_by(Todo.due_date).all()
@app.get("/todos/{todo_id}", response_model=TodoResponse)
def get_todo(todo_id: int, db: Session = Depends(get_db)):
@ -47,14 +173,27 @@ def get_todo(todo_id: int, db: Session = Depends(get_db)):
return db_todo
@app.put("/todos/{todo_id}", response_model=TodoResponse)
def update_todo(todo_id: int, todo: TodoCreate, db: Session = Depends(get_db)):
def update_todo(todo_id: int, todo: TodoUpdate, db: Session = Depends(get_db)):
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
for key, value in todo.model_dump().items():
# Update todo attributes
todo_data = todo.model_dump(exclude={"tag_ids"}, exclude_unset=True)
for key, value in todo_data.items():
setattr(db_todo, key, value)
# Update tags if provided
if todo.tag_ids is not None:
# Clear existing tags
db_todo.tags = []
# Add new tags
for tag_id in todo.tag_ids:
tag = db.query(Tag).filter(Tag.id == tag_id).first()
if tag is None:
raise HTTPException(status_code=404, detail=f"Tag with id {tag_id} not found")
db_todo.tags.append(tag)
db.commit()
db.refresh(db_todo)
return db_todo
@ -64,7 +203,7 @@ def delete_todo(todo_id: int, db: Session = Depends(get_db)):
db_todo = db.query(Todo).filter(Todo.id == todo_id).first()
if db_todo is None:
raise HTTPException(status_code=404, detail="Todo not found")
db.delete(db_todo)
db.commit()
return db_todo

View File

@ -0,0 +1,30 @@
"""add priority and due date to todos
Revision ID: 002
Revises: 001
Create Date: 2023-11-21
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = '002'
down_revision = '001'
branch_labels = None
depends_on = None
def upgrade():
# Add priority column to todos table
with op.batch_alter_table('todos') as batch_op:
batch_op.add_column(sa.Column('priority', sa.String(length=10), nullable=False, server_default='medium'))
batch_op.add_column(sa.Column('due_date', sa.DateTime(timezone=True), nullable=True))
def downgrade():
# Remove columns
with op.batch_alter_table('todos') as batch_op:
batch_op.drop_column('due_date')
batch_op.drop_column('priority')

View File

@ -0,0 +1,45 @@
"""add tags table and associations
Revision ID: 003
Revises: 002
Create Date: 2023-11-22
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '003'
down_revision = '002'
branch_labels = None
depends_on = None
def upgrade():
# Create tags table
op.create_table('tags',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=50), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)')),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tags_id'), 'tags', ['id'], unique=False)
op.create_index(op.f('ix_tags_name'), 'tags', ['name'], unique=True)
# Create association table
op.create_table('todo_tag',
sa.Column('todo_id', sa.Integer(), nullable=False),
sa.Column('tag_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
sa.ForeignKeyConstraint(['todo_id'], ['todos.id'], ),
sa.PrimaryKeyConstraint('todo_id', 'tag_id')
)
def downgrade():
# Drop tables
op.drop_table('todo_tag')
op.drop_index(op.f('ix_tags_name'), table_name='tags')
op.drop_index(op.f('ix_tags_id'), table_name='tags')
op.drop_table('tags')