From c9baaa994e1f74c615b5b9deeccf45cbb9839378 Mon Sep 17 00:00:00 2001 From: Automated Action Date: Fri, 20 Jun 2025 19:35:55 +0000 Subject: [PATCH] Create comprehensive Task Manager API with FastAPI - Add user authentication with JWT tokens - Implement task CRUD operations with status and priority - Set up SQLite database with SQLAlchemy ORM - Create Alembic migrations for database schema - Add comprehensive API documentation - Include health check endpoint and CORS configuration - Structure codebase with proper separation of concerns --- README.md | 176 +++++++++++++++++++++- alembic.ini | 41 +++++ alembic/env.py | 49 ++++++ alembic/script.py.mako | 24 +++ alembic/versions/001_initial_migration.py | 79 ++++++++++ app/__init__.py | 0 app/api/__init__.py | 0 app/api/auth.py | 37 +++++ app/api/tasks.py | 79 ++++++++++ app/api/users.py | 23 +++ app/core/__init__.py | 0 app/core/auth.py | 37 +++++ app/core/config.py | 16 ++ app/core/security.py | 41 +++++ app/db/__init__.py | 0 app/db/base.py | 3 + app/db/session.py | 22 +++ app/models/__init__.py | 0 app/models/task.py | 45 ++++++ app/models/user.py | 18 +++ app/schemas/__init__.py | 0 app/schemas/task.py | 36 +++++ app/schemas/user.py | 42 ++++++ app/services/__init__.py | 0 app/services/task.py | 64 ++++++++ app/services/user.py | 53 +++++++ main.py | 46 ++++++ requirements.txt | 9 ++ 28 files changed, 938 insertions(+), 2 deletions(-) create mode 100644 alembic.ini create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/001_initial_migration.py create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/auth.py create mode 100644 app/api/tasks.py create mode 100644 app/api/users.py create mode 100644 app/core/__init__.py create mode 100644 app/core/auth.py create mode 100644 app/core/config.py create mode 100644 app/core/security.py create mode 100644 app/db/__init__.py create mode 100644 app/db/base.py create mode 100644 app/db/session.py create mode 100644 app/models/__init__.py create mode 100644 app/models/task.py create mode 100644 app/models/user.py create mode 100644 app/schemas/__init__.py create mode 100644 app/schemas/task.py create mode 100644 app/schemas/user.py create mode 100644 app/services/__init__.py create mode 100644 app/services/task.py create mode 100644 app/services/user.py create mode 100644 main.py create mode 100644 requirements.txt diff --git a/README.md b/README.md index e8acfba..eb73628 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,175 @@ -# FastAPI Application +# Task Manager API -This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform. +A comprehensive Task Manager API built with FastAPI, featuring user authentication, task management, and SQLite database integration. + +## Features + +- User registration and authentication with JWT tokens +- Task CRUD operations (Create, Read, Update, Delete) +- Task filtering by status and priority +- User profile management +- SQLite database with SQLAlchemy ORM +- Database migrations with Alembic +- API documentation with Swagger UI +- Health check endpoint +- CORS enabled for all origins + +## Project Structure + +``` +├── main.py # FastAPI application entry point +├── requirements.txt # Python dependencies +├── alembic.ini # Alembic configuration +├── alembic/ # Database migrations +├── app/ +│ ├── api/ # API route handlers +│ │ ├── auth.py # Authentication endpoints +│ │ ├── tasks.py # Task management endpoints +│ │ └── users.py # User management endpoints +│ ├── core/ # Core application logic +│ │ ├── auth.py # Authentication dependencies +│ │ ├── config.py # Application configuration +│ │ └── security.py # Security utilities +│ ├── db/ # Database configuration +│ │ ├── base.py # SQLAlchemy base class +│ │ └── session.py # Database session management +│ ├── models/ # SQLAlchemy models +│ │ ├── task.py # Task model +│ │ └── user.py # User model +│ ├── schemas/ # Pydantic schemas +│ │ ├── task.py # Task schemas +│ │ └── user.py # User schemas +│ └── services/ # Business logic layer +│ ├── task.py # Task service +│ └── user.py # User service +└── storage/ + └── db/ # SQLite database storage +``` + +## Installation + +1. Install dependencies: +```bash +pip install -r requirements.txt +``` + +2. Run database migrations: +```bash +alembic upgrade head +``` + +3. Start the application: +```bash +uvicorn main:app --reload +``` + +The API will be available at `http://localhost:8000` + +## Environment Variables + +Set the following environment variables for production: + +- `SECRET_KEY`: Secret key for JWT token signing (default: "your-secret-key-here") + +Example: +```bash +export SECRET_KEY="your-super-secret-key-here" +``` + +## API Endpoints + +### Authentication +- `POST /auth/register` - Register a new user +- `POST /auth/login` - Login and get access token + +### Tasks +- `GET /tasks/` - Get all tasks for authenticated 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 + +### Users +- `GET /users/me` - Get current user profile +- `PUT /users/me` - Update current user profile + +### System +- `GET /` - API information +- `GET /health` - Health check endpoint + +## API Documentation + +- Swagger UI: `http://localhost:8000/docs` +- ReDoc: `http://localhost:8000/redoc` +- OpenAPI JSON: `http://localhost:8000/openapi.json` + +## Task Management + +### Task Status Options +- `pending` - Task is pending +- `in_progress` - Task is in progress +- `completed` - Task is completed +- `cancelled` - Task is cancelled + +### Task Priority Options +- `low` - Low priority +- `medium` - Medium priority +- `high` - High priority +- `urgent` - Urgent priority + +## Authentication + +The API uses JWT (JSON Web Tokens) for authentication. After registration or login, include the token in the Authorization header: + +``` +Authorization: Bearer +``` + +## Database + +The application uses SQLite as the database with the following features: +- Database file stored at `/app/storage/db/db.sqlite` +- Automatic table creation on startup +- Database migrations managed by Alembic +- Relationships between users and tasks + +## Development + +### Running Tests +To run tests (when available): +```bash +pytest +``` + +### Code Formatting +The project uses Ruff for code formatting and linting: +```bash +ruff check . +ruff format . +``` + +### Database Operations +Create a new migration: +```bash +alembic revision --autogenerate -m "description" +``` + +Apply migrations: +```bash +alembic upgrade head +``` + +## Health Check + +The application provides a health check endpoint at `/health` that returns: +```json +{ + "status": "healthy", + "service": "Task Manager API", + "version": "1.0.0" +} +``` + +## License + +This project is generated by BackendIM, the AI-powered backend generation platform. \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..017f263 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,41 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +version_path_separator = os +sqlalchemy.url = sqlite:////app/storage/db/db.sqlite + +[post_write_hooks] + +[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 \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..7acb267 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,49 @@ +from logging.config import fileConfig +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from alembic import context +import sys +import os + +sys.path.append(os.path.dirname(os.path.dirname(__file__))) + +from app.db.base import Base + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + 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() -> None: + 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() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..37d0cac --- /dev/null +++ b/alembic/script.py.mako @@ -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"} \ No newline at end of file diff --git a/alembic/versions/001_initial_migration.py b/alembic/versions/001_initial_migration.py new file mode 100644 index 0000000..51d9ab0 --- /dev/null +++ b/alembic/versions/001_initial_migration.py @@ -0,0 +1,79 @@ +"""Initial migration + +Revision ID: 001 +Revises: +Create Date: 2024-01-01 12:00:00.000000 + +""" + +from alembic import op +import sqlalchemy as sa + +revision = "001" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "users", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("email", 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), + 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_users_email"), "users", ["email"], unique=True) + op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False) + + 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( + "PENDING", "IN_PROGRESS", "COMPLETED", "CANCELLED", name="taskstatus" + ), + nullable=True, + ), + sa.Column( + "priority", + sa.Enum("LOW", "MEDIUM", "HIGH", "URGENT", name="taskpriority"), + nullable=True, + ), + sa.Column("is_completed", sa.Boolean(), nullable=True), + sa.Column("due_date", sa.DateTime(timezone=True), 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.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["owner_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_tasks_id"), "tasks", ["id"], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f("ix_tasks_id"), table_name="tasks") + op.drop_table("tasks") + 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") diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/auth.py b/app/api/auth.py new file mode 100644 index 0000000..7c16370 --- /dev/null +++ b/app/api/auth.py @@ -0,0 +1,37 @@ +from datetime import timedelta +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from app.db.session import get_db +from app.schemas.user import UserCreate, User, Token +from app.services.user import create_user, authenticate_user, get_user_by_email +from app.core.security import create_access_token +from app.core.config import settings + +router = APIRouter() + + +@router.post("/register", response_model=User) +def register(user: UserCreate, db: Session = Depends(get_db)): + db_user = get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + return create_user(db=db, user=user) + + +@router.post("/login", response_model=Token) +def login( + form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db) +): + user = authenticate_user(db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=settings.access_token_expire_minutes) + access_token = create_access_token( + data={"sub": user.email}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} diff --git a/app/api/tasks.py b/app/api/tasks.py new file mode 100644 index 0000000..f8f6124 --- /dev/null +++ b/app/api/tasks.py @@ -0,0 +1,79 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from app.db.session import get_db +from app.schemas.task import Task, TaskCreate, TaskUpdate +from app.services.task import ( + get_tasks_by_user, + get_task_by_id, + create_task, + update_task, + delete_task, + get_tasks_by_status, +) +from app.core.auth import get_current_active_user +from app.models.user import User +from app.models.task import TaskStatus + +router = APIRouter() + + +@router.get("/", response_model=List[Task]) +def read_tasks( + skip: int = 0, + limit: int = 100, + status: TaskStatus = None, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + if status: + return get_tasks_by_status(db, user_id=current_user.id, status=status) + return get_tasks_by_user(db, user_id=current_user.id, skip=skip, limit=limit) + + +@router.post("/", response_model=Task) +def create_new_task( + task: TaskCreate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + return create_task(db, task=task, user_id=current_user.id) + + +@router.get("/{task_id}", response_model=Task) +def read_task( + task_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + db_task = get_task_by_id(db, task_id=task_id, user_id=current_user.id) + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found") + return db_task + + +@router.put("/{task_id}", response_model=Task) +def update_existing_task( + task_id: int, + task: TaskUpdate, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + db_task = update_task( + db, task_id=task_id, task_update=task, user_id=current_user.id + ) + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found") + return db_task + + +@router.delete("/{task_id}") +def delete_existing_task( + task_id: int, + current_user: User = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + success = delete_task(db, task_id=task_id, user_id=current_user.id) + if not success: + raise HTTPException(status_code=404, detail="Task not found") + return {"message": "Task deleted successfully"} diff --git a/app/api/users.py b/app/api/users.py new file mode 100644 index 0000000..82f1669 --- /dev/null +++ b/app/api/users.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.db.session import get_db +from app.schemas.user import User, UserUpdate +from app.services.user import update_user +from app.core.auth import get_current_active_user +from app.models.user import User as UserModel + +router = APIRouter() + + +@router.get("/me", response_model=User) +def read_current_user(current_user: UserModel = Depends(get_current_active_user)): + return current_user + + +@router.put("/me", response_model=User) +def update_current_user( + user_update: UserUpdate, + current_user: UserModel = Depends(get_current_active_user), + db: Session = Depends(get_db), +): + return update_user(db, user_id=current_user.id, user_update=user_update) diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/auth.py b/app/core/auth.py new file mode 100644 index 0000000..0bc74c5 --- /dev/null +++ b/app/core/auth.py @@ -0,0 +1,37 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from app.db.session import get_db +from app.core.security import verify_token +from app.services.user import get_user_by_email +from app.models.user import User + +security = HTTPBearer() + + +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db), +) -> User: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + token = credentials.credentials + email = verify_token(token) + if email is None: + raise credentials_exception + + user = get_user_by_email(db, email=email) + if user is None: + raise credentials_exception + + return user + + +def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: + if not current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..df82382 --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,16 @@ +import os +from pydantic import BaseSettings + + +class Settings(BaseSettings): + secret_key: str = os.getenv("SECRET_KEY", "your-secret-key-here") + algorithm: str = "HS256" + access_token_expire_minutes: int = 30 + app_name: str = "Task Manager API" + app_version: str = "1.0.0" + + class Config: + env_file = ".env" + + +settings = Settings() diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..15b386b --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,41 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from app.core.config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +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) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, settings.secret_key, algorithm=settings.algorithm + ) + return encoded_jwt + + +def verify_token(token: str): + try: + payload = jwt.decode( + token, settings.secret_key, algorithms=[settings.algorithm] + ) + email: str = payload.get("sub") + if email is None: + return None + return email + except JWTError: + return None diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/base.py b/app/db/base.py new file mode 100644 index 0000000..860e542 --- /dev/null +++ b/app/db/base.py @@ -0,0 +1,3 @@ +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() diff --git a/app/db/session.py b/app/db/session.py new file mode 100644 index 0000000..fbff7a0 --- /dev/null +++ b/app/db/session.py @@ -0,0 +1,22 @@ +from pathlib import Path +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +DB_DIR = Path("/app") / "storage" / "db" +DB_DIR.mkdir(parents=True, exist_ok=True) + +SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/models/task.py b/app/models/task.py new file mode 100644 index 0000000..6894935 --- /dev/null +++ b/app/models/task.py @@ -0,0 +1,45 @@ +from sqlalchemy import ( + Column, + Integer, + String, + Text, + Boolean, + DateTime, + ForeignKey, + Enum, +) +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.db.base import Base +import enum + + +class TaskStatus(enum.Enum): + PENDING = "pending" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" + + +class TaskPriority(enum.Enum): + LOW = "low" + MEDIUM = "medium" + HIGH = "high" + URGENT = "urgent" + + +class Task(Base): + __tablename__ = "tasks" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, nullable=False) + description = Column(Text, nullable=True) + status = Column(Enum(TaskStatus), default=TaskStatus.PENDING) + priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM) + is_completed = Column(Boolean, default=False) + due_date = Column(DateTime(timezone=True), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + owner_id = Column(Integer, ForeignKey("users.id"), nullable=False) + + owner = relationship("User", back_populates="tasks") diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..636e68f --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from app.db.base import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = 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) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now()) + + tasks = relationship("Task", back_populates="owner") diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/task.py b/app/schemas/task.py new file mode 100644 index 0000000..c3fded6 --- /dev/null +++ b/app/schemas/task.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime +from app.models.task import TaskStatus, TaskPriority + + +class TaskBase(BaseModel): + title: str + description: Optional[str] = None + status: TaskStatus = TaskStatus.PENDING + priority: TaskPriority = TaskPriority.MEDIUM + due_date: Optional[datetime] = None + + +class TaskCreate(TaskBase): + pass + + +class TaskUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + status: Optional[TaskStatus] = None + priority: Optional[TaskPriority] = None + due_date: Optional[datetime] = None + is_completed: Optional[bool] = None + + +class Task(TaskBase): + id: int + is_completed: bool + created_at: datetime + updated_at: Optional[datetime] = None + owner_id: int + + class Config: + from_attributes = True diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..c2a0f00 --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,42 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional +from datetime import datetime + + +class UserBase(BaseModel): + email: EmailStr + full_name: Optional[str] = None + + +class UserCreate(UserBase): + password: str + + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + full_name: Optional[str] = None + password: Optional[str] = None + + +class User(UserBase): + id: int + is_active: bool + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class Token(BaseModel): + access_token: str + token_type: str + + +class TokenData(BaseModel): + email: Optional[str] = None diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/task.py b/app/services/task.py new file mode 100644 index 0000000..f8b2b2c --- /dev/null +++ b/app/services/task.py @@ -0,0 +1,64 @@ +from sqlalchemy.orm import Session +from app.models.task import Task, TaskStatus +from app.schemas.task import TaskCreate, TaskUpdate +from typing import List, Optional + + +def get_tasks_by_user( + db: Session, user_id: int, skip: int = 0, limit: int = 100 +) -> List[Task]: + return ( + db.query(Task).filter(Task.owner_id == user_id).offset(skip).limit(limit).all() + ) + + +def get_task_by_id(db: Session, task_id: int, user_id: int) -> Optional[Task]: + return db.query(Task).filter(Task.id == task_id, Task.owner_id == user_id).first() + + +def create_task(db: Session, task: TaskCreate, user_id: int) -> Task: + db_task = Task(**task.dict(), owner_id=user_id) + db.add(db_task) + db.commit() + db.refresh(db_task) + return db_task + + +def update_task( + db: Session, task_id: int, task_update: TaskUpdate, user_id: int +) -> Optional[Task]: + db_task = get_task_by_id(db, task_id, user_id) + if not db_task: + return None + + update_data = task_update.dict(exclude_unset=True) + + if "status" in update_data: + if update_data["status"] == TaskStatus.COMPLETED: + update_data["is_completed"] = True + elif update_data["status"] != TaskStatus.COMPLETED and db_task.is_completed: + update_data["is_completed"] = False + + if "is_completed" in update_data and update_data["is_completed"]: + update_data["status"] = TaskStatus.COMPLETED + + for field, value in update_data.items(): + setattr(db_task, field, value) + + db.commit() + db.refresh(db_task) + return db_task + + +def delete_task(db: Session, task_id: int, user_id: int) -> bool: + db_task = get_task_by_id(db, task_id, user_id) + if not db_task: + return False + + db.delete(db_task) + db.commit() + return True + + +def get_tasks_by_status(db: Session, user_id: int, status: TaskStatus) -> List[Task]: + return db.query(Task).filter(Task.owner_id == user_id, Task.status == status).all() diff --git a/app/services/user.py b/app/services/user.py new file mode 100644 index 0000000..157437b --- /dev/null +++ b/app/services/user.py @@ -0,0 +1,53 @@ +from sqlalchemy.orm import Session +from app.models.user import User +from app.schemas.user import UserCreate, UserUpdate +from app.core.security import get_password_hash, verify_password +from typing import Optional + + +def get_user_by_email(db: Session, email: str) -> Optional[User]: + return db.query(User).filter(User.email == email).first() + + +def get_user_by_id(db: Session, user_id: int) -> Optional[User]: + return db.query(User).filter(User.id == user_id).first() + + +def create_user(db: Session, user: UserCreate) -> User: + hashed_password = get_password_hash(user.password) + db_user = User( + email=user.email, + hashed_password=hashed_password, + full_name=user.full_name, + ) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def authenticate_user(db: Session, email: str, password: str) -> Optional[User]: + user = get_user_by_email(db, email) + if not user: + return None + if not verify_password(password, user.hashed_password): + return None + return user + + +def update_user(db: Session, user_id: int, user_update: UserUpdate) -> Optional[User]: + db_user = get_user_by_id(db, user_id) + if not db_user: + return None + + update_data = user_update.dict(exclude_unset=True) + + if "password" in update_data: + update_data["hashed_password"] = get_password_hash(update_data.pop("password")) + + for field, value in update_data.items(): + setattr(db_user, field, value) + + db.commit() + db.refresh(db_user) + return db_user diff --git a/main.py b/main.py new file mode 100644 index 0000000..52d1242 --- /dev/null +++ b/main.py @@ -0,0 +1,46 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.api import auth, tasks, users +from app.core.config import settings +from app.db.session import engine +from app.db.base import Base + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + description="A comprehensive Task Manager API built with FastAPI", + openapi_url="/openapi.json", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +Base.metadata.create_all(bind=engine) + +app.include_router(auth.router, prefix="/auth", tags=["authentication"]) +app.include_router(tasks.router, prefix="/tasks", tags=["tasks"]) +app.include_router(users.router, prefix="/users", tags=["users"]) + + +@app.get("/") +def read_root(): + return { + "title": settings.app_name, + "version": settings.app_version, + "documentation": "/docs", + "health_check": "/health", + } + + +@app.get("/health") +def health_check(): + return { + "status": "healthy", + "service": settings.app_name, + "version": settings.app_version, + } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2a26868 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +sqlalchemy==2.0.23 +alembic==1.12.1 +pydantic==2.5.0 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 +ruff==0.1.6 \ No newline at end of file