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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@
This is a simple app is aimed to solidify and escalate web api knowledge

Build and run the app:
```bash
docker build -t webapi:dev .; docker run --rm -p 8000:8000 webapi:dev
```

## Database diagram documentation

The ORM model and relational mapping documentation for dbdiagram.io is available at [`plans/dbdiagram.md`](plans/dbdiagram.md).
49 changes: 49 additions & 0 deletions plans/dbdiagram.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Database Diagram Documentation

This document describes the ORM data model implemented in [`User`](../webapi/models/user.py:8) and [`Prompts`](../webapi/models/prompts.py:6), and provides a DBML definition ready for dbdiagram.io.

## Source of truth

- [`User`](../webapi/models/user.py:8)
- [`Prompts`](../webapi/models/prompts.py:6)
- DB engine initialization in [`engine = create_engine(config.DB_URL, echo=True)`](../webapi/db/db_connection.py:6)
- Schema bootstrap in [`SQLModel.metadata.create_all(engine)`](../webapi/db/db_connection.py:10)

## DBML for dbdiagram.io
Visualize below code at: https://dbdiagram.io/d

```dbml
Table user {
id int [pk, increment]
username varchar(50) [not null]
name varchar(100) [not null]
last_name varchar(100) [not null]
phone bigint [unique]
email varchar(100) [not null]
hashed_password varchar(255) [not null]

Note: 'Mapped from SQLModel User entity'
}

Table prompts {
id int [pk, increment]
user_id int [not null, ref: > user.id]
model_name varchar(30) [not null]
prompt_text varchar(150) [not null]
category varchar(30) [not null]
rate varchar(30) [not null]

Note: 'Mapped from SQLModel Prompts entity'
}
```

## Relationship cardinality

- One-to-many from [`User.prompts`](../webapi/models/user.py:18) to [`Prompts.user`](../webapi/models/prompts.py:14)
- Foreign key defined at [`user_id: int = Field(foreign_key='user.id', nullable=False)`](../webapi/models/prompts.py:8)

## Notes and naming consistency

- ORM class is pluralized as [`class Prompts(SQLModel, table=True):`](../webapi/models/prompts.py:6), while the related attribute in [`User`](../webapi/models/user.py:18) is also plural.
- File naming appears to include both [`webapi/models/prompts.py`](../webapi/models/prompts.py) and a visible editor tab for [`webapi/models/prompt.py`](../webapi/models/prompt.py). Consolidating to one naming convention may reduce confusion.
- If desired in a future refactor, entity naming could be normalized to singular class names with explicit `__tablename__` values to keep SQL table names stable.
92 changes: 0 additions & 92 deletions webapi/api/endpoints/v1/cards.py

This file was deleted.

100 changes: 100 additions & 0 deletions webapi/api/endpoints/v1/prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from fastapi import APIRouter, HTTPException, Depends, Header
from sqlmodel import Session, select
from typing import Optional
from models.user import User
from models.prompts import Prompts
from db.db_connection import get_session
from auth.auth_service import validar_jwt
from infrastructure.email.smtp_service import send_email

router = APIRouter()

