Add Task Manager API functionality with CRUD operations
This commit is contained in:
parent
a813887960
commit
b08b228767
30
README.md
30
README.md
@ -1,13 +1,14 @@
|
|||||||
# Generic REST API Service
|
# Task Manager API
|
||||||
|
|
||||||
A modern REST API service built with FastAPI and SQLite for handling item resources.
|
A modern REST API service built with FastAPI and SQLite for task management and item resources.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- FastAPI-based REST API with automatic OpenAPI documentation
|
- FastAPI-based REST API with automatic OpenAPI documentation
|
||||||
- SQLAlchemy ORM with SQLite database
|
- SQLAlchemy ORM with SQLite database
|
||||||
- Alembic database migrations
|
- Alembic database migrations
|
||||||
- CRUD operations for item resources
|
- CRUD operations for task and item resources
|
||||||
|
- Task management with status, priority, and due dates
|
||||||
- Health check endpoint
|
- Health check endpoint
|
||||||
- Async-ready with uvicorn
|
- Async-ready with uvicorn
|
||||||
|
|
||||||
@ -20,22 +21,26 @@ A modern REST API service built with FastAPI and SQLite for handling item resour
|
|||||||
│ ├── api/
|
│ ├── api/
|
||||||
│ │ ├── api.py
|
│ │ ├── api.py
|
||||||
│ │ └── endpoints/
|
│ │ └── endpoints/
|
||||||
│ │ └── items.py
|
│ │ ├── items.py
|
||||||
|
│ │ └── tasks.py
|
||||||
│ ├── core/
|
│ ├── core/
|
||||||
│ │ ├── config.py
|
│ │ ├── config.py
|
||||||
│ │ └── database.py
|
│ │ └── database.py
|
||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ │ ├── base.py
|
│ │ ├── base.py
|
||||||
│ │ └── item.py
|
│ │ ├── item.py
|
||||||
|
│ │ └── task.py
|
||||||
│ └── schemas/
|
│ └── schemas/
|
||||||
│ └── item.py
|
│ ├── item.py
|
||||||
|
│ └── task.py
|
||||||
├── main.py
|
├── main.py
|
||||||
├── migrations/
|
├── migrations/
|
||||||
│ ├── env.py
|
│ ├── env.py
|
||||||
│ ├── README
|
│ ├── README
|
||||||
│ ├── script.py.mako
|
│ ├── script.py.mako
|
||||||
│ └── versions/
|
│ └── versions/
|
||||||
│ └── 001_initial_migration.py
|
│ ├── 001_initial_migration.py
|
||||||
|
│ └── 002_add_task_table.py
|
||||||
└── requirements.txt
|
└── requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -87,13 +92,24 @@ FastAPI automatically generates interactive API documentation:
|
|||||||
|
|
||||||
## API Endpoints
|
## API Endpoints
|
||||||
|
|
||||||
|
### Health
|
||||||
- **GET /health** - Health check endpoint
|
- **GET /health** - Health check endpoint
|
||||||
|
|
||||||
|
### Items
|
||||||
- **GET /api/v1/items/** - List all items
|
- **GET /api/v1/items/** - List all items
|
||||||
- **POST /api/v1/items/** - Create a new item
|
- **POST /api/v1/items/** - Create a new item
|
||||||
- **GET /api/v1/items/{item_id}** - Get a specific item
|
- **GET /api/v1/items/{item_id}** - Get a specific item
|
||||||
- **PUT /api/v1/items/{item_id}** - Update a specific item
|
- **PUT /api/v1/items/{item_id}** - Update a specific item
|
||||||
- **DELETE /api/v1/items/{item_id}** - Delete a specific item
|
- **DELETE /api/v1/items/{item_id}** - Delete a specific item
|
||||||
|
|
||||||
|
### Tasks
|
||||||
|
- **GET /api/v1/tasks/** - List all tasks (with optional status filtering)
|
||||||
|
- **POST /api/v1/tasks/** - Create a new task
|
||||||
|
- **GET /api/v1/tasks/{task_id}** - Get a specific task
|
||||||
|
- **PUT /api/v1/tasks/{task_id}** - Update a specific task
|
||||||
|
- **DELETE /api/v1/tasks/{task_id}** - Delete a specific task
|
||||||
|
- **POST /api/v1/tasks/{task_id}/complete** - Mark a task as completed
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
### Database Migrations
|
### Database Migrations
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.endpoints import items
|
from app.api.endpoints import items, tasks
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(items.router, prefix="/items", tags=["items"])
|
api_router.include_router(items.router, prefix="/items", tags=["items"])
|
||||||
|
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
|
135
app/api/endpoints/tasks.py
Normal file
135
app/api/endpoints/tasks.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.models.task import Task, TaskStatus
|
||||||
|
from app.schemas.task import Task as TaskSchema
|
||||||
|
from app.schemas.task import TaskCreate, TaskUpdate
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[TaskSchema])
|
||||||
|
def read_tasks(
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
status: Optional[TaskStatus] = None,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a list of tasks with optional filtering by status.
|
||||||
|
"""
|
||||||
|
query = db.query(Task)
|
||||||
|
|
||||||
|
# Filter by status if provided
|
||||||
|
if status:
|
||||||
|
query = query.filter(Task.status == status)
|
||||||
|
|
||||||
|
# Apply pagination
|
||||||
|
tasks = query.order_by(Task.created_at.desc()).offset(skip).limit(limit).all()
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
@router.post("/", response_model=TaskSchema, status_code=status.HTTP_201_CREATED)
|
||||||
|
def create_task(
|
||||||
|
task_in: TaskCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new task.
|
||||||
|
"""
|
||||||
|
db_task = Task(
|
||||||
|
title=task_in.title,
|
||||||
|
description=task_in.description,
|
||||||
|
due_date=task_in.due_date,
|
||||||
|
status=task_in.status,
|
||||||
|
priority=task_in.priority
|
||||||
|
)
|
||||||
|
db.add(db_task)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@router.get("/{task_id}", response_model=TaskSchema)
|
||||||
|
def read_task(
|
||||||
|
task_id: str,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get a task by ID.
|
||||||
|
"""
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if db_task is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@router.put("/{task_id}", response_model=TaskSchema)
|
||||||
|
def update_task(
|
||||||
|
task_id: str,
|
||||||
|
task_in: TaskUpdate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update a task.
|
||||||
|
"""
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if db_task is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
update_data = task_in.dict(exclude_unset=True)
|
||||||
|
|
||||||
|
# If status is being set to completed and there's no completed_at date yet
|
||||||
|
if (update_data.get("status") == TaskStatus.COMPLETED and
|
||||||
|
not db_task.completed_at):
|
||||||
|
db_task.completed_at = datetime.utcnow()
|
||||||
|
|
||||||
|
# If status is being changed from completed to something else
|
||||||
|
if (db_task.status == TaskStatus.COMPLETED and
|
||||||
|
update_data.get("status") and
|
||||||
|
update_data.get("status") != TaskStatus.COMPLETED):
|
||||||
|
db_task.completed_at = None
|
||||||
|
|
||||||
|
for field, value in update_data.items():
|
||||||
|
setattr(db_task, field, value)
|
||||||
|
|
||||||
|
db.add(db_task)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
||||||
|
|
||||||
|
@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
|
||||||
|
def delete_task(
|
||||||
|
task_id: str,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete a task.
|
||||||
|
"""
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if db_task is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
db.delete(db_task)
|
||||||
|
db.commit()
|
||||||
|
return None
|
||||||
|
|
||||||
|
@router.post("/{task_id}/complete", response_model=TaskSchema)
|
||||||
|
def complete_task(
|
||||||
|
task_id: str,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Mark a task as completed.
|
||||||
|
"""
|
||||||
|
db_task = db.query(Task).filter(Task.id == task_id).first()
|
||||||
|
if db_task is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
|
||||||
|
if db_task.status == TaskStatus.COMPLETED:
|
||||||
|
raise HTTPException(status_code=400, detail="Task is already completed")
|
||||||
|
|
||||||
|
db_task.mark_as_completed()
|
||||||
|
db.add(db_task)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_task)
|
||||||
|
return db_task
|
42
app/models/task.py
Normal file
42
app/models/task.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from sqlalchemy import Column, String, Text, DateTime, Enum
|
||||||
|
from datetime import datetime
|
||||||
|
import enum
|
||||||
|
|
||||||
|
from app.models.base import BaseModel
|
||||||
|
|
||||||
|
class TaskStatus(str, enum.Enum):
|
||||||
|
"""Task status enum."""
|
||||||
|
TODO = "todo"
|
||||||
|
IN_PROGRESS = "in_progress"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
CANCELLED = "cancelled"
|
||||||
|
|
||||||
|
class TaskPriority(str, enum.Enum):
|
||||||
|
"""Task priority enum."""
|
||||||
|
LOW = "low"
|
||||||
|
MEDIUM = "medium"
|
||||||
|
HIGH = "high"
|
||||||
|
|
||||||
|
class Task(BaseModel):
|
||||||
|
"""Task model for task management."""
|
||||||
|
title = Column(String(200), index=True, nullable=False)
|
||||||
|
description = Column(Text, nullable=True)
|
||||||
|
due_date = Column(DateTime, nullable=True)
|
||||||
|
status = Column(
|
||||||
|
Enum(TaskStatus),
|
||||||
|
default=TaskStatus.TODO,
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
priority = Column(
|
||||||
|
Enum(TaskPriority),
|
||||||
|
default=TaskPriority.MEDIUM,
|
||||||
|
nullable=False,
|
||||||
|
index=True
|
||||||
|
)
|
||||||
|
completed_at = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
def mark_as_completed(self):
|
||||||
|
"""Mark the task as completed."""
|
||||||
|
self.status = TaskStatus.COMPLETED
|
||||||
|
self.completed_at = datetime.utcnow()
|
40
app/schemas/task.py
Normal file
40
app/schemas/task.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from app.models.task import TaskStatus, TaskPriority
|
||||||
|
|
||||||
|
class TaskBase(BaseModel):
|
||||||
|
"""Base schema for Task."""
|
||||||
|
title: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
due_date: Optional[datetime] = None
|
||||||
|
status: TaskStatus = TaskStatus.TODO
|
||||||
|
priority: TaskPriority = TaskPriority.MEDIUM
|
||||||
|
|
||||||
|
class TaskCreate(TaskBase):
|
||||||
|
"""Schema for creating a Task."""
|
||||||
|
title: str = Field(..., min_length=1, max_length=200)
|
||||||
|
|
||||||
|
class TaskUpdate(BaseModel):
|
||||||
|
"""Schema for updating a Task."""
|
||||||
|
title: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||||
|
description: Optional[str] = None
|
||||||
|
due_date: Optional[datetime] = None
|
||||||
|
status: Optional[TaskStatus] = None
|
||||||
|
priority: Optional[TaskPriority] = None
|
||||||
|
|
||||||
|
class TaskInDBBase(TaskBase):
|
||||||
|
"""Base schema for Task with DB fields."""
|
||||||
|
id: str
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
completed_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
class Task(TaskInDBBase):
|
||||||
|
"""Schema for returning a Task."""
|
||||||
|
pass
|
48
migrations/versions/002_add_task_table.py
Normal file
48
migrations/versions/002_add_task_table.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""Add task table
|
||||||
|
|
||||||
|
Revision ID: 002
|
||||||
|
Revises: 001
|
||||||
|
Create Date: 2023-10-23
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '002'
|
||||||
|
down_revision = '001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# Create task table
|
||||||
|
op.create_table(
|
||||||
|
'task',
|
||||||
|
sa.Column('id', sa.String(36), primary_key=True, index=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('title', sa.String(200), index=True, nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('due_date', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('status', sa.Enum('todo', 'in_progress', 'completed', 'cancelled', name='taskstatus'), nullable=False, default='todo'),
|
||||||
|
sa.Column('priority', sa.Enum('low', 'medium', 'high', name='taskpriority'), nullable=False, default='medium'),
|
||||||
|
sa.Column('completed_at', sa.DateTime(), nullable=True),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_task_id'), 'task', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_task_title'), 'task', ['title'], unique=False)
|
||||||
|
op.create_index(op.f('ix_task_status'), 'task', ['status'], unique=False)
|
||||||
|
op.create_index(op.f('ix_task_priority'), 'task', ['priority'], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_index(op.f('ix_task_priority'), table_name='task')
|
||||||
|
op.drop_index(op.f('ix_task_status'), table_name='task')
|
||||||
|
op.drop_index(op.f('ix_task_title'), table_name='task')
|
||||||
|
op.drop_index(op.f('ix_task_id'), table_name='task')
|
||||||
|
op.drop_table('task')
|
||||||
|
|
||||||
|
# Drop the enum types
|
||||||
|
op.execute('DROP TYPE IF EXISTS taskstatus;')
|
||||||
|
op.execute('DROP TYPE IF EXISTS taskpriority;')
|
Loading…
x
Reference in New Issue
Block a user