Skip to content

Commit 2fc8257

Browse files
committed
wrote crud routes for transaction
1 parent a2b3a1d commit 2fc8257

File tree

13 files changed

+260
-88
lines changed

13 files changed

+260
-88
lines changed

app/api/endpoints/expenses.py

Lines changed: 0 additions & 32 deletions
This file was deleted.

app/api/endpoints/transaction.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from typing import List
2+
import uuid
3+
4+
from fastapi import APIRouter, Depends, HTTPException, status, Query
5+
from sqlalchemy.orm import Session
6+
from sqlalchemy.exc import IntegrityError
7+
8+
from app.core.database import get_db
9+
from app.models.transaction import Transaction
10+
from app.schemas.transaction import TransactionCreate, TransactionUpdate, TransactionResponse
11+
12+
router = APIRouter()
13+
14+
15+
def retrieve_transaction(transaction_id: uuid.UUID, db: Session) -> Transaction:
16+
"""Gets a single transaction or raise if non existent.
17+
18+
Raises:
19+
HTTPException (404 Not Found) if the transaction does not exist.
20+
"""
21+
transaction = db.query(Transaction).filter(Transaction.id == transaction_id).one_or_none()
22+
23+
if transaction is None:
24+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Transaction with ID {transaction_id} not found")
25+
26+
return transaction
27+
28+
29+
@router.post(
30+
"/",
31+
response_model=TransactionResponse,
32+
status_code=status.HTTP_201_CREATED,
33+
)
34+
def create_transaction(
35+
data: TransactionCreate,
36+
db: Session = Depends(get_db)
37+
) -> TransactionResponse:
38+
39+
transaction = Transaction(
40+
amount=data.amount,
41+
method=data.method,
42+
date=data.date,
43+
description=data.description,
44+
)
45+
db.add(transaction)
46+
try:
47+
db.commit()
48+
db.refresh(transaction)
49+
except IntegrityError as e:
50+
db.rollback()
51+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Database constraint violation") from e
52+
53+
return transaction.to_response()
54+
55+
56+
@router.get(
57+
"/{transaction_id}",
58+
response_model=TransactionResponse,
59+
status_code=status.HTTP_200_OK,
60+
)
61+
def get_single_transaction(
62+
transaction_id: uuid.UUID,
63+
db: Session = Depends(get_db)
64+
) -> TransactionResponse:
65+
66+
transaction = retrieve_transaction(transaction_id, db)
67+
return transaction.to_response()
68+
69+
70+
@router.get(
71+
"/",
72+
response_model=List[TransactionResponse],
73+
status_code=status.HTTP_200_OK,
74+
)
75+
def get_all_transactions(
76+
db: Session = Depends(get_db),
77+
offset: int = Query(0, ge=0, description="Offset must be 0 or greater (defaults to 0)"),
78+
limit: int = Query(10, ge=1, le=100, description="Limit must be between 1 and 100 (defaults to 10)"),
79+
) -> List[TransactionResponse]:
80+
81+
transactions = db.query(Transaction).offset(offset).limit(limit).all()
82+
return [t.to_response() for t in transactions]
83+
84+
85+
@router.patch(
86+
"/{transaction_id}",
87+
response_model=TransactionResponse,
88+
status_code=status.HTTP_200_OK,
89+
)
90+
def update_transaction(
91+
transaction_id: uuid.UUID,
92+
data: TransactionUpdate,
93+
db: Session = Depends(get_db),
94+
) -> TransactionResponse:
95+
96+
transaction = retrieve_transaction(transaction_id, db)
97+
98+
update_data = data.model_dump(exclude_unset=True)
99+
100+
field_mapping = {
101+
"amount": "amount",
102+
"description": "description",
103+
"method": "method",
104+
"date": "date"
105+
}
106+
107+
for schema_field, db_field in field_mapping.items():
108+
if schema_field in update_data:
109+
setattr(transaction, db_field, update_data[schema_field])
110+
111+
db.commit()
112+
db.refresh(transaction)
113+
114+
return transaction.to_response()
115+
116+
117+
@router.delete(
118+
"/{transaction_id}",
119+
status_code=status.HTTP_204_NO_CONTENT
120+
)
121+
def delete_transaction(
122+
transaction_id: uuid.UUID,
123+
db: Session = Depends(get_db),
124+
) -> None:
125+
126+
transaction = retrieve_transaction(transaction_id, db)
127+
128+
try:
129+
db.delete(transaction)
130+
db.commit()
131+
except IntegrityError as e:
132+
db.rollback()
133+
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Cannot delete transaction due to related records") from e

