Implement task management tool with FastAPI and SQLite

This commit is contained in:
Automated Action 2025-06-12 19:05:38 +00:00
parent 7efc959601
commit 3ae0760d76
37 changed files with 1167 additions and 2 deletions

134
README.md
View File

@ -1,3 +1,133 @@
# FastAPI Application
# Task Management Tool
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A RESTful API for managing tasks, built with FastAPI and SQLite.
## Features
- User registration and authentication with JWT
- CRUD operations for tasks
- Task filtering, sorting, and pagination
- Health check endpoint
## Requirements
- Python 3.8+
- Dependencies listed in `requirements.txt`
## Environment Variables
The application uses the following environment variables:
- `SECRET_KEY` - Secret key for JWT token generation (default: a development key, must be changed in production)
- `ACCESS_TOKEN_EXPIRE_MINUTES` - Expiration time for JWT tokens in minutes (default: 30)
## Installation and Setup
1. Clone the repository
2. Create a virtual environment and activate it
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
3. Install dependencies
```bash
pip install -r requirements.txt
```
4. Run database migrations
```bash
alembic upgrade head
```
5. Start the server
```bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
## API Documentation
The API documentation is available at `/docs` or `/redoc` when the server is running.
### Base URL
```
http://localhost:8000/api/v1
```
### Authentication
The API uses JWT for authentication. To obtain a token, send a POST request to `/api/v1/auth/login` with your email and password.
```bash
curl -X 'POST' \
'http://localhost:8000/api/v1/auth/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'username=your@email.com&password=yourpassword'
```
### Endpoints
#### Authentication
- `POST /auth/register` - Register a new user
- `POST /auth/login` - Login and get access token
#### Users
- `GET /users/me` - Get current user information
- `PUT /users/me` - Update current user information
#### Tasks
- `GET /tasks` - List all tasks (with optional filtering and sorting)
- `POST /tasks` - Create a new task
- `GET /tasks/{task_id}` - Get a specific task
- `PUT /tasks/{task_id}` - Update a specific task
- `DELETE /tasks/{task_id}` - Delete a specific task
#### Filtering and Sorting Tasks
Tasks can be filtered and sorted using query parameters:
- `is_completed` - Filter by completion status (true/false)
- `priority` - Filter by priority (1=Low, 2=Medium, 3=High)
- `sort_by` - Sort by field (created_at, due_date, priority, title)
- `sort_order` - Sort order (asc/desc)
- `skip` - Number of items to skip (for pagination)
- `limit` - Maximum number of items to return (for pagination)
Example:
```
GET /tasks?is_completed=false&priority=3&sort_by=due_date&sort_order=asc&skip=0&limit=10
```
## Health Check
The API provides a health check endpoint at `/health`.
## Development
### Running Tests
```bash
pytest
```
### Database Migrations
Create a new migration:
```bash
alembic revision --autogenerate -m "Description of changes"
```
Apply migrations:
```bash
alembic upgrade head
```
Revert migrations:
```bash
alembic downgrade -1
```

86
alembic.ini Normal file
View File

@ -0,0 +1,86 @@
# 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

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

@ -0,0 +1,43 @@
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 verify_password
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import TokenPayload
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/auth/login"
)
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.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 = db.query(User).filter(User.id == token_data.sub).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def authenticate_user(db: Session, email: str, password: str) -> User:
user = db.query(User).filter(User.email == email).first()
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user

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

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

@ -0,0 +1,8 @@
from fastapi import APIRouter
from app.api.v1.endpoints import auth, tasks, users
router = APIRouter()
router.include_router(auth.router, prefix="/auth", tags=["authentication"])
router.include_router(users.router, prefix="/users", tags=["users"])
router.include_router(tasks.router, prefix="/tasks", tags=["tasks"])

View File

View File

@ -0,0 +1,73 @@
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 import crud
from app.api import deps
from app.core import security
from app.core.config import settings
from app.schemas.token import Token
from app.schemas.user import User, UserCreate
router = APIRouter()
@router.post("/login", response_model=Token)
def login_access_token(
db: Session = Depends(deps.get_db),
form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = crud.user.authenticate(
db, email=form_data.username, password=form_data.password
)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
elif not crud.user.is_active(user):
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": security.create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.post("/register", response_model=User)
def register_new_user(
*,
db: Session = Depends(deps.get_db),
user_in: UserCreate,
) -> Any:
"""
Create new user
"""
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",
)
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 registered",
)
user = crud.user.create(db, obj_in=user_in)
return user

View File

@ -0,0 +1,118 @@
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
from app import crud
from app.api import deps
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(
db: Session = Depends(deps.get_db),
skip: int = 0,
limit: int = 100,
is_completed: Optional[bool] = None,
priority: Optional[int] = Query(None, ge=1, le=3),
sort_by: str = Query("created_at", regex="^(created_at|due_date|priority|title)$"),
sort_order: str = Query("desc", regex="^(asc|desc)$"),
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
Retrieve tasks for current user with optional filtering and sorting.
"""
tasks = crud.task.get_multi_by_owner(
db=db,
owner_id=current_user.id,
skip=skip,
limit=limit,
is_completed=is_completed,
priority=priority,
sort_by=sort_by,
sort_order=sort_order
)
return tasks
@router.post("/", response_model=Task)
def create_task(
*,
db: Session = Depends(deps.get_db),
task_in: TaskCreate,
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
Create new task.
"""
task = crud.task.create_with_owner(db=db, obj_in=task_in, owner_id=current_user.id)
return task
@router.get("/{task_id}", response_model=Task)
def read_task(
*,
db: Session = Depends(deps.get_db),
task_id: str,
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
Get task by ID.
"""
task = crud.task.get_task_by_id_and_owner(
db=db, task_id=task_id, owner_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(deps.get_db),
task_id: str,
task_in: TaskUpdate,
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
Update a task.
"""
task = crud.task.get_task_by_id_and_owner(
db=db, task_id=task_id, owner_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}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_task(
*,
db: Session = Depends(deps.get_db),
task_id: str,
current_user: User = Depends(deps.get_current_user),
) -> Any:
"""
Delete a task.
"""
task = crud.task.get_task_by_id_and_owner(
db=db, task_id=task_id, owner_id=current_user.id
)
if not task:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Task not found"
)
crud.task.remove(db=db, id=task_id)
return None

View File

@ -0,0 +1,53 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import crud
from app.api import deps
from app.models.user import User as UserModel
from app.schemas.user import User, UserUpdate
router = APIRouter()
@router.get("/me", response_model=User)
def read_user_me(
current_user: UserModel = Depends(deps.get_current_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.put("/me", response_model=User)
def update_user_me(
*,
db: Session = Depends(deps.get_db),
user_in: UserUpdate,
current_user: UserModel = Depends(deps.get_current_user),
) -> Any:
"""
Update own user.
"""
# Check if email is being updated and if it's already in use
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 username is being updated and if it's already in use
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 registered"
)
user = crud.user.update(db, db_obj=current_user, obj_in=user_in)
return user

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

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

@ -0,0 +1,22 @@
from pathlib import Path
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
PROJECT_NAME: str = "Task Management Tool"
API_V1_STR: str = "/api/v1"
# JWT Settings
SECRET_KEY: str = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7" # Change in production
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# For SQLite
DB_DIR: Path = Path("/app") / "storage" / "db"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
settings = Settings()

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

@ -0,0 +1,26 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None) -> str:
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=settings.ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)

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

@ -0,0 +1,4 @@
from app.crud.crud_user import user
from app.crud.crud_task import task
__all__ = ["user", "task"]

66
app/crud/base.py Normal file
View File

@ -0,0 +1,66 @@
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.db.base_class import Base
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
def __init__(self, model: Type[ModelType]):
"""
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
**Parameters**
* `model`: A SQLAlchemy model class
* `schema`: A Pydantic model (schema) class
"""
self.model = model
def get(self, db: Session, id: Any) -> Optional[ModelType]:
return db.query(self.model).filter(self.model.id == id).first()
def get_multi(
self, db: Session, *, skip: int = 0, limit: int = 100
) -> List[ModelType]:
return db.query(self.model).offset(skip).limit(limit).all()
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
obj_in_data = jsonable_encoder(obj_in)
db_obj = self.model(**obj_in_data) # type: ignore
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self,
db: Session,
*,
db_obj: ModelType,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
) -> ModelType:
obj_data = jsonable_encoder(db_obj)
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
for field in obj_data:
if field in update_data:
setattr(db_obj, field, update_data[field])
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def remove(self, db: Session, *, id: Any) -> ModelType:
obj = db.query(self.model).get(id)
db.delete(obj)
db.commit()
return obj

69
app/crud/crud_task.py Normal file
View File

@ -0,0 +1,69 @@
import uuid
from typing import List, Optional
from sqlalchemy import and_, asc, desc
from sqlalchemy.orm import Session
from app.crud.base import CRUDBase
from app.models.task import Task
from app.schemas.task import TaskCreate, TaskUpdate
class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]):
def create_with_owner(
self, db: Session, *, obj_in: TaskCreate, owner_id: str
) -> Task:
db_obj = Task(
id=str(uuid.uuid4()),
title=obj_in.title,
description=obj_in.description,
priority=obj_in.priority,
due_date=obj_in.due_date,
is_completed=obj_in.is_completed,
owner_id=owner_id,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def get_multi_by_owner(
self,
db: Session,
*,
owner_id: str,
skip: int = 0,
limit: int = 100,
is_completed: Optional[bool] = None,
priority: Optional[int] = None,
sort_by: str = "created_at",
sort_order: str = "desc"
) -> List[Task]:
# Start with base query
query = db.query(Task).filter(Task.owner_id == owner_id)
# Apply filters if provided
if is_completed is not None:
query = query.filter(Task.is_completed == is_completed)
if priority is not None:
query = query.filter(Task.priority == priority)
# Apply sorting
if sort_order.lower() == "asc":
query = query.order_by(asc(getattr(Task, sort_by)))
else:
query = query.order_by(desc(getattr(Task, sort_by)))
# Apply pagination
return query.offset(skip).limit(limit).all()
def get_task_by_id_and_owner(
self, db: Session, *, task_id: str, owner_id: str
) -> Optional[Task]:
return db.query(Task).filter(
and_(Task.id == task_id, Task.owner_id == owner_id)
).first()
task = CRUDTask(Task)

57
app/crud/crud_user.py Normal file
View File

@ -0,0 +1,57 @@
import uuid
from typing import Any, Dict, Optional, Union
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.crud.base import CRUDBase
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def get_by_username(self, db: Session, *, username: str) -> Optional[User]:
return db.query(User).filter(User.username == username).first()
def create(self, db: Session, *, obj_in: UserCreate) -> User:
db_obj = User(
id=str(uuid.uuid4()),
email=obj_in.email,
username=obj_in.username,
hashed_password=get_password_hash(obj_in.password),
is_active=obj_in.is_active,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
) -> User:
if isinstance(obj_in, dict):
update_data = obj_in
else:
update_data = obj_in.dict(exclude_unset=True)
if update_data.get("password"):
hashed_password = get_password_hash(update_data["password"])
del update_data["password"]
update_data["hashed_password"] = hashed_password
return super().update(db, db_obj=db_obj, obj_in=update_data)
def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
user = self.get_by_email(db, email=email)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def is_active(self, user: User) -> bool:
return user.is_active
user = CRUDUser(User)

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

5
app/db/base.py Normal file
View File

@ -0,0 +1,5 @@
# Import all the models, so that Base has them before being
# imported by Alembic
from app.db.base_class import Base # noqa
from app.models.user import User # noqa
from app.models.task import Task # noqa

14
app/db/base_class.py Normal file
View File

@ -0,0 +1,14 @@
from typing import Any
from sqlalchemy.ext.declarative import as_declarative, declared_attr
@as_declarative()
class Base:
id: Any
__name__: str
# Generate __tablename__ automatically
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()

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

@ -0,0 +1,23 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
# Ensure the database directory exists
settings.DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{settings.DB_DIR}/db.sqlite"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

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

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

@ -0,0 +1,22 @@
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class Task(Base):
id = Column(String, primary_key=True, index=True)
title = Column(String, index=True, nullable=False)
description = Column(Text, nullable=True)
priority = Column(Integer, default=1, nullable=False) # 1=Low, 2=Medium, 3=High
due_date = Column(DateTime(timezone=True), nullable=True)
is_completed = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Foreign keys
owner_id = Column(String, ForeignKey("user.id", ondelete="CASCADE"), nullable=False)
# Relationships
owner = relationship("User", back_populates="tasks")

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

@ -0,0 +1,18 @@
from sqlalchemy import Boolean, Column, String, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.db.base_class import Base
class User(Base):
id = Column(String, 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)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationship with tasks
tasks = relationship("Task", back_populates="owner", cascade="all, delete-orphan")

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

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

@ -0,0 +1,47 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
# Shared properties
class TaskBase(BaseModel):
title: str
description: Optional[str] = None
priority: int = Field(1, ge=1, le=3) # 1=Low, 2=Medium, 3=High
due_date: Optional[datetime] = None
is_completed: bool = False
# Properties to receive via API on creation
class TaskCreate(TaskBase):
pass
# Properties to receive via API on update
class TaskUpdate(BaseModel):
title: Optional[str] = None
description: Optional[str] = None
priority: Optional[int] = Field(None, ge=1, le=3)
due_date: Optional[datetime] = None
is_completed: Optional[bool] = None
class TaskInDBBase(TaskBase):
id: str
owner_id: str
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Additional properties to return via API
class Task(TaskInDBBase):
pass
# Additional properties stored in DB
class TaskInDB(TaskInDBBase):
pass

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

@ -0,0 +1,12 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
class TokenPayload(BaseModel):
sub: Optional[str] = None

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

@ -0,0 +1,43 @@
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, EmailStr
# Shared properties
class UserBase(BaseModel):
email: EmailStr
username: str
is_active: Optional[bool] = True
# Properties to receive via API on creation
class UserCreate(UserBase):
password: str
# Properties to receive via API on update
class UserUpdate(BaseModel):
email: Optional[EmailStr] = None
username: Optional[str] = None
password: Optional[str] = None
is_active: Optional[bool] = None
class UserInDBBase(UserBase):
id: str
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Additional properties to return via API
class User(UserInDBBase):
pass
# Additional properties stored in DB
class UserInDB(UserInDBBase):
hashed_password: str

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

43
main.py Normal file
View File

@ -0,0 +1,43 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.api import router as api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description="Task Management API",
version="0.1.0",
openapi_url="/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Set up CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/")
async def root():
"""Root endpoint that provides basic service information"""
return {
"title": settings.PROJECT_NAME,
"docs": f"{settings.API_V1_STR}/docs",
"health": "/health"
}
@app.get("/health", status_code=200)
async def health_check():
"""Health check endpoint"""
return {"status": "healthy"}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

0
migrations/__init__.py Normal file
View File

85
migrations/env.py Normal file
View File

@ -0,0 +1,85 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# Import Base for metadata
from app.db.base 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.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
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() -> None:
"""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"},
render_as_batch=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""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
)
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() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

View File

@ -0,0 +1,62 @@
"""Initial migration
Revision ID: a1b2c3d4e5f6
Revises:
Create Date: 2023-11-09 12:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a1b2c3d4e5f6'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create user table
op.create_table(
'user',
sa.Column('id', sa.String(), 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('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True)
# Create task table
op.create_table(
'task',
sa.Column('id', sa.String(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('priority', sa.Integer(), nullable=False, default=1),
sa.Column('due_date', sa.DateTime(timezone=True), nullable=True),
sa.Column('is_completed', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('owner_id', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_task_id'), 'task', ['id'], unique=False)
op.create_index(op.f('ix_task_title'), 'task', ['title'], unique=False)
def downgrade() -> None:
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')
op.drop_index(op.f('ix_user_username'), table_name='user')
op.drop_index(op.f('ix_user_id'), table_name='user')
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')

12
requirements.txt Normal file
View File

@ -0,0 +1,12 @@
fastapi==0.104.1
uvicorn==0.23.2
sqlalchemy==2.0.23
alembic==1.12.1
pydantic==2.4.2
pydantic-settings==2.0.3
python-jose==3.3.0
passlib==1.7.4
python-multipart==0.0.6
bcrypt==4.0.1
email-validator==2.1.0
ruff==0.1.3