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: 2 additions & 1 deletion app/api/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from fastapi import APIRouter

from app.api.routes import auth, user
from app.api.routes import auth, questions, user

api_router = APIRouter()

api_router.include_router(user.router, prefix="/users", tags=["users"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(questions.router, prefix="/questions", tags=["questions"])
16 changes: 16 additions & 0 deletions app/api/routes/questions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import APIRouter

from app.settings import settings
from app.utils.http_client import make_request

router = APIRouter()


GENERATOR_SERVICE_URL = settings.GENERATOR_SERVICE_URL


@router.get("/daily_questions")
def get_daily_questions():
"""Fetch daily questions from the external generator service."""
url = f"{GENERATOR_SERVICE_URL}/api/v1/questions/get_daily_question"
return make_request("GET", url)
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
52 changes: 48 additions & 4 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,60 @@
# from functools import lru_cache
from fastapi import FastAPI
import logging

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi.responses import JSONResponse
from starlette.middleware.sessions import SessionMiddleware

from app.api.main import api_router
from app.schemas.exceptions import ErrorResponse
from app.settings import settings
from app.utils.exceptions import AppException

logger = logging.getLogger(__name__)

app = FastAPI()

origins = [
"http://localhost:5173", # Vite local dev
"http://127.0.0.1:5173",
"http://yourdomain.com", # replace with final domain
]

app.add_middleware(
CORSMiddleware,
allow_origins=origins, # origins allowed
allow_credentials=True,
allow_methods=["*"], # allow all HTTP methods
allow_headers=["*"], # allow all headers
)


@app.exception_handler(AppException)
async def app_exception_handler(_request: Request, exc: AppException):
logger.error("AppException: %s | Context: %s", exc.detail, exc.context)
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(
detail=exc.detail,
code=exc.code,
status_code=exc.status_code,
context=exc.context,
).model_dump(),
)


@app.exception_handler(Exception)
async def unhandled_exception_handler(_request: Request, exc: Exception):
logger.exception("Unhandled Exception occurred : %s", exc)
return JSONResponse(
status_code=500,
content=ErrorResponse(
detail="Internal Server Error", code="internal_error", status_code=500
).model_dump(),
)


def custom_openapi():
if app.openapi_schema:
Expand Down Expand Up @@ -56,8 +103,5 @@ def health_check():
app.include_router(api_router, prefix=settings.API_V1_STR, tags=["v1"])

# TODO:

# add middleware for CORS
# add middleware for request and response
# add basic logging setup
# add exception handling for common errors
10 changes: 10 additions & 0 deletions app/schemas/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import Optional

from pydantic import BaseModel


class ErrorResponse(BaseModel):
detail: str
code: str
status_code: int
context: Optional[dict] = None
3 changes: 3 additions & 0 deletions app/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class Settings(BaseSettings):
GOOGLE_CLIENT_ID: str
GOOGLE_CLIENT_SECRET: str

# Services endpoints
GENERATOR_SERVICE_URL: str

class Config:
"""Configuration for the settings."""

Expand Down
49 changes: 49 additions & 0 deletions app/utils/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
class AppException(Exception):
"""Base application exception with additional context."""

def __init__(
self,
detail: str,
code: str = "app_error",
status_code: int = 400,
context: dict | None = None,
):
self.detail = detail
self.code = code
self.status_code = status_code
self.context = context or {}
super().__init__(detail) # keeps the built-in Exception message


class RequestTimeoutError(AppException):
"""Raised when a request times out."""

def __init__(self, detail: str = "Request timed out", context: dict | None = None):
super().__init__(
detail=detail, code="timeout_error", status_code=408, context=context
)


class RequestConnectionError(AppException):
"""Raised when a connection to the server fails."""

def __init__(
self, detail: str = "Failed to connect to server", context: dict | None = None
):
super().__init__(
detail=detail, code="connection_error", status_code=503, context=context
)


class RequestHTTPError(AppException):
"""Raised for non-2xx HTTP responses."""

def __init__(
self,
detail: str = "HTTP error occurred",
status_code: int = 500,
context: dict | None = None,
):
super().__init__(
detail=detail, code="http_error", status_code=status_code, context=context
)
57 changes: 57 additions & 0 deletions app/utils/http_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from typing import Any, Dict, Optional

import requests

from app.utils.exceptions import (AppException, RequestConnectionError,
RequestHTTPError, RequestTimeoutError)


def make_request(
method: str,
url: str,
params: Optional[Dict[str, Any]] = None,
data: Optional[Any] = None,
json: Optional[Any] = None,
headers: Optional[Dict[str, str]] = None,
timeout: int = 60,
) -> Dict[str, Any]:
"""
Generic HTTP request utility.
"""
try:
response = requests.request(
method=method,
url=url,
params=params,
data=data,
json=json,
headers=headers,
timeout=timeout,
)
response.raise_for_status()
return response.json()

except requests.Timeout as exc:
raise RequestTimeoutError(
"The request timed out", context={"url": url}
) from exc

except requests.ConnectionError as exc:
raise RequestConnectionError(
"Failed to connect to the service", context={"url": url}
) from exc

except requests.HTTPError as exc:
status_code = exc.response.status_code if exc.response else 500
message = exc.response.text if exc.response else str(exc)
raise RequestHTTPError(
detail=message, status_code=status_code, context={"url": url}
) from exc

except Exception as exc:
# Catch-all wrapped in your base AppException
raise AppException(
"Unexpected error in HTTP request",
code="unexpected_request_error",
context={"url": url, "error": str(exc)},
) from exc
2 changes: 1 addition & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ echo "Running Alembic migrations..."
alembic upgrade head

echo "Starting FastAPI app..."
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
2 changes: 2 additions & 0 deletions local_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
POSTGRES_DB=JamAndFlow
POSTGRES_HOST=db
POSTGRES_PORT=5432

GENERATOR_SERVICE_URL=http://generator-service:8000
```

# TODO: Add docker setup instructions below
Expand Down