diff --git a/alembic/versions/20250413_221758_0f5510ea_update_grocery.py b/alembic/versions/20250413_221758_0f5510ea_update_grocery.py new file mode 100644 index 0000000..cf19cec --- /dev/null +++ b/alembic/versions/20250413_221758_0f5510ea_update_grocery.py @@ -0,0 +1,27 @@ +"""create groceries table +Revision ID: b8c2316d +Revises: a7b1205a +Create Date: 2024-01-23 10:30:00.000000 +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'b8c2316d' +down_revision = 'a7b1205a' +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table( + 'groceries', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('name', sa.String(), nullable=False), + 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(op.f('ix_groceries_name'), 'groceries', ['name'], unique=False) + +def downgrade(): + op.drop_index(op.f('ix_groceries_name'), table_name='groceries') + op.drop_table('groceries') \ No newline at end of file diff --git a/endpoints/create-grocery.post.py b/endpoints/create-grocery.post.py index e69de29..118f1bd 100644 --- a/endpoints/create-grocery.post.py +++ b/endpoints/create-grocery.post.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from core.database import get_db +from schemas.grocery import GroceryCreate, GrocerySchema +from helpers.grocery_helpers import create_grocery, get_grocery_by_name, validate_grocery_name, normalize_grocery_name + +router = APIRouter() + +@router.post("/create-grocery", response_model=GrocerySchema, status_code=status.HTTP_201_CREATED) +async def create_grocery_item( + grocery: GroceryCreate, + db: Session = Depends(get_db) +): + if not validate_grocery_name(grocery.name): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid grocery name" + ) + + normalized_name = normalize_grocery_name(grocery.name) + + existing_grocery = get_grocery_by_name(db, normalized_name) + if existing_grocery: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Grocery with this name already exists" + ) + + grocery_data = GroceryCreate(name=normalized_name) + new_grocery = create_grocery(db, grocery_data) + + if not new_grocery: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Failed to create grocery" + ) + + return new_grocery \ No newline at end of file diff --git a/helpers/grocery_helpers.py b/helpers/grocery_helpers.py new file mode 100644 index 0000000..967e3f8 --- /dev/null +++ b/helpers/grocery_helpers.py @@ -0,0 +1,75 @@ +from typing import Optional +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError + +from models.grocery import Grocery +from schemas.grocery import GroceryCreate + +def create_grocery(db: Session, grocery_data: GroceryCreate) -> Optional[Grocery]: + """ + Creates a new grocery item in the database. + + Args: + db (Session): The database session + grocery_data (GroceryCreate): The data for the grocery item to create + + Returns: + Optional[Grocery]: The newly created grocery object, or None if creation failed + """ + try: + db_grocery = Grocery(**grocery_data.dict()) + db.add(db_grocery) + db.commit() + db.refresh(db_grocery) + return db_grocery + except IntegrityError: + db.rollback() + return None + +def get_grocery_by_name(db: Session, name: str) -> Optional[Grocery]: + """ + Retrieves a grocery item by its name. + + Args: + db (Session): The database session + name (str): The name of the grocery item to retrieve + + Returns: + Optional[Grocery]: The grocery object if found, otherwise None + """ + return db.query(Grocery).filter(Grocery.name == name).first() + +def validate_grocery_name(name: str) -> bool: + """ + Validates the grocery name according to business rules. + + Args: + name (str): The name to validate + + Returns: + bool: True if the name is valid, False otherwise + """ + if not name or not isinstance(name, str): + return False + + # Remove leading/trailing whitespace and check length + name = name.strip() + if len(name) < 1: + return False + + # Additional validation rules could be added here + return True + +def normalize_grocery_name(name: str) -> str: + """ + Normalizes a grocery name for consistency in storage. + + Args: + name (str): The name to normalize + + Returns: + str: The normalized name + """ + # Remove extra whitespace and capitalize first letter + normalized = " ".join(name.split()) + return normalized.strip().capitalize() \ No newline at end of file diff --git a/models/grocery.py b/models/grocery.py new file mode 100644 index 0000000..780542d --- /dev/null +++ b/models/grocery.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, String, DateTime +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func +from core.database import Base +import uuid + +class Grocery(Base): + __tablename__ = "groceries" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + name = Column(String, nullable=False, index=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/grocery.py b/schemas/grocery.py new file mode 100644 index 0000000..f5dfc5b --- /dev/null +++ b/schemas/grocery.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from uuid import UUID + +class GroceryBase(BaseModel): + name: str = Field(..., min_length=1, description="Name of the grocery item") + +class GroceryCreate(GroceryBase): + pass + +class GroceryUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, description="Name of the grocery item") + +class GrocerySchema(GroceryBase): + id: UUID + created_at: datetime + updated_at: datetime + + class Config: + orm_mode = True + schema_extra = { + "example": { + "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "name": "Milk", + "created_at": "2023-01-01T12:00:00", + "updated_at": "2023-01-01T12:00:00" + } + } \ No newline at end of file