diff --git a/app/api/v1/api.py b/app/api/v1/api.py index 807cd7a..ef200dc 100644 --- a/app/api/v1/api.py +++ b/app/api/v1/api.py @@ -5,4 +5,4 @@ from app.api.v1.endpoints import auth, tasks, users api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(users.router, prefix="/users", tags=["users"]) -api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"]) \ No newline at end of file +api_router.include_router(tasks.router, prefix="/tasks", tags=["tasks"]) diff --git a/app/api/v1/endpoints/tasks.py b/app/api/v1/endpoints/tasks.py index 182fb08..677c332 100644 --- a/app/api/v1/endpoints/tasks.py +++ b/app/api/v1/endpoints/tasks.py @@ -21,9 +21,7 @@ async def read_tasks( """ Retrieve tasks. """ - tasks = crud.task.get_multi_by_owner( - db=db, owner_id=current_user.id, skip=skip, limit=limit - ) + tasks = crud.task.get_multi_by_owner(db=db, owner_id=current_user.id, skip=skip, limit=limit) return tasks @@ -53,9 +51,7 @@ async def read_task( """ task = crud.task.get_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" - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found") return task @@ -72,9 +68,7 @@ async def update_task( """ task = crud.task.get_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" - ) + 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 @@ -91,8 +85,6 @@ async def delete_task( """ task = crud.task.get_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" - ) + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found") task = crud.task.remove(db=db, id=task_id) - return task \ No newline at end of file + return task diff --git a/app/api/v1/endpoints/users.py b/app/api/v1/endpoints/users.py index 931019e..763337c 100644 --- a/app/api/v1/endpoints/users.py +++ b/app/api/v1/endpoints/users.py @@ -76,4 +76,4 @@ async def read_users( detail="The user doesn't have enough privileges", ) users = crud.user.get_multi(db, skip=skip, limit=limit) - return users \ No newline at end of file + return users diff --git a/app/core/config/__init__.py b/app/core/config/__init__.py index 23b714c..84a6cc5 100644 --- a/app/core/config/__init__.py +++ b/app/core/config/__init__.py @@ -1,3 +1,3 @@ from .settings import settings -__all__ = ["settings"] \ No newline at end of file +__all__ = ["settings"] diff --git a/app/core/config/settings.py b/app/core/config/settings.py index 2c0d6d0..cda04e3 100644 --- a/app/core/config/settings.py +++ b/app/core/config/settings.py @@ -9,20 +9,18 @@ class Settings(BaseSettings): PROJECT_NAME: str = "Task Manager API" DESCRIPTION: str = "API for managing tasks and users" VERSION: str = "0.1.0" - + # Security SECRET_KEY: str = os.environ.get("SECRET_KEY", secrets.token_urlsafe(32)) # 8 days expiration ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.environ.get("ACCESS_TOKEN_EXPIRE_MINUTES", 60 * 24 * 8)) - + # Database - SQLALCHEMY_DATABASE_URL: str = os.environ.get( - "DATABASE_URL", "sqlite:////app/storage/db/db.sqlite" - ) - + SQLALCHEMY_DATABASE_URL: str = os.environ.get("DATABASE_URL", "sqlite:////app/storage/db/db.sqlite") + class Config: case_sensitive = True env_file = ".env" -settings = Settings() \ No newline at end of file +settings = Settings() diff --git a/app/core/security/__init__.py b/app/core/security/__init__.py index 6859300..3b247d0 100644 --- a/app/core/security/__init__.py +++ b/app/core/security/__init__.py @@ -6,10 +6,4 @@ from .security import ( verify_password, ) -__all__ = [ - "create_access_token", - "verify_password", - "get_password_hash", - "get_current_user", - "get_current_active_user" -] \ No newline at end of file +__all__ = ["create_access_token", "verify_password", "get_password_hash", "get_current_user", "get_current_active_user"] diff --git a/app/core/security/security.py b/app/core/security/security.py index 5f637ad..441b59a 100644 --- a/app/core/security/security.py +++ b/app/core/security/security.py @@ -22,15 +22,11 @@ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login") -def create_access_token( - subject: Union[str, Any], expires_delta: timedelta = None -) -> str: +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=ACCESS_TOKEN_EXPIRE_MINUTES - ) + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode = {"exp": expire, "sub": str(subject)} encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -44,25 +40,22 @@ def get_password_hash(password: str) -> str: return pwd_context.hash(password) -async def get_current_user( - db: Session = Depends(get_db), token: str = Depends(oauth2_scheme) -) -> User: +async def get_current_user(db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)) -> User: credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: - payload = jwt.decode( - token, SECRET_KEY, algorithms=[ALGORITHM] - ) + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) token_data = TokenPayload(**payload) if token_data.sub is None: raise credentials_exception except JWTError as e: raise credentials_exception from e - + from app.crud import user as user_crud + user = user_crud.get(db, id=token_data.sub) if user is None: raise credentials_exception @@ -74,4 +67,4 @@ async def get_current_active_user( ) -> User: if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") - return current_user \ No newline at end of file + return current_user diff --git a/app/crud/__init__.py b/app/crud/__init__.py index 401b354..e5196df 100644 --- a/app/crud/__init__.py +++ b/app/crud/__init__.py @@ -1,4 +1,4 @@ from .crud_task import task from .crud_user import user -__all__ = ["user", "task"] \ No newline at end of file +__all__ = ["user", "task"] diff --git a/app/crud/base.py b/app/crud/base.py index d49a61c..9521c5a 100644 --- a/app/crud/base.py +++ b/app/crud/base.py @@ -26,9 +26,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): 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]: + 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: @@ -39,13 +37,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): db.refresh(db_obj) return db_obj - def update( - self, - db: Session, - *, - db_obj: ModelType, - obj_in: Union[UpdateSchemaType, Dict[str, Any]] - ) -> ModelType: + 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 @@ -63,4 +55,4 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): obj = db.query(self.model).get(id) db.delete(obj) db.commit() - return obj \ No newline at end of file + return obj diff --git a/app/crud/crud_task.py b/app/crud/crud_task.py index a0c6736..e534ba4 100644 --- a/app/crud/crud_task.py +++ b/app/crud/crud_task.py @@ -8,20 +8,10 @@ from app.schemas.task import TaskCreate, TaskUpdate class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]): - def get_multi_by_owner( - self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100 - ) -> List[Task]: - return ( - db.query(self.model) - .filter(Task.owner_id == owner_id) - .offset(skip) - .limit(limit) - .all() - ) + def get_multi_by_owner(self, db: Session, *, owner_id: int, skip: int = 0, limit: int = 100) -> List[Task]: + return db.query(self.model).filter(Task.owner_id == owner_id).offset(skip).limit(limit).all() - def create_with_owner( - self, db: Session, *, obj_in: TaskCreate, owner_id: int - ) -> Task: + def create_with_owner(self, db: Session, *, obj_in: TaskCreate, owner_id: int) -> Task: obj_in_data = obj_in.model_dump() db_obj = self.model(**obj_in_data, owner_id=owner_id) db.add(db_obj) @@ -29,14 +19,8 @@ class CRUDTask(CRUDBase[Task, TaskCreate, TaskUpdate]): db.refresh(db_obj) return db_obj - def get_by_id_and_owner( - self, db: Session, *, task_id: int, owner_id: int - ) -> Optional[Task]: - return ( - db.query(self.model) - .filter(Task.id == task_id, Task.owner_id == owner_id) - .first() - ) + def get_by_id_and_owner(self, db: Session, *, task_id: int, owner_id: int) -> Optional[Task]: + return db.query(self.model).filter(Task.id == task_id, Task.owner_id == owner_id).first() -task = CRUDTask(Task) \ No newline at end of file +task = CRUDTask(Task) diff --git a/app/crud/crud_user.py b/app/crud/crud_user.py index e98f1cf..145941e 100644 --- a/app/crud/crud_user.py +++ b/app/crud/crud_user.py @@ -11,7 +11,7 @@ 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() @@ -28,9 +28,7 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): db.refresh(db_obj) return db_obj - def update( - self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]] - ) -> User: + 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: @@ -56,4 +54,4 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]): return user.is_superuser -user = CRUDUser(User) \ No newline at end of file +user = CRUDUser(User) diff --git a/app/db/base.py b/app/db/base.py index 7c2377a..860e542 100644 --- a/app/db/base.py +++ b/app/db/base.py @@ -1,3 +1,3 @@ from sqlalchemy.ext.declarative import declarative_base -Base = declarative_base() \ No newline at end of file +Base = declarative_base() diff --git a/app/db/base_class.py b/app/db/base_class.py index 4b26c6e..8c1e9b6 100644 --- a/app/db/base_class.py +++ b/app/db/base_class.py @@ -8,7 +8,7 @@ class Base: id = Column(Integer, primary_key=True, index=True) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - + @declared_attr def __tablename__(cls) -> str: - return cls.__name__.lower() \ No newline at end of file + return cls.__name__.lower() diff --git a/app/db/base_imports.py b/app/db/base_imports.py index 7f51223..107cb88 100644 --- a/app/db/base_imports.py +++ b/app/db/base_imports.py @@ -2,4 +2,4 @@ # imported by Alembic from app.db.base import Base # noqa from app.models.user import User # noqa -from app.models.task import Task # noqa \ No newline at end of file +from app.models.task import Task # noqa diff --git a/app/db/init_db.py b/app/db/init_db.py index c97d7de..53cca52 100644 --- a/app/db/init_db.py +++ b/app/db/init_db.py @@ -23,4 +23,4 @@ def init_db(db: Session) -> None: password="adminpassword", is_superuser=True, ) - crud.user.create(db, obj_in=user_in) \ No newline at end of file + crud.user.create(db, obj_in=user_in) diff --git a/app/db/session.py b/app/db/session.py index edbc53b..1631592 100644 --- a/app/db/session.py +++ b/app/db/session.py @@ -11,10 +11,7 @@ DB_DIR.mkdir(parents=True, exist_ok=True) SQLALCHEMY_DATABASE_URL = settings.SQLALCHEMY_DATABASE_URL -engine = create_engine( - SQLALCHEMY_DATABASE_URL, - connect_args={"check_same_thread": False} -) +engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -23,4 +20,4 @@ def get_db(): try: yield db finally: - db.close() \ No newline at end of file + db.close() diff --git a/app/models/__init__.py b/app/models/__init__.py index fa70155..116e21f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,4 +1,4 @@ from .task import Task from .user import User -__all__ = ["User", "Task"] \ No newline at end of file +__all__ = ["User", "Task"] diff --git a/app/models/task.py b/app/models/task.py index 98e8105..bd2d596 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -25,9 +25,9 @@ class Task(Base, BaseClass): status = Column(Enum(TaskStatus), default=TaskStatus.TODO, nullable=False) priority = Column(Enum(TaskPriority), default=TaskPriority.MEDIUM, nullable=False) is_completed = Column(Boolean, default=False) - + # Foreign key to user owner_id = Column(Integer, ForeignKey("user.id"), nullable=False) - + # Relationship with user - owner = relationship("User", back_populates="tasks") \ No newline at end of file + owner = relationship("User", back_populates="tasks") diff --git a/app/models/user.py b/app/models/user.py index 115b676..8c99061 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -11,6 +11,6 @@ class User(Base, BaseClass): hashed_password = Column(String, nullable=False) is_active = Column(Boolean, default=True) is_superuser = Column(Boolean, default=False) - + # Relationship with tasks - tasks = relationship("Task", back_populates="owner", cascade="all, delete-orphan") \ No newline at end of file + tasks = relationship("Task", back_populates="owner", cascade="all, delete-orphan") diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 38a2635..5047f5c 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -3,7 +3,14 @@ from .token import Token, TokenPayload from .user import User, UserCreate, UserInDB, UserUpdate __all__ = [ - "User", "UserCreate", "UserUpdate", "UserInDB", - "Task", "TaskCreate", "TaskUpdate", "TaskInDB", - "Token", "TokenPayload" -] \ No newline at end of file + "User", + "UserCreate", + "UserUpdate", + "UserInDB", + "Task", + "TaskCreate", + "TaskUpdate", + "TaskInDB", + "Token", + "TokenPayload", +] diff --git a/app/schemas/task.py b/app/schemas/task.py index d585ed1..4437f76 100644 --- a/app/schemas/task.py +++ b/app/schemas/task.py @@ -46,4 +46,4 @@ class Task(TaskInDBBase): # Properties stored in DB class TaskInDB(TaskInDBBase): - pass \ No newline at end of file + pass diff --git a/app/schemas/user.py b/app/schemas/user.py index 1ca8708..9fefc37 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -40,4 +40,4 @@ class User(UserInDBBase): # Additional properties stored in DB class UserInDB(UserInDBBase): - hashed_password: str \ No newline at end of file + hashed_password: str diff --git a/main.py b/main.py index 6582bc1..6b0fa5c 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,3 @@ - import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -27,19 +26,18 @@ app.add_middleware( # Include API router app.include_router(api_router, prefix=settings.API_V1_STR) + @app.get("/") async def root(): """Root endpoint returning service information""" - return { - "title": settings.PROJECT_NAME, - "docs": "/docs", - "health": "/health" - } + return {"title": settings.PROJECT_NAME, "docs": "/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) \ No newline at end of file + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) diff --git a/migrations/env.py b/migrations/env.py index c4734d1..3f42550 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,4 +1,3 @@ - import os import sys from logging.config import fileConfig @@ -88,4 +87,4 @@ def run_migrations_online(): if context.is_offline_mode(): run_migrations_offline() else: - run_migrations_online() \ No newline at end of file + run_migrations_online() diff --git a/migrations/versions/init_db.py b/migrations/versions/init_db.py index 904e493..fb3e0a8 100644 --- a/migrations/versions/init_db.py +++ b/migrations/versions/init_db.py @@ -1,15 +1,16 @@ """init db Revision ID: 001_init_db -Revises: +Revises: Create Date: 2023-11-14 12:00:00.000000 """ + import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = '001_init_db' +revision = "001_init_db" down_revision = None branch_labels = None depends_on = None @@ -18,51 +19,54 @@ depends_on = None def upgrade(): # Create users table op.create_table( - 'user', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.Column('updated_at', sa.DateTime(), nullable=True), - 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('is_superuser', sa.Boolean(), nullable=True), - sa.PrimaryKeyConstraint('id') + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + 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("is_superuser", sa.Boolean(), 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) + 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 tasks table op.create_table( - 'task', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=True), - sa.Column('updated_at', sa.DateTime(), nullable=True), - sa.Column('title', sa.String(100), nullable=False), - sa.Column('description', sa.Text(), nullable=True), - sa.Column('status', sa.Enum('todo', 'in_progress', 'done', name='taskstatus'), nullable=False), - sa.Column('priority', sa.Enum('low', 'medium', 'high', name='taskpriority'), nullable=False), - sa.Column('is_completed', sa.Boolean(), nullable=True), - sa.Column('owner_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ), - sa.PrimaryKeyConstraint('id') + "task", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.Column("title", sa.String(100), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("status", sa.Enum("todo", "in_progress", "done", name="taskstatus"), nullable=False), + sa.Column("priority", sa.Enum("low", "medium", "high", name="taskpriority"), nullable=False), + sa.Column("is_completed", sa.Boolean(), nullable=True), + sa.Column("owner_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["owner_id"], + ["user.id"], + ), + 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) + 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(): # Drop tasks table - 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_task_title"), table_name="task") + op.drop_index(op.f("ix_task_id"), table_name="task") + op.drop_table("task") + # Drop enum types (only works in PostgreSQL, not SQLite) # No need to drop enums in SQLite - + # Drop users table - 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') \ No newline at end of file + 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")