diff --git a/alembic/versions/20250411_201618_8c9c9d7b_update_fruit.py b/alembic/versions/20250411_201618_8c9c9d7b_update_fruit.py new file mode 100644 index 0000000..26e91e3 --- /dev/null +++ b/alembic/versions/20250411_201618_8c9c9d7b_update_fruit.py @@ -0,0 +1,30 @@ +"""create 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 = '8c9c9d7b' +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), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()) + ) + op.create_index(op.f('ix_fruits_name'), 'fruits', ['name']) + +def downgrade(): + op.drop_index(op.f('ix_fruits_name'), 'fruits') + op.drop_table('fruits') \ No newline at end of file diff --git a/endpoints/fruits.get.py b/endpoints/fruits.get.py index c000657..9467b43 100644 --- a/endpoints/fruits.get.py +++ b/endpoints/fruits.get.py @@ -1,9 +1,19 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from typing import List +from core.database import get_db +from schemas.fruit import FruitSchema +from helpers.fruit_helpers import get_all_fruits router = APIRouter() -@router.get("/fruits", status_code=200) -async def get_fruits(): - """Get all fruits""" - fruits = ["apple", "banana", "orange", "grape", "kiwi"] +@router.get("/fruits", response_model=List[FruitSchema]) +async def get_fruits( + skip: int = 0, + limit: int = 100, + sort_by: str = "created_at", + sort_desc: bool = True, + db: Session = Depends(get_db) +): + fruits = get_all_fruits(db, skip=skip, limit=limit, sort_by=sort_by, sort_desc=sort_desc) return fruits \ No newline at end of file diff --git a/helpers/fruit_helpers.py b/helpers/fruit_helpers.py index 533e272..4e43b61 100644 --- a/helpers/fruit_helpers.py +++ b/helpers/fruit_helpers.py @@ -1,62 +1,62 @@ from typing import List, Optional, Dict, Any from uuid import UUID from sqlalchemy.orm import Session +from sqlalchemy import desc from models.fruit import Fruit from schemas.fruit import FruitCreate, FruitUpdate -def get_all_fruits(db: Session, include_inactive: bool = False) -> List[Fruit]: +def get_all_fruits( + db: Session, + skip: int = 0, + limit: int = 100, + sort_by: str = "created_at", + sort_desc: bool = True +) -> List[Fruit]: """ - Retrieves all fruits from the database. + Retrieves all fruits from the database with pagination and sorting. Args: - db (Session): The database session. - include_inactive (bool): Whether to include inactive fruits. + db (Session): The database session + skip (int): Number of records to skip + limit (int): Maximum number of records to return + sort_by (str): Field to sort by + sort_desc (bool): Sort in descending order if True Returns: - List[Fruit]: A list of all fruit objects. + List[Fruit]: List of fruit objects """ query = db.query(Fruit) - if not include_inactive: - query = query.filter(Fruit.is_active.is_(True)) - return query.all() + + if hasattr(Fruit, sort_by): + order_criteria = desc(getattr(Fruit, sort_by)) if sort_desc else getattr(Fruit, sort_by) + query = query.order_by(order_criteria) + + return query.offset(skip).limit(limit).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. + 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. + 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. + db (Session): The database session + fruit_data (FruitCreate): The data for the fruit to create Returns: - Fruit: The newly created fruit object. + Fruit: The newly created fruit object """ db_fruit = Fruit(**fruit_data.dict()) db.add(db_fruit) @@ -69,21 +69,21 @@ def update_fruit(db: Session, fruit_id: UUID, fruit_data: FruitUpdate) -> Option 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. + db (Session): The database session + fruit_id (UUID): The ID of the fruit to update + fruit_data (FruitUpdate): The update data Returns: - Optional[Fruit]: The updated fruit object if found, otherwise None. + 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 @@ -93,55 +93,54 @@ 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. + 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. + bool: True if deletion was successful, False if fruit wasn't 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: +def search_fruits_by_name(db: Session, name: str, limit: int = 10) -> List[Fruit]: """ - Soft deletes a fruit by setting is_active to False. + Searches for fruits by name (case-insensitive partial match). Args: - db (Session): The database session. - fruit_id (UUID): The ID of the fruit to soft delete. + db (Session): The database session + name (str): The name to search for + limit (int): Maximum number of results to return Returns: - bool: True if the fruit was soft deleted, False if not found. + List[Fruit]: List of matching fruit objects """ - db_fruit = get_fruit_by_id(db, fruit_id) - if not db_fruit: - return False - - db_fruit.is_active = False - db.commit() - return True + return db.query(Fruit).filter( + Fruit.name.ilike(f"%{name}%") + ).limit(limit).all() def validate_fruit_data(data: Dict[str, Any]) -> bool: """ - Validates fruit input data dictionary. + Validates fruit input data. Args: - data (Dict[str, Any]): The input data to validate. + data (Dict[str, Any]): The input data to validate Returns: - bool: True if the data is valid, False otherwise. + 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 + + if "description" in data and data["description"] is not None: + if not isinstance(data["description"], str): + return False + return True \ No newline at end of file diff --git a/models/fruit.py b/models/fruit.py index 764ed44..017ff57 100644 --- a/models/fruit.py +++ b/models/fruit.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, String, DateTime, Boolean +from sqlalchemy import Column, String, DateTime, Text from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.sql import func from core.database import Base @@ -8,8 +8,7 @@ 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) + name = Column(String, nullable=False, index=True) + description = Column(Text, nullable=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 index 0552612..f3a6423 100644 --- a/schemas/fruit.py +++ b/schemas/fruit.py @@ -4,17 +4,15 @@ 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") + name: str = Field(..., description="Name of the fruit") + description: Optional[str] = Field(None, description="Description of the fruit") 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") + name: Optional[str] = Field(None, description="Name of the fruit") + description: Optional[str] = Field(None, description="Description of the fruit") class FruitSchema(FruitBase): id: UUID @@ -27,8 +25,7 @@ class FruitSchema(FruitBase): "example": { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "name": "Apple", - "description": "A sweet, edible fruit", - "is_active": True, + "description": "A sweet and crunchy fruit", "created_at": "2023-01-01T12:00:00", "updated_at": "2023-01-01T12:00:00" }