Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions fastapi_ecom/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from fastapi_ecom.config import config
from fastapi_ecom.router import business, customer, order, product
from fastapi_ecom.utils.logging_setup import general

# Metadata for API tags
tags_metadata = [
Expand Down Expand Up @@ -36,6 +37,7 @@ def root() -> dict[str, str]:

:return: Metadata about the API, including title, description, and version.
"""
general("Root endpoint accessed")
return{
"title": "FastAPI ECOM",
"description": "E-Commerce API for businesses and end users using FastAPI.",
Expand All @@ -58,6 +60,7 @@ def start_service():

:raises RuntimeError: If configuration parameters are missing or invalid.
"""
general("FastAPI server started")
uvicorn.run(
"fastapi_ecom.app:app",
host=config.servhost,
Expand Down
34 changes: 34 additions & 0 deletions fastapi_ecom/config/config.py.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from logging import getLogger
from logging.config import dictConfig

# The database name
database = "test_db"

Expand Down Expand Up @@ -33,3 +36,34 @@ GOOGLE_CLIENT_ID = "example.apps.googleusercontent.com"

# Google Client secret
GOOGLE_CLIENT_SECRET = "example"

# The default configuration for service logging
log_config = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(message)s %(asctime)s",
"datefmt": "[%Y-%m-%d %I:%M:%S %z]",
},
},
"handlers": {
"console": {
"level": "INFO",
"formatter": "standard",
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
},
},
"loggers": {
"fastapi_ecom": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
},
}

dictConfig(log_config)

logger = getLogger(__name__)
6 changes: 6 additions & 0 deletions fastapi_ecom/database/db_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
migrpath,
models,
)
from fastapi_ecom.utils.logging_setup import general, success


def make_database() -> None:
Expand All @@ -24,16 +25,21 @@ def make_database() -> None:
- Marks the database as being at the latest migration version.
"""
# Use the synchronous engine to create the database schema.
general("Creating database schema with synchronous engine")
sync_engine = get_engine(engine="sync")
baseobjc.metadata.create_all(bind=sync_engine)
success("Database schema created successfully")

# Set up Alembic configuration for migration management.
general("Setting up Alembic configuration")
alembic_config = config.Config(alempath)
alembic_config.set_main_option("script_location", migrpath)
alembic_config.set_main_option("sqlalchemy.url", get_database_url().render_as_string(hide_password=False))

# Mark the database at the latest migration head.
general("Marking database at latest migration head")
command.stamp(alembic_config, "head")
success("Database marked at migration head successfully")

async def get_db() -> AsyncGenerator[AsyncSession, None]:
"""
Expand Down
8 changes: 4 additions & 4 deletions fastapi_ecom/database/pydantic_schemas/customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ class CustomerView(CustomerBase):
"""
email: EmailStr
name: str
addr_line_1: str
addr_line_2: str
city: str
state: str
addr_line_1: Optional[str]
addr_line_2: Optional[str]
city: Optional[str]
state: Optional[str]


class CustomerCreate(CustomerView):
Expand Down
11 changes: 11 additions & 0 deletions fastapi_ecom/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from fastapi_ecom.app import start_service
from fastapi_ecom.database.db_setup import make_database
from fastapi_ecom.migrations.main import alembic_migration
from fastapi_ecom.utils.logging_setup import general, success


@click.group(name="fastapi_ecom", help="E-Commerce API for businesses and end users using FastAPI.")
Expand All @@ -26,7 +27,9 @@ def setup() -> None:

:return: None
"""
general("Setting up database schema")
make_database()
success("Database schema setup completed")

@main.command(name="start", help="Start the FastAPI eComm application")
def start() -> None:
Expand All @@ -37,6 +40,7 @@ def start() -> None:

:return: None
"""
general("Starting FastAPI eComm application")
start_service()

@main.command(name="create-migration", help="Create a new migration script")
Expand All @@ -51,7 +55,9 @@ def create_migration(comment: str, autogenerate: bool) -> None:

:return: None
"""
general(f"Creating migration with comment: {comment}")
alembic_migration.create(comment, autogenerate)
success(f"Migration created successfully: {comment}")

@main.command(name="db-version", help="Show the current database version")
def db_version() -> None:
Expand All @@ -62,6 +68,7 @@ def db_version() -> None:

:return: None
"""
general("Checking database version")
alembic_migration.db_version()

@main.command(name="upgrade-db", help="Upgrade the database to a specific version")
Expand All @@ -75,7 +82,9 @@ def upgrade_db(version: str) -> None:

:return: None
"""
general(f"Upgrading database to version: {version}")
alembic_migration.upgrade(version)
success(f"Database upgraded to version: {version}")