app/api/endpoints/users.py

Whitespace-only changes.

app/api/exception_handlers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import logging
2+
3+
from fastapi import HTTPException, status
4+
from sqlalchemy.exc import IntegrityError
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
def sqlalchemy_exception_handler(request, exc) -> None:
10+
"""Global handler for all SQLAlchemy errors."""
11+
# pylint: disable = unused-argument
12+
13+
# log the error and do not include it into user messages
14+
# to avoid exposing sensitive database information
15+
logger.error("Database error: %s", str(exc))
16+
17+
if isinstance(exc, IntegrityError):
18+
raise HTTPException(
19+
status_code=status.HTTP_400_BAD_REQUEST,
20+
detail="Database integrity error. Possible duplicate entry or constraint violation."
21+
)
22+
23+
raise HTTPException(
24+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
25+
detail="A database error occurred. Please try again later."
26+
)

app/core/config.py

Whitespace-only changes.

app/core/database.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,10 @@
1919
engine = create_engine(DATABASE_URL)
2020
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
2121
Base = declarative_base()
22+
23+
def get_db():
24+
db = SessionLocal()
25+
try:
26+
yield db
27+
finally:
28+
db.close()

app/main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
from fastapi import FastAPI
22
from fastapi.responses import RedirectResponse
33

4+
from sqlalchemy.exc import SQLAlchemyError
5+
46
from app.core.database import engine, Base
5-
from app.api.endpoints import expenses
7+
from app.api.endpoints import transaction
8+
import app.api.exception_handlers as ExHandler
69

710
Base.metadata.create_all(bind=engine)
811

912
app = FastAPI(title="Expense Tracker API")
13+
app.add_exception_handler(SQLAlchemyError, ExHandler.sqlalchemy_exception_handler)
1014

11-
app.include_router(expenses.router, prefix="/expenses", tags=["Expenses"])
15+
app.include_router(transaction.router, prefix="/transaction", tags=["Transactions"])
1216

1317
@app.get("/", include_in_schema=False)
1418
async def redirect_to_swagger():

app/models/expense.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

app/models/transaction.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Expense database model. """
2+
3+
from datetime import datetime
4+
import uuid
5+
from enum import Enum as PyEnum
6+
7+
from sqlalchemy import Column, String, Float, DateTime, Enum, UUID
8+
9+
from app.core.database import Base
10+
from app.schemas.transaction import TransactionResponse
11+
12+
13+
class TransactionMethod(str, PyEnum):
14+
BANK_TRANSFER = "bank_transfer"
15+
CREDIT_CARD = "credit_card"
16+
DEBIT_CARD = "debit_card"
17+
CASH = "cash"
18+
PAYPAL = "paypal"
19+
OTHER = "other"
20+
21+
22+
class Transaction(Base):
23+
__tablename__ = "transactions"
24+
25+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
26+
amount = Column(Float, nullable=False)
27+
method = Column(Enum(TransactionMethod), nullable=False)
28+
date = Column(DateTime, default=datetime.now, nullable=False)
29+
description = Column(String(255), nullable=True)
30+
31+
32+
def to_response(self) -> TransactionResponse:
33+
"""Convert a Transaction ORM object into a TransactionResponse Pydantic model."""
34+
return TransactionResponse(
35+
id=self.id,
36+
amount=self.amount,
37+
method=self.method,
38+
date=self.date,
39+
description=self.description,
40+
)

app/models/user.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)