diff --git a/alembic/versions/20250429_190226_aa23ae9e_update_expense.py b/alembic/versions/20250429_190226_aa23ae9e_update_expense.py new file mode 100644 index 0000000..f3d675a --- /dev/null +++ b/alembic/versions/20250429_190226_aa23ae9e_update_expense.py @@ -0,0 +1,33 @@ +"""create table for expenses + +Revision ID: 6c1f8e2b7a4d +Revises: 0001 +Create Date: 2023-05-21 12:22:12.936460 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.sql import func + +# revision identifiers, used by Alembic. +revision = '6c1f8e2b7a4d' +down_revision = '0001' +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + 'expenses', + sa.Column('id', sa.String(36), primary_key=True), + sa.Column('title', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('category', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=func.now()), + sa.Column('updated_at', sa.DateTime(), server_default=func.now(), onupdate=func.now()) + ) + + +def downgrade(): + op.drop_table('expenses') \ No newline at end of file diff --git a/endpoints/expense.get.py b/endpoints/expense.get.py index e69de29..b9e371d 100644 --- a/endpoints/expense.get.py +++ b/endpoints/expense.get.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from typing import List +from core.database import get_db +from schemas.expense import ExpenseSchema +from helpers.expense_helpers import get_all_expenses + +router = APIRouter() + +@router.get("/expense", status_code=200, response_model=List[ExpenseSchema]) +async def get_expenses(db: Session = Depends(get_db)): + expenses = get_all_expenses(db) + return expenses \ No newline at end of file diff --git a/helpers/expense_helpers.py b/helpers/expense_helpers.py new file mode 100644 index 0000000..f2c22ee --- /dev/null +++ b/helpers/expense_helpers.py @@ -0,0 +1,171 @@ +from typing import List, Optional, Dict, Any +from uuid import UUID +from sqlalchemy.orm import Session +from models.expense import Expense +from schemas.expense import ExpenseCreate, ExpenseUpdate, ExpenseSchema + +def get_all_expenses(db: Session) -> List[ExpenseSchema]: + """ + Retrieves all expenses from the database. + + Args: + db (Session): The database session. + + Returns: + List[ExpenseSchema]: A list of all expense objects. + """ + expenses = db.query(Expense).all() + return [ExpenseSchema.from_orm(expense) for expense in expenses] + +def get_expense_by_id(db: Session, expense_id: UUID) -> Optional[ExpenseSchema]: + """ + Retrieves a single expense by its ID. + + Args: + db (Session): The database session. + expense_id (UUID): The ID of the expense to retrieve. + + Returns: + Optional[ExpenseSchema]: The expense object if found, otherwise None. + """ + expense = db.query(Expense).filter(Expense.id == expense_id).first() + return ExpenseSchema.from_orm(expense) if expense else None + +def create_expense(db: Session, expense_data: ExpenseCreate) -> ExpenseSchema: + """ + Creates a new expense in the database. + + Args: + db (Session): The database session. + expense_data (ExpenseCreate): The data for the expense to create. + + Returns: + ExpenseSchema: The newly created expense object. + """ + db_expense = Expense(**expense_data.dict()) + db.add(db_expense) + db.commit() + db.refresh(db_expense) + return ExpenseSchema.from_orm(db_expense) + +def update_expense(db: Session, expense_id: UUID, expense_data: ExpenseUpdate) -> Optional[ExpenseSchema]: + """ + Updates an existing expense in the database. + + Args: + db (Session): The database session. + expense_id (UUID): The ID of the expense to update. + expense_data (ExpenseUpdate): The updated data for the expense. + + Returns: + Optional[ExpenseSchema]: The updated expense object if found, otherwise None. + """ + expense = db.query(Expense).filter(Expense.id == expense_id).first() + if not expense: + return None + + expense_data = expense_data.dict(exclude_unset=True) + for key, value in expense_data.items(): + setattr(expense, key, value) + + db.add(expense) + db.commit() + db.refresh(expense) + return ExpenseSchema.from_orm(expense) + +def delete_expense(db: Session, expense_id: UUID) -> bool: + """ + Deletes an expense from the database. + + Args: + db (Session): The database session. + expense_id (UUID): The ID of the expense to delete. + + Returns: + bool: True if the expense was successfully deleted, False otherwise. + """ + expense = db.query(Expense).filter(Expense.id == expense_id).first() + if not expense: + return False + + db.delete(expense) + db.commit() + return True + +def validate_expense_data(expense_data: Dict[str, Any]) -> bool: + """ + Validates the expense data dictionary. + + Args: + expense_data (Dict[str, Any]): The expense data to validate. + + Returns: + bool: True if the data is valid, False otherwise. + """ + if not expense_data: + return False + + if "title" not in expense_data or not isinstance(expense_data["title"], str) or len(expense_data["title"]) < 3: + return False + + if "amount" not in expense_data or not isinstance(expense_data["amount"], (int, float)) or expense_data["amount"] <= 0: + return False + + if "description" in expense_data and not isinstance(expense_data["description"], str): + return False + + if "category" in expense_data and not isinstance(expense_data["category"], str): + return False + + return True + +def validate_expense_title(title: str) -> bool: + """ + Validates the expense title string. + + Args: + title (str): The expense title to validate. + + Returns: + bool: True if the title is valid, False otherwise. + """ + if not isinstance(title, str) or len(title) < 3: + return False + + # Add additional validation rules for the title if needed + return True + +def validate_expense_amount(amount: float) -> bool: + """ + Validates the expense amount. + + Args: + amount (float): The expense amount to validate. + + Returns: + bool: True if the amount is valid, False otherwise. + """ + if not isinstance(amount, (int, float)) or amount <= 0: + return False + + # Add additional validation rules for the amount if needed + return True + +def validate_expense_category(category: Optional[str]) -> bool: + """ + Validates the expense category string. + + Args: + category (Optional[str]): The expense category to validate. + + Returns: + bool: True if the category is valid, False otherwise. + """ + if category is None: + return True + + if not isinstance(category, str): + return False + + # Add additional validation rules for the category if needed + return True \ No newline at end of file diff --git a/models/expense.py b/models/expense.py new file mode 100644 index 0000000..4def1f6 --- /dev/null +++ b/models/expense.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, String, DateTime, Float +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func +from core.database import Base +import uuid + +class Expense(Base): + __tablename__ = "expenses" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + title = Column(String, nullable=False) + description = Column(String, nullable=True) + amount = Column(Float, nullable=False) + category = Column(String, 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/requirements.txt b/requirements.txt index 596e6f3..db12c92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,6 @@ sqlalchemy>=1.4.0 python-dotenv>=0.19.0 bcrypt>=3.2.0 alembic>=1.13.1 +jose +passlib +pydantic diff --git a/schemas/expense.py b/schemas/expense.py new file mode 100644 index 0000000..15b1c11 --- /dev/null +++ b/schemas/expense.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +import uuid + +class ExpenseBase(BaseModel): + title: str = Field(..., description="Expense title") + description: Optional[str] = Field(None, description="Expense description") + amount: float = Field(..., description="Expense amount") + category: Optional[str] = Field(None, description="Expense category") + +class ExpenseCreate(ExpenseBase): + pass + +class ExpenseUpdate(ExpenseBase): + title: Optional[str] = Field(None, description="Expense title") + description: Optional[str] = Field(None, description="Expense description") + amount: Optional[float] = Field(None, description="Expense amount") + category: Optional[str] = Field(None, description="Expense category") + +class ExpenseSchema(ExpenseBase): + id: uuid.UUID + created_at: datetime + updated_at: datetime + + class Config: + orm_mode = True \ No newline at end of file