@main.command(name="downgrade-db", help="Downgrade the database to a specific version")
@click.argument("version", type=str)
Expand All @@ -88,4 +97,6 @@ def downgrade_db(version: str) -> None:

:return: None
"""
general(f"Downgrading database to version: {version}")
alembic_migration.downgrade(version)
success(f"Database downgraded to version: {version}")
17 changes: 17 additions & 0 deletions fastapi_ecom/router/business.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
BusinessView,
)
from fastapi_ecom.utils.auth import verify_business_cred
from fastapi_ecom.utils.logging_setup import failure, general, success, warning

router = APIRouter(prefix="/business")

Expand Down Expand Up @@ -50,19 +51,23 @@ async def create_business(business: BusinessCreate, db: AsyncSession = Depends(g
state=business.state.strip(),
uuid=uuid4().hex[0:8] # Assign UUID manually; One UUID per transaction
)
general(f"Adding account for business in database: {business.email}")
db.add(db_business)
try:
await db.flush()
except IntegrityError as expt:
failure(f"Business account creation failed - Uniqueness constraint violation for email: {business.email}")
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Uniqueness constraint failed - Please try again"
) from expt
except Exception as expt:
failure(f"Business account creation failed with unexpected error for email: {business.email}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected database error occurred."
) from expt
success(f"Business account created successfully with email: {business.email}")
return {
"action": "post",
"business": BusinessView.model_validate(db_business).model_dump()
Expand All @@ -77,6 +82,7 @@ async def get_business_me(business_auth = Depends(verify_business_cred)) -> dict

:return: Dictionary containing the action type and the authenticated business's email.
"""
general(f"Business authentication successful for: {business_auth.email}")
return {
"action": "get",
"email": business_auth.email
Expand All @@ -101,14 +107,17 @@ async def get_businesses(
:raises HTTPException:
- If no business exist in the database, it raises 404 Not Found.
"""
general(f"Searching businesses with skip={skip}, limit={limit}")
query = select(Business).options(selectinload("*")).offset(skip).limit(limit)
result = await db.execute(query)
businesses = result.scalars().all()
if not businesses:
warning("No businesses found in database")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No business present in database"
)
success(f"Found {len(businesses)} businesses")
return {
"action": "get",
"businesses": [BusinessView.model_validate(business).model_dump() for business in businesses]
Expand All @@ -129,6 +138,7 @@ async def delete_business(db: AsyncSession = Depends(get_db), business_auth = De
- If a uniqueness constraint fails, it returns a 409 Conflict status.
- If there are other database errors, it returns a 500 Internal Server Error.
"""
general(f"Deleting business account: {business_auth.email}")
query = select(Business).where(Business.uuid == business_auth.uuid).options(selectinload("*"))
result = await db.execute(query)
business_to_delete = result.scalar_one_or_none()
Expand All @@ -142,10 +152,12 @@ async def delete_business(db: AsyncSession = Depends(get_db), business_auth = De
interactions due to which mocking one part wont produce the desired result. Thus,
we will keep it uncovered until a alternative can be made for testing this exception block.
"""
failure(f"Business account deletion failed for: {business_auth.email}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected database error occurred."
) from expt
success(f"Business account deleted successfully: {business_auth.email}")
return {
"action": "delete",
"business": BusinessView.model_validate(business_to_delete).model_dump()
Expand All @@ -171,6 +183,8 @@ async def update_business(
- If a uniqueness constraint fails, it returns a 409 Conflict status.
- If there are other database errors, it returns a 500 Internal Server Error.
"""
business_email = business_auth.email # Capture email before potential database errors
general(f"Updating details of business: {business_email}")
query = select(Business).where(Business.uuid == business_auth.uuid).options(selectinload("*"))
result = await db.execute(query)
business_to_update = result.scalar_one_or_none()
Expand All @@ -190,6 +204,7 @@ async def update_business(
try:
await db.flush()
except IntegrityError as expt:
failure(f"Business update failed - Uniqueness constraint violation for: {business_email}")
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Uniqueness constraint failed - Please try again"
Expand All @@ -200,10 +215,12 @@ async def update_business(
interactions due to which mocking one part wont produce the desired result. Thus,
we will keep it uncovered until a alternative can be made for testing this exception block.
"""
failure(f"Business update failed with unexpected error for: {business_email}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected database error occurred."
) from expt
success(f"Business details updated successfully: {business_email}")
return {
"action": "put",
"business": BusinessView.model_validate(business_to_update).model_dump()
Expand Down
22 changes: 20 additions & 2 deletions fastapi_ecom/router/customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
CustomerView,
)
from fastapi_ecom.utils.auth import verify_cust_cred
from fastapi_ecom.utils.logging_setup import failure, general, success, warning

router = APIRouter(prefix="/customer")

Expand Down Expand Up @@ -50,19 +51,23 @@ async def create_customer(customer: CustomerCreate, db: AsyncSession = Depends(g
state = customer.state.strip(),
uuid = uuid4().hex[0:8] # Assign UUID manually; One UUID per transaction
)
general(f"Adding account for customer in database: {customer.email}")
db.add(db_customer)
try:
await db.flush()
except IntegrityError as expt:
raise HTTPException(
failure(f"Customer account creation failed - Uniqueness constraint violation for email: {customer.email}")
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Uniqueness constraint failed - Please try again"
) from expt
except Exception as expt:
failure(f"Customer account creation failed with unexpected error for email: {customer.email}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected database error occurred."
) from expt
success(f"Customer account created successfully with email: {customer.email}")
return {
"action": "post",
"customer": CustomerView.model_validate(db_customer).model_dump()
Expand All @@ -77,6 +82,7 @@ async def get_customer_me(customer_auth = Depends(verify_cust_cred)) -> dict[str

:return: Dictionary containing the action type and the authenticated customer's email.
"""
general(f"Customer authentication successful for: {customer_auth.email}")
return {
"action": "get",
"email": customer_auth.email
Expand All @@ -101,14 +107,17 @@ async def get_customers(
:raises HTTPException:
- If no customer exist in the database, it raises 404 Not Found.
"""
general(f"Searching customers with skip={skip}, limit={limit}")
query = select(Customer).options(selectinload("*")).offset(skip).limit(limit)
result = await db.execute(query)
customers = result.scalars().all()
if not customers:
warning("No customers found in database")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No customer present in database"
)
success(f"Found {len(customers)} customers")
return {
"action": "get",
"customers": [CustomerView.model_validate(customer).model_dump() for customer in customers]
Expand All @@ -122,12 +131,14 @@ async def delete_customer(db: AsyncSession = Depends(get_db), customer_auth = De
:param db: Active asynchronous database session dependency.
:param customer_auth: Authenticated customer object.

:return: Dictionary containing the action type and the deleted customer's data.
:return: Dictionary containing the action type and the deleted customer's data, validated and
serialized using the `CustomerView` schema.

:raises HTTPException:
- If a uniqueness constraint fails, it returns a 409 Conflict status.
- If there are other database errors, it returns a 500 Internal Server Error.
"""
general(f"Deleting customer account: {customer_auth.email}")
query = select(Customer).where(Customer.uuid == customer_auth.uuid).options(selectinload("*"))
result = await db.execute(query)
customer_to_delete = result.scalar_one_or_none()
Expand All @@ -141,10 +152,12 @@ async def delete_customer(db: AsyncSession = Depends(get_db), customer_auth = De
interactions due to which mocking one part wont produce the desired result. Thus,
we will keep it uncovered until a alternative can be made for testing this exception block.
"""
failure(f"Customer account deletion failed for: {customer_auth.email}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected database error occurred."
) from expt
success(f"Customer account deleted successfully: {customer_auth.email}")
return {
"action": "delete",
"customer": CustomerView.model_validate(customer_to_delete).model_dump()
Expand All @@ -165,6 +178,8 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g
- If a uniqueness constraint fails, it returns a 409 Conflict status.
- If there are other database errors, it returns a 500 Internal Server Error.
"""
customer_email = customer_auth.email # Capture email before potential database errors
general(f"Updating details of customer: {customer_email}")
query = select(Customer).where(Customer.uuid == customer_auth.uuid).options(selectinload("*"))
result = await db.execute(query)
customer_to_update = result.scalar_one_or_none()
Expand All @@ -184,6 +199,7 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g
try:
await db.flush()
except IntegrityError as expt:
failure(f"Customer update failed - Uniqueness constraint violation for: {customer_email}")
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Uniqueness constraint failed - Please try again"
Expand All @@ -194,10 +210,12 @@ async def update_customer(customer: CustomerUpdate, db: AsyncSession = Depends(g
interactions due to which mocking one part wont produce the desired result. Thus,
we will keep it uncovered until a alternative can be made for testing this exception block.
"""
failure(f"Customer update failed with unexpected error for: {customer_email}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected database error occurred."
) from expt
success(f"Customer details updated successfully: {customer_email}")
return {
"action": "put",
"customer": CustomerView.model_validate(customer_to_update).model_dump()
Expand Down
Loading