diff --git a/alembic/versions/20250411_201603_e03622fd_update_fruit.py b/alembic/versions/20250411_201603_e03622fd_update_fruit.py new file mode 100644 index 0000000..7e46e0d --- /dev/null +++ b/alembic/versions/20250411_201603_e03622fd_update_fruit.py @@ -0,0 +1,31 @@ +"""Initial creation of fruits table +Revision ID: a1b2c3d4e5f6 +Revises: 0002 +Create Date: 2024-01-20 10:00:00.000000 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +import uuid + +# revision identifiers, used by Alembic. +revision = 'e03622fd' +down_revision = '0002' +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table( + 'fruits', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4), + sa.Column('name', sa.String(), nullable=False, unique=True), + sa.Column('description', sa.String(), nullable=True), + sa.Column('is_active', sa.Boolean(), server_default=sa.text('true')), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now()) + ) + op.create_index('ix_fruits_name', 'fruits', ['name']) + +def downgrade(): + op.drop_index('ix_fruits_name') + op.drop_table('fruits') \ No newline at end of file diff --git a/endpoints/fruits.get.py b/endpoints/fruits.get.py new file mode 100644 index 0000000..c000657 --- /dev/null +++ b/endpoints/fruits.get.py @@ -0,0 +1,9 @@ +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/fruits", status_code=200) +async def get_fruits(): + """Get all fruits""" + fruits = ["apple", "banana", "orange", "grape", "kiwi"] + return fruits \ No newline at end of file diff --git a/helpers/fruit_helpers.py b/helpers/fruit_helpers.py new file mode 100644 index 0000000..533e272 --- /dev/null +++ b/helpers/fruit_helpers.py @@ -0,0 +1,147 @@ +from typing import List, Optional, Dict, Any +from uuid import UUID +from sqlalchemy.orm import Session + +from models.fruit import Fruit +from schemas.fruit import FruitCreate, FruitUpdate + +def get_all_fruits(db: Session, include_inactive: bool = False) -> List[Fruit]: + """ + Retrieves all fruits from the database. + + Args: + db (Session): The database session. + include_inactive (bool): Whether to include inactive fruits. + + Returns: + List[Fruit]: A list of all fruit objects. + """ + query = db.query(Fruit) + if not include_inactive: + query = query.filter(Fruit.is_active.is_(True)) + return query.all() + +def get_fruit_by_id(db: Session, fruit_id: UUID) -> Optional[Fruit]: + """ + Retrieves a single fruit by its ID. + + Args: + db (Session): The database session. + fruit_id (UUID): The ID of the fruit to retrieve. + + Returns: + Optional[Fruit]: The fruit object if found, otherwise None. + """ + return db.query(Fruit).filter(Fruit.id == fruit_id).first() + +def get_fruit_by_name(db: Session, name: str) -> Optional[Fruit]: + """ + Retrieves a single fruit by its name. + + Args: + db (Session): The database session. + name (str): The name of the fruit to retrieve. + + Returns: + Optional[Fruit]: The fruit object if found, otherwise None. + """ + return db.query(Fruit).filter(Fruit.name == name).first() + +def create_fruit(db: Session, fruit_data: FruitCreate) -> Fruit: + """ + Creates a new fruit in the database. + + Args: + db (Session): The database session. + fruit_data (FruitCreate): The data for the fruit to create. + + Returns: + Fruit: The newly created fruit object. + """ + db_fruit = Fruit(**fruit_data.dict()) + db.add(db_fruit) + db.commit() + db.refresh(db_fruit) + return db_fruit + +def update_fruit(db: Session, fruit_id: UUID, fruit_data: FruitUpdate) -> Optional[Fruit]: + """ + Updates an existing fruit in the database. + + Args: + db (Session): The database session. + fruit_id (UUID): The ID of the fruit to update. + fruit_data (FruitUpdate): The data to update the fruit with. + + Returns: + Optional[Fruit]: The updated fruit object if found, otherwise None. + """ + db_fruit = get_fruit_by_id(db, fruit_id) + if not db_fruit: + return None + + update_data = fruit_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(db_fruit, field, value) + + db.commit() + db.refresh(db_fruit) + return db_fruit + +def delete_fruit(db: Session, fruit_id: UUID) -> bool: + """ + Deletes a fruit from the database. + + Args: + db (Session): The database session. + fruit_id (UUID): The ID of the fruit to delete. + + Returns: + bool: True if the fruit was deleted, False if not found. + """ + db_fruit = get_fruit_by_id(db, fruit_id) + if not db_fruit: + return False + + db.delete(db_fruit) + db.commit() + return True + +def soft_delete_fruit(db: Session, fruit_id: UUID) -> bool: + """ + Soft deletes a fruit by setting is_active to False. + + Args: + db (Session): The database session. + fruit_id (UUID): The ID of the fruit to soft delete. + + Returns: + bool: True if the fruit was soft deleted, False if not found. + """ + db_fruit = get_fruit_by_id(db, fruit_id) + if not db_fruit: + return False + + db_fruit.is_active = False + db.commit() + return True + +def validate_fruit_data(data: Dict[str, Any]) -> bool: + """ + Validates fruit input data dictionary. + + Args: + data (Dict[str, Any]): The input data to validate. + + Returns: + bool: True if the data is valid, False otherwise. + """ + if not data: + return False + if "name" not in data or not isinstance(data["name"], str) or len(data["name"]) < 1: + return False + if "description" in data and not isinstance(data["description"], (str, type(None))): + return False + if "is_active" in data and not isinstance(data["is_active"], bool): + return False + return True \ No newline at end of file diff --git a/models/fruit.py b/models/fruit.py new file mode 100644 index 0000000..764ed44 --- /dev/null +++ b/models/fruit.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, String, DateTime, Boolean +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func +from core.database import Base +import uuid + +class Fruit(Base): + __tablename__ = "fruits" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, unique=True, nullable=False, index=True) + description = Column(String, nullable=True) + is_active = Column(Boolean, default=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) \ No newline at end of file diff --git a/schemas/fruit.py b/schemas/fruit.py new file mode 100644 index 0000000..0552612 --- /dev/null +++ b/schemas/fruit.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from uuid import UUID + +class FruitBase(BaseModel): + name: str = Field(..., min_length=1, description="Fruit name") + description: Optional[str] = Field(None, description="Fruit description") + is_active: bool = Field(True, description="Whether the fruit is active") + +class FruitCreate(FruitBase): + pass + +class FruitUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, description="Fruit name") + description: Optional[str] = Field(None, description="Fruit description") + is_active: Optional[bool] = Field(None, description="Whether the fruit is active") + +class FruitSchema(FruitBase): + id: UUID + created_at: datetime + updated_at: datetime + + class Config: + orm_mode = True + schema_extra = { + "example": { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "name": "Apple", + "description": "A sweet, edible fruit", + "is_active": True, + "created_at": "2023-01-01T12:00:00", + "updated_at": "2023-01-01T12:00:00" + } + } \ No newline at end of file