Implement Task Management Tool with FastAPI and SQLite

- Set up FastAPI project structure with API versioning
- Create database models for users and tasks
- Implement SQLAlchemy ORM with SQLite database
- Initialize Alembic for database migrations
- Create API endpoints for task management (CRUD)
- Create API endpoints for user management
- Add JWT authentication and authorization
- Add health check endpoint
- Add comprehensive README.md with API documentation
This commit is contained in:
Automated Action 2025-06-02 20:40:57 +00:00
parent 33bf1f9d90
commit f8bb3dd21d
34 changed files with 1340 additions and 2 deletions

114
README.md
View File

@ -1,3 +1,113 @@
# FastAPI Application
# Task Management Tool
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A FastAPI backend for managing tasks with user authentication and SQLite database.
## Features
- User registration and authentication with JWT tokens
- Task management with CRUD operations
- Task prioritization and status tracking
- RESTful API with JSON responses
- SQLite database with SQLAlchemy ORM
- Database migrations with Alembic
- Health check endpoint
## API Endpoints
### Health Check
- `GET /health` - Check API health
### User Management
- `POST /users/register` - Register a new user
- `POST /users/login` - Login and get access token
- `GET /users/me` - Get current user info
- `PUT /users/me` - Update current user info
### Task Management
- `GET /tasks` - List all tasks for current user
- `POST /tasks` - Create a new task
- `GET /tasks/{task_id}` - Get a specific task
- `PUT /tasks/{task_id}` - Update a task
- `DELETE /tasks/{task_id}` - Delete a task
## Tech Stack
- [FastAPI](https://fastapi.tiangolo.com/) - Modern, fast web framework for building APIs
- [SQLAlchemy](https://www.sqlalchemy.org/) - SQL toolkit and Object-Relational Mapping
- [Alembic](https://alembic.sqlalchemy.org/) - Database migration tool
- [Pydantic](https://pydantic-docs.helpmanual.io/) - Data validation and settings management
- [SQLite](https://www.sqlite.org/) - Lightweight relational database
- [Python-Jose](https://python-jose.readthedocs.io/) - JavaScript Object Signing and Encryption (JOSE) implementation
- [Passlib](https://passlib.readthedocs.io/) - Password hashing library
## Setup and Installation
### Prerequisites
- Python 3.9+
- pip (Python package installer)
### Environment Setup
1. Clone the repository:
```bash
git clone <repository-url>
cd taskmanagementtool
```
2. Create and activate a virtual environment (optional but recommended):
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install dependencies:
```bash
pip install -r requirements.txt
```
4. Set environment variables (optional, defaults are provided):
```bash
export SECRET_KEY="your-secret-key"
export ACCESS_TOKEN_EXPIRE_MINUTES=30
```
### Database Setup
1. Run Alembic migrations to set up the database:
```bash
alembic upgrade head
```
### Running the Application
1. Start the FastAPI server:
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
2. Access the API documentation:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
## Development
### Adding Migrations
When you modify the database models, create a new migration:
```bash
alembic revision --autogenerate -m "Description of changes"
```
### Applying Migrations
To update your database to the latest schema:
```bash
alembic upgrade head
```
## API Documentation
The API documentation is automatically generated and can be accessed at `/docs` or `/redoc` endpoints.

85
alembic.ini Normal file
View File

@ -0,0 +1,85 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat migrations/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# SQLite URL with absolute path
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration
[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

0
app/__init__.py Normal file
View File

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

54
app/api/deps.py Normal file
View File

@ -0,0 +1,54 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import ALGORITHM
from app.crud.user import get as get_user
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import TokenPayload
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/users/login")
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
"""
Get the current user from the token
"""
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[ALGORITHM]
)
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
user = get_user(db, user_id=int(token_data.sub))
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
def get_current_active_user(current_user: User = Depends(get_current_user)) -> User:
"""
Get the current active user
"""
if not current_user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
return current_user

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

11
app/api/v1/api.py Normal file
View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
from app.api.v1.endpoints import health, tasks, users
# Create API router
api_router = APIRouter()
# Include routes from endpoint modules
api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
api_router.include_router(users.router, prefix="/users", tags=["users"])

View File

View File

@ -0,0 +1,24 @@
from fastapi import APIRouter, Depends
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.db.session import get_db
router = APIRouter()
@router.get("", status_code=200)
async def health_check(db: Session = Depends(get_db)):
"""
Health check endpoint to verify API is running correctly
"""
# Check database connection
try:
db.execute(text("SELECT 1"))
db_status = "healthy"
except Exception:
db_status = "unhealthy"
return {
"status": "ok",
"database": db_status
}

View File

@ -0,0 +1,101 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_user
from app.crud import task as crud_task
from app.db.session import get_db
from app.models.user import User
from app.schemas.task import Task, TaskCreate, TaskUpdate
router = APIRouter()
@router.get("", response_model=List[Task])
def read_tasks(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve tasks for the current user.
"""
tasks = crud_task.get_multi(
db=db, user_id=current_user.id, skip=skip, limit=limit
)
return tasks
@router.post("", response_model=Task, status_code=status.HTTP_201_CREATED)
def create_task(
*,
db: Session = Depends(get_db),
task_in: TaskCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create a new task.
"""
task = crud_task.create(db=db, obj_in=task_in, user_id=current_user.id)
return task
@router.get("/{task_id}", response_model=Task)
def read_task(
*,
db: Session = Depends(get_db),
task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get task by ID.
"""
task = crud_task.get(db=db, task_id=task_id, user_id=current_user.id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
return task
@router.put("/{task_id}", response_model=Task)
def update_task(
*,
db: Session = Depends(get_db),
task_id: int,
task_in: TaskUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a task.
"""
task = crud_task.get(db=db, task_id=task_id, user_id=current_user.id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
task = crud_task.update(db=db, db_obj=task, obj_in=task_in)
return task
@router.delete(
"/{task_id}",
response_model=None,
status_code=status.HTTP_204_NO_CONTENT
)
def delete_task(
*,
db: Session = Depends(get_db),
task_id: int,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a task.
"""
task = crud_task.get(db=db, task_id=task_id, user_id=current_user.id)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
crud_task.delete(db=db, task_id=task_id, user_id=current_user.id)
return None

View File

@ -0,0 +1,123 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_user
from app.core.config import settings
from app.core.security import create_access_token
from app.crud import user as crud_user
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import Token
from app.schemas.user import User as UserSchema
from app.schemas.user import UserCreate, UserUpdate
router = APIRouter()
@router.post(
"/register",
response_model=UserSchema,
status_code=status.HTTP_201_CREATED
)
def create_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
) -> Any:
"""
Create a new user.
"""
# Check if user with given email exists
user = crud_user.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Check if user with given username exists
user = crud_user.get_by_username(db, username=user_in.username)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already taken"
)
# Create new user
user = crud_user.create(db, obj_in=user_in)
return user
@router.post("/login", response_model=Token)
def login_access_token(
db: Session = Depends(get_db),
form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests.
"""
user = crud_user.authenticate(
db=db, username=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Inactive user"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.get("/me", response_model=UserSchema)
def read_user_me(
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.put("/me", response_model=UserSchema)
def update_user_me(
*,
db: Session = Depends(get_db),
user_in: UserUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update current user.
"""
# Check if user is trying to update email and if email already exists
if user_in.email and user_in.email != current_user.email:
user = crud_user.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered"
)
# Check if user is trying to update username and if username already exists
if user_in.username and user_in.username != current_user.username:
user = crud_user.get_by_username(db, username=user_in.username)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already taken"
)
# Update user
user = crud_user.update(db, db_obj=current_user, obj_in=user_in)
return user

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

39
app/core/app.py Normal file
View File

@ -0,0 +1,39 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.api import api_router
from app.core.config import settings
def create_app() -> FastAPI:
"""
Create and configure the FastAPI application
"""
app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.PROJECT_VERSION,
openapi_url="/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router)
# Ensure DB directory exists
db_dir = Path(settings.DB_DIR)
db_dir.mkdir(parents=True, exist_ok=True)
return app

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

@ -0,0 +1,31 @@
import os
from pathlib import Path
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""
Application settings
"""
# Project info
PROJECT_NAME: str = "Task Management Tool"
PROJECT_DESCRIPTION: str = "A FastAPI backend for managing tasks"
PROJECT_VERSION: str = "0.1.0"
# Database settings
DB_DIR: Path = Path("/app/storage/db")
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
# JWT settings
SECRET_KEY: str = os.getenv("SECRET_KEY", "supersecretkey")
ACCESS_TOKEN_EXPIRE_MINUTES: int = int(
os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30")
)
# Configure Pydantic to read environment variables
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
settings = Settings()

40
app/core/security.py Normal file
View File

@ -0,0 +1,40 @@
from datetime import datetime, timedelta
from typing import Any, Optional
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# JWT settings
ALGORITHM = "HS256"
def create_access_token(subject: Any, expires_delta: Optional[timedelta] = None) -> str:
"""
Create a JWT access token
"""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify a password against a hash
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""
Hash a password
"""
return pwd_context.hash(password)

32
app/crud/__init__.py Normal file
View File

@ -0,0 +1,32 @@
# Re-export task operations
from app.crud.task import create as create_task
from app.crud.task import delete as delete_task
from app.crud.task import get as get_task
from app.crud.task import get_multi as get_tasks
from app.crud.task import hard_delete as hard_delete_task
from app.crud.task import update as update_task
# Re-export user operations
from app.crud.user import authenticate as authenticate_user
from app.crud.user import create as create_user
from app.crud.user import delete as delete_user
from app.crud.user import get as get_user
from app.crud.user import get_by_email as get_user_by_email
from app.crud.user import get_by_username as get_user_by_username
from app.crud.user import update as update_user
__all__ = [
"create_task",
"delete_task",
"get_task",
"get_tasks",
"hard_delete_task",
"update_task",
"authenticate_user",
"create_user",
"delete_user",
"get_user",
"get_user_by_email",
"get_user_by_username",
"update_user",
]

84
app/crud/task.py Normal file
View File

@ -0,0 +1,84 @@
from typing import Any, Dict, List, Optional, Union
from sqlalchemy import and_
from sqlalchemy.orm import Session
from app.models.task import Task
from app.schemas.task import TaskCreate, TaskUpdate
def get(db: Session, task_id: int, user_id: int) -> Optional[Task]:
"""
Get a task by ID and user_id
"""
return db.query(Task).filter(
and_(Task.id == task_id, Task.user_id == user_id, not Task.is_deleted)
).first()
def get_multi(
db: Session, user_id: int, skip: int = 0, limit: int = 100
) -> List[Task]:
"""
Get multiple tasks by user_id
"""
return db.query(Task).filter(
and_(Task.user_id == user_id, not Task.is_deleted)
).offset(skip).limit(limit).all()
def create(db: Session, obj_in: TaskCreate, user_id: int) -> Task:
"""
Create a new task
"""
db_obj = Task(
**obj_in.model_dump(),
user_id=user_id
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
db: Session, db_obj: Task, obj_in: Union[TaskUpdate, Dict[str, Any]]
) -> Task:
"""
Update a task
"""
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.model_dump(exclude_unset=True)
for field in update_data:
if hasattr(db_obj, field):
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def delete(db: Session, task_id: int, user_id: int) -> bool:
"""
Soft delete a task by setting is_deleted to True
"""
task = get(db, task_id=task_id, user_id=user_id)
if not task:
return False
task.is_deleted = True
db.add(task)
db.commit()
return True
def hard_delete(db: Session, task_id: int, user_id: int) -> bool:
"""
Hard delete a task
"""
task = get(db, task_id=task_id, user_id=user_id)
if not task:
return False
db.delete(task)
db.commit()
return True

90
app/crud/user.py Normal file
View File

@ -0,0 +1,90 @@
from typing import Any, Dict, Optional, Union
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
def get(db: Session, user_id: int) -> Optional[User]:
"""
Get a user by ID
"""
return db.query(User).filter(User.id == user_id).first()
def get_by_email(db: Session, email: str) -> Optional[User]:
"""
Get a user by email
"""
return db.query(User).filter(User.email == email).first()
def get_by_username(db: Session, username: str) -> Optional[User]:
"""
Get a user by username
"""
return db.query(User).filter(User.username == username).first()
def create(db: Session, obj_in: UserCreate) -> User:
"""
Create a new user
"""
db_obj = User(
email=obj_in.email,
username=obj_in.username,
hashed_password=get_password_hash(obj_in.password),
full_name=obj_in.full_name,
is_active=obj_in.is_active
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
db: Session, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
) -> User:
"""
Update a user
"""
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.model_dump(exclude_unset=True)
if "password" in update_data and update_data["password"]:
hashed_password = get_password_hash(update_data["password"])
del update_data["password"]
update_data["hashed_password"] = hashed_password
for field in update_data:
if hasattr(db_obj, field):
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def delete(db: Session, user_id: int) -> bool:
"""
Delete a user by ID
"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
return False
db.delete(user)
db.commit()
return True
def authenticate(db: Session, username: str, password: str) -> Optional[User]:
"""
Authenticate a user with username and password
"""
user = get_by_username(db, username=username)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user

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

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

@ -0,0 +1,28 @@
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}
)
# Create sessionmaker
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create Base class for models
Base = declarative_base()
# Dependency for getting DB session
def get_db():
"""
Dependency function that provides a SQLAlchemy session
"""
db = SessionLocal()
try:
yield db
finally:
db.close()

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

@ -0,0 +1,4 @@
from app.models.task import Task, TaskPriority, TaskStatus
from app.models.user import User
__all__ = ["Task", "TaskPriority", "TaskStatus", "User"]

65
app/models/task.py Normal file
View File

@ -0,0 +1,65 @@
import enum
from sqlalchemy import (
Boolean,
Column,
DateTime,
Enum,
ForeignKey,
Integer,
String,
Text,
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.session import Base
class TaskPriority(str, enum.Enum):
"""
Enum for task priority levels
"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class TaskStatus(str, enum.Enum):
"""
Enum for task status
"""
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
class Task(Base):
"""
Task model for storing task information
"""
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True, nullable=False)
description = Column(Text, nullable=True)
status = Column(Enum(TaskStatus), default=TaskStatus.TODO, nullable=False)
priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM, nullable=False)
due_date = Column(DateTime(timezone=True), nullable=True)
is_deleted = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now()
)
# Foreign keys
user_id = Column(
Integer,
ForeignKey("users.id", ondelete="CASCADE"),
nullable=False
)
# Relationships
user = relationship("User", backref="tasks")

25
app/models/user.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy import Boolean, Column, DateTime, Integer, String
from sqlalchemy.sql import func
from app.db.session import Base
class User(Base):
"""
User model for storing user information
"""
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
username = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
full_name = Column(String, nullable=True)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now()
)

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

@ -0,0 +1,17 @@
from app.schemas.task import Task, TaskCreate, TaskPriority, TaskStatus, TaskUpdate
from app.schemas.token import Token, TokenPayload
from app.schemas.user import User, UserCreate, UserInDB, UserUpdate
__all__ = [
"Task",
"TaskCreate",
"TaskUpdate",
"TaskStatus",
"TaskPriority",
"Token",
"TokenPayload",
"User",
"UserCreate",
"UserUpdate",
"UserInDB",
]

67
app/schemas/task.py Normal file
View File

@ -0,0 +1,67 @@
from datetime import datetime
from enum import Enum
from typing import Optional
from pydantic import BaseModel, ConfigDict
class TaskPriority(str, Enum):
"""
Enum for task priority levels
"""
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class TaskStatus(str, Enum):
"""
Enum for task status
"""
TODO = "todo"
IN_PROGRESS = "in_progress"
DONE = "done"
class TaskBase(BaseModel):
"""
Base task schema with shared attributes
"""
title: str
description: Optional[str] = None
status: TaskStatus = TaskStatus.TODO
priority: TaskPriority = TaskPriority.MEDIUM
due_date: Optional[datetime] = None
class TaskCreate(TaskBase):
"""
Schema for task creation
"""
pass
class TaskUpdate(BaseModel):
"""
Schema for task updates
"""
title: Optional[str] = None
description: Optional[str] = None
status: Optional[TaskStatus] = None
priority: Optional[TaskPriority] = None
due_date: Optional[datetime] = None
is_deleted: Optional[bool] = None
class TaskInDBBase(TaskBase):
"""
Base schema for tasks in DB with id and timestamps
"""
id: int
user_id: int
created_at: datetime
updated_at: datetime
is_deleted: bool = False
model_config = ConfigDict(from_attributes=True)
class Task(TaskInDBBase):
"""
Schema for returning task information
"""
pass

17
app/schemas/token.py Normal file
View File

@ -0,0 +1,17 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
"""
Schema for token response
"""
access_token: str
token_type: str
class TokenPayload(BaseModel):
"""
Schema for token payload
"""
sub: Optional[str] = None

53
app/schemas/user.py Normal file
View File

@ -0,0 +1,53 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class UserBase(BaseModel):
"""
Base user schema with shared attributes
"""
email: EmailStr
username: str
full_name: Optional[str] = None
is_active: bool = True
class UserCreate(UserBase):
"""
Schema for user creation with password
"""
password: str = Field(..., min_length=8)
class UserUpdate(BaseModel):
"""
Schema for user updates
"""
email: Optional[EmailStr] = None
username: Optional[str] = None
full_name: Optional[str] = None
password: Optional[str] = Field(None, min_length=8)
is_active: Optional[bool] = None
class UserInDBBase(UserBase):
"""
Base schema for users in DB with id and timestamps
"""
id: int
created_at: datetime
updated_at: datetime
is_superuser: bool = False
model_config = ConfigDict(from_attributes=True)
class User(UserInDBBase):
"""
Schema for returning user information
"""
pass
class UserInDB(UserInDBBase):
"""
Schema with hashed password for internal use
"""
hashed_password: str

8
main.py Normal file
View File

@ -0,0 +1,8 @@
import uvicorn
from app.core.app import create_app
app = create_app()
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration with SQLite.

79
migrations/env.py Normal file
View File

@ -0,0 +1,79 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# Import models for autogenerate support
from app.db.session import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
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():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
is_sqlite = connection.dialect.name == "sqlite"
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=is_sqlite, # Important for SQLite
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/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,99 @@
"""Initial migration
Revision ID: 01_initial_tables
Revises:
Create Date: 2023-10-20
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = '01_initial_tables'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create users table
op.create_table(
'users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.Column('is_superuser', sa.Boolean(), nullable=True, default=False),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True
),
sa.Column(
'updated_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True
),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
# Create tasks table
op.create_table(
'tasks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column(
'status',
sa.Enum('todo', 'in_progress', 'done', name='taskstatus'),
nullable=False,
default='todo'
),
sa.Column(
'priority',
sa.Enum('low', 'medium', 'high', name='taskpriority'),
nullable=False,
default='medium'
),
sa.Column('due_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True, default=False),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True
),
sa.Column(
'updated_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True
),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_tasks_id'), 'tasks', ['id'], unique=False)
op.create_index(op.f('ix_tasks_title'), 'tasks', ['title'], unique=False)
def downgrade():
# Drop tasks table
op.drop_index(op.f('ix_tasks_title'), table_name='tasks')
op.drop_index(op.f('ix_tasks_id'), table_name='tasks')
op.drop_table('tasks')
# Drop users table
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[tool.ruff]
line-length = 88
indent-width = 4
target-version = "py39"
[tool.ruff.lint]
select = ["E", "F", "I", "W"]
ignore = []
fixable = ["ALL"]
unfixable = []
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
fastapi>=0.103.1
uvicorn>=0.23.2
sqlalchemy>=2.0.20
alembic>=1.12.0
pydantic>=2.4.0
pydantic-settings>=2.0.3
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
python-multipart>=0.0.6
ruff>=0.0.292