@router.post("", response_model=Prompts)
async def create_prompt(
prompt: Prompts,
session: Session = Depends(get_session),
authorization: Optional[str] = Header(None),
send_email_header: Optional[str] = Header("false", alias="send_email")
):
data = validar_jwt(authorization)
if not data:
raise HTTPException(status_code=401, detail="Unauthorized token")
# Ensure the user exists before creating a prompt
user = session.get(User, prompt.user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")

session.add(prompt)
session.commit()
session.refresh(prompt)

# Email notification is best-effort and should not block prompt persistence.
if str(send_email_header).lower() == "true":
try:
await send_email(user.email, user.username)
except Exception:
pass

return prompt

@router.get("", response_model=list[Prompts])
def read_prompts(skip: int = 0, limit: int = 10,
session: Session = Depends(get_session),
authorization: Optional[str] = Header(None)):
data = validar_jwt(authorization)
if not data:
raise HTTPException(status_code=401, detail="Unauthorized token")
statement = select(Prompts).offset(skip).limit(limit)
prompts = session.exec(statement).all()
if not prompts:
raise HTTPException(status_code=404, detail="No prompts found")
return prompts


@router.get("/{prompt_id}", response_model=Prompts)
def get_prompt(prompt_id: int, session: Session = Depends(get_session),
authorization: Optional[str] = Header(None)):
data = validar_jwt(authorization)
if not data:
raise HTTPException(status_code=401, detail="Unauthorized token")
prompt = session.get(Prompts, prompt_id)
if not prompt:
raise HTTPException(status_code=404, detail="Prompt not found")
return prompt

@router.put("/{prompt_id}", response_model=Prompts)
def update_prompt(prompt_id: int, prompt: Prompts,
session: Session = Depends(get_session),
authorization: Optional[str] = Header(None)):
data = validar_jwt(authorization)
if not data:
raise HTTPException(status_code=401, detail="Unauthorized token")
existing_prompt = session.get(Prompts, prompt_id)
if not existing_prompt:
raise HTTPException(status_code=404, detail="Prompt not found")
existing_prompt.model_name = prompt.model_name
existing_prompt.prompt_text = prompt.prompt_text
existing_prompt.category = prompt.category
existing_prompt.rate = prompt.rate
existing_prompt.user_id = prompt.user_id # Ensure the user exists
user = session.get(User, prompt.user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found for this prompt")
session.add(existing_prompt)
session.commit()
session.refresh(existing_prompt)
return existing_prompt


@router.delete("/{prompt_id}")
def delete_prompt(prompt_id: int, session: Session = Depends(get_session),
authorization: Optional[str] = Header(None)):
data = validar_jwt(authorization)
if not data:
raise HTTPException(status_code=401, detail="Unauthorized token")
prompt = session.get(Prompts, prompt_id)
if not prompt:
raise HTTPException(status_code=404, detail="Prompt not found to delete")
session.delete(prompt)
session.commit()
return prompt
14 changes: 8 additions & 6 deletions webapi/api/endpoints/v1/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from sqlmodel import Session, select
from models.user import User
from typing import Optional
from schemas.user_schema import UserReadWithCards
from schemas.user_schema import UserReadWithPrompts
from db.db_connection import get_session
from auth.auth_service import validar_jwt
from passlib.hash import sha256_crypt
Expand Down Expand Up @@ -41,8 +41,8 @@ def get_user(user_id: int, session: Session = Depends(get_session),
return user


@router.get("/cards/{user_id}", response_model=UserReadWithCards)
def get_user_with_cards(user_id: int, session: Session = Depends(get_session),
@router.get("/prompts/{user_id}", response_model=UserReadWithPrompts)
def get_user_with_prompts(user_id: int, session: Session = Depends(get_session),
authorization: Optional[str] = Header(None)):
data = validar_jwt(authorization)
if not data:
Expand All @@ -52,8 +52,8 @@ def get_user_with_cards(user_id: int, session: Session = Depends(get_session),
user = result.one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Access cards within session to trigger lazy load
_ = user.cards
# Access prompts within session to trigger lazy load
_ = user.prompts
return user

@router.put("/{user_id}", response_model=User)
Expand Down Expand Up @@ -95,7 +95,9 @@ def delete_user(user_id: int, session: Session = Depends(get_session),
raise HTTPException(status_code=404, detail="User not found to delete")
session.delete(user)
session.commit()
except HTTPException:
raise
except Exception as e:
session.rollback()
raise HTTPException(status_code=500, detail=f"Error deleting user: {str(e)}")
return user
return user
6 changes: 3 additions & 3 deletions webapi/api/routers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import APIRouter
from .endpoints.v1 import auths, users, cards
from .endpoints.v1 import auths, users, prompts

api_router = APIRouter()
api_router.include_router(auths.router, prefix="/auth", tags=["Auth"])
api_router.include_router(cards.router, prefix="/cards", tags=["Cards"])
api_router.include_router(users.router, prefix="/users", tags=["Users"])
api_router.include_router(prompts.router, prefix="/prompts", tags=["Prompts"])
api_router.include_router(users.router, prefix="/users", tags=["Users"])
18 changes: 9 additions & 9 deletions webapi/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi import FastAPI
import uvicorn
from api.routers import api_router
# from api.endpoints.v1 import auths, users, cards
# from api.endpoints.v1 import auths, users, prompts

tags_metadata = [
{
Expand All @@ -13,15 +13,15 @@
"description": "Operations with users (CRUD).",
},
{
"name": "Cards",
"description": "Operations with cards.",
"name": "Prompts",
"description": "Operations with prompts.",
},
]

myapp = FastAPI(
title="Bank API",
description="API for managing users, cards, and authentication in a banking system.",
version="1.0.0",
title="Portfolio API",
description="API for managing users, prompts, and authentication in a web environment.",
version="0.1.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_tags=tags_metadata
Expand All @@ -46,7 +46,7 @@

@myapp.get("/")
def root():
return {"Bank App": "This is a simple app using FastAPI and mariadb."}
return {"Portfolio App": "This is a simple app using FastAPI and mariadb."}


# Include all API routes
Expand All @@ -55,9 +55,9 @@ def root():
# Include routers
"""
app.include_router(users.router, prefix="/users", tags=["Users"])
app.include_router(cards.router, prefix="/cards", tags=["Cards"])
app.include_router(prompts.router, prefix="/prompts", tags=["Prompts"])
app.include_router(auths.router, prefix="/auth", tags=["Auth"])
"""

if __name__ == "__main__":
uvicorn.run(myapp, host="127.0.0.1", port=8000)
uvicorn.run(myapp, host="127.0.0.1", port=8000)
4 changes: 2 additions & 2 deletions webapi/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# models/__init__.py
from .user import User
from .card import Cards
from .prompts import Prompts

__all__ = ["User", "Cards"]
__all__ = ["User", "Prompts"]
14 changes: 7 additions & 7 deletions webapi/models/card.py → webapi/models/prompts.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from typing import Optional
from sqlmodel import SQLModel, Field, Relationship
from sqlalchemy import CHAR
import models


class Cards(SQLModel, table=True):
class Prompts(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id", nullable=False)
card_type: str = Field(max_length=10, nullable=False)
card_number: str = Field(sa_type=CHAR(16), nullable=False, unique=True)
expiration_date: str = Field(max_length=7, nullable=False) # MM/YYYY format
model_name: str = Field(max_length=30, nullable=False)
prompt_text: str = Field(max_length=150, nullable=False)
category: str = Field(max_length=30, nullable=False)
rate: str = Field(max_length=30, nullable=False)

user: Optional["User"] = Relationship(
sa_relationship_kwargs={"lazy": "selectin"},
back_populates="cards"
)
back_populates="prompts"
)
Loading