Create Todo application with FastAPI and SQLite

- Implemented project structure
- Added SQLAlchemy models and database connection
- Created Alembic migrations
- Implemented CRUD API endpoints
- Added health endpoint
- Updated README.md

generated with BackendIM... (backend.im)
This commit is contained in:
Automated Action 2025-05-13 06:15:34 +00:00
parent 82e755410b
commit e852047583
22 changed files with 485 additions and 2 deletions

View File

@ -1,3 +1,83 @@
# FastAPI Application
# Todo Application
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A simple Todo application built with FastAPI and SQLite.
## Features
- Create, read, update, and delete todo items
- SQLite database with SQLAlchemy ORM
- Database migrations with Alembic
- Health check endpoint
- OpenAPI documentation
## Project Structure
```
todoapplication/
├── alembic/ # Database migrations
├── app/ # Application code
│ ├── api/ # API routes and endpoints
│ │ ├── crud/ # CRUD operations
│ │ ├── endpoints/ # API endpoint implementations
│ ├── core/ # Core functionality
│ │ ├── config.py # Application configuration
│ ├── db/ # Database related code
│ │ ├── session.py # Database session management
│ ├── models/ # SQLAlchemy models
│ │ ├── todo.py # Todo model
│ ├── schemas/ # Pydantic schemas
│ │ ├── todo.py # Todo schemas
├── storage/ # Storage directory
│ ├── db/ # Database files location
├── alembic.ini # Alembic configuration
├── main.py # Application entry point
├── requirements.txt # Python dependencies
```
## API Endpoints
- `GET /api/v1/todos`: Get all todos
- `POST /api/v1/todos`: Create a new todo
- `GET /api/v1/todos/{todo_id}`: Get a specific todo
- `PUT /api/v1/todos/{todo_id}`: Update a specific todo
- `DELETE /api/v1/todos/{todo_id}`: Delete a specific todo
- `GET /health`: Health check endpoint
- `GET /docs`: API documentation (Swagger UI)
- `GET /redoc`: API documentation (ReDoc)
## Getting Started
### Prerequisites
- Python 3.8+
### Installation
1. Clone the repository
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Run the application:
```bash
uvicorn main:app --reload
```
The application will be available at http://localhost:8000.
### Database Migrations
Run migrations:
```bash
alembic upgrade head
```
Create a new migration:
```bash
alembic revision -m "your migration message"
```

38
alembic.ini Normal file
View File

@ -0,0 +1,38 @@
[alembic]
script_location = alembic
prepend_sys_path = .
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

47
alembic/env.py Normal file
View File

@ -0,0 +1,47 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from app.models import Base
from app.core.config import settings
config = context.config
fileConfig(config.config_file_name)
target_metadata = Base.metadata
# Set the SQLAlchemy URL from our settings
config.set_main_option("sqlalchemy.url", settings.SQLALCHEMY_DATABASE_URL)
def run_migrations_offline():
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
alembic/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,38 @@
"""Initial migration
Revision ID: 001
Revises:
Create Date: 2023-05-13
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create todos table
op.create_table(
'todos',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.String(), nullable=True),
sa.Column('completed', sa.Boolean(), nullable=False, default=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_todos_id'), 'todos', ['id'], unique=False)
op.create_index(op.f('ix_todos_title'), 'todos', ['title'], unique=False)
def downgrade():
op.drop_index(op.f('ix_todos_title'), table_name='todos')
op.drop_index(op.f('ix_todos_id'), table_name='todos')
op.drop_table('todos')

0
app/__init__.py Normal file
View File

0
app/api/__init__.py Normal file
View File

0
app/api/crud/__init__.py Normal file
View File

43
app/api/crud/todo.py Normal file
View File

@ -0,0 +1,43 @@
from sqlalchemy.orm import Session
from typing import List, Optional
from app.models.todo import Todo
from app.schemas.todo import TodoCreate, TodoUpdate
def get_todo(db: Session, todo_id: int) -> Optional[Todo]:
return db.query(Todo).filter(Todo.id == todo_id).first()
def get_todos(db: Session, skip: int = 0, limit: int = 100) -> List[Todo]:
return db.query(Todo).offset(skip).limit(limit).all()
def create_todo(db: Session, todo: TodoCreate) -> Todo:
db_todo = Todo(
title=todo.title,
description=todo.description,
completed=todo.completed
)
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
def update_todo(db: Session, todo_id: int, todo: TodoUpdate) -> Optional[Todo]:
db_todo = get_todo(db, todo_id)
if not db_todo:
return None
update_data = todo.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_todo, field, value)
db.commit()
db.refresh(db_todo)
return db_todo
def delete_todo(db: Session, todo_id: int) -> bool:
db_todo = get_todo(db, todo_id)
if not db_todo:
return False
db.delete(db_todo)
db.commit()
return True

View File

View File

@ -0,0 +1,85 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Any
from app.api.crud import todo as crud
from app.schemas.todo import Todo, TodoCreate, TodoUpdate
from app.db.session import get_db
router = APIRouter()
@router.get("/", response_model=List[Todo])
def read_todos(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
) -> Any:
"""
Retrieve all todos.
"""
todos = crud.get_todos(db, skip=skip, limit=limit)
return todos
@router.post("/", response_model=Todo, status_code=status.HTTP_201_CREATED)
def create_todo(
*,
db: Session = Depends(get_db),
todo_in: TodoCreate,
) -> Any:
"""
Create new todo.
"""
todo = crud.create_todo(db, todo_in)
return todo
@router.get("/{todo_id}", response_model=Todo)
def read_todo(
*,
db: Session = Depends(get_db),
todo_id: int,
) -> Any:
"""
Get todo by ID.
"""
todo = crud.get_todo(db, todo_id)
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo not found"
)
return todo
@router.put("/{todo_id}", response_model=Todo)
def update_todo(
*,
db: Session = Depends(get_db),
todo_id: int,
todo_in: TodoUpdate,
) -> Any:
"""
Update a todo.
"""
todo = crud.update_todo(db, todo_id, todo_in)
if not todo:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo not found"
)
return todo
@router.delete("/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(
*,
db: Session = Depends(get_db),
todo_id: int,
) -> Any:
"""
Delete a todo.
"""
success = crud.delete_todo(db, todo_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo not found"
)
return None

6
app/api/routes.py Normal file
View File

@ -0,0 +1,6 @@
from fastapi import APIRouter
from app.api.endpoints import todos
router = APIRouter()
router.include_router(todos.router, prefix="/todos", tags=["todos"])

0
app/core/__init__.py Normal file
View File

24
app/core/config.py Normal file
View File

@ -0,0 +1,24 @@
from pydantic import BaseModel
from typing import Optional
from pathlib import Path
class Settings(BaseModel):
API_V1_STR: str = "/api/v1"
PROJECT_NAME: str = "Todo API"
PROJECT_DESCRIPTION: str = "A simple Todo API built with FastAPI and SQLite"
VERSION: str = "0.1.0"
# Database settings
DB_DIR: Path = Path("/app") / "storage" / "db"
SQLALCHEMY_DATABASE_URL: Optional[str] = None
class Config:
case_sensitive = True
settings = Settings()
# Ensure DB directory exists
settings.DB_DIR.mkdir(parents=True, exist_ok=True)
# Set the database URL
settings.SQLALCHEMY_DATABASE_URL = f"sqlite:///{settings.DB_DIR}/db.sqlite"

0
app/db/__init__.py Normal file
View File

24
app/db/session.py Normal file
View File

@ -0,0 +1,24 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
# Create SQLAlchemy engine
engine = create_engine(
settings.SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False} # Only needed for SQLite
)
# Create a SessionLocal class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create a Base class
Base = declarative_base()
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

1
app/models/__init__.py Normal file
View File

@ -0,0 +1 @@
from app.models.todo import Todo

13
app/models/todo.py Normal file
View File

@ -0,0 +1,13 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.sql import func
from app.db.session import Base
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, nullable=True)
completed = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())

1
app/schemas/__init__.py Normal file
View File

@ -0,0 +1 @@
from app.schemas.todo import Todo, TodoCreate, TodoUpdate

27
app/schemas/todo.py Normal file
View File

@ -0,0 +1,27 @@
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
class TodoBase(BaseModel):
title: str
description: Optional[str] = None
completed: bool = False
class TodoCreate(TodoBase):
pass
class TodoUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
completed: Optional[bool] = None
class TodoInDBBase(TodoBase):
id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
orm_mode = True
class Todo(TodoInDBBase):
pass

25
main.py Normal file
View File

@ -0,0 +1,25 @@
from fastapi import FastAPI
from app.api.routes import router as api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.VERSION,
openapi_url=f"{settings.API_V1_STR}/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/health", status_code=200)
def health_check():
"""
Health check endpoint to verify the application is running
"""
return {"status": "healthy", "version": settings.VERSION}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
fastapi==0.110.0
uvicorn==0.28.0
sqlalchemy==2.0.27
pydantic==2.5.3
alembic==1.12.1
python-dotenv==1.0.0
pathlib==1.0.1