From fa220ad125a8c519994890fc6ccde6c6c46c9df0 Mon Sep 17 00:00:00 2001 From: KevinRawal Date: Sat, 23 Aug 2025 15:11:13 +0530 Subject: [PATCH 1/3] added question route --- app/api/main.py | 3 ++- app/api/routes/questions.py | 16 +++++++++++++++ app/settings.py | 3 +++ app/utils/http_client.py | 41 +++++++++++++++++++++++++++++++++++++ entrypoint.sh | 2 +- local_setup.md | 2 ++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 app/api/routes/questions.py create mode 100644 app/utils/http_client.py diff --git a/app/api/main.py b/app/api/main.py index d5ce163..d7974a2 100644 --- a/app/api/main.py +++ b/app/api/main.py @@ -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"]) diff --git a/app/api/routes/questions.py b/app/api/routes/questions.py new file mode 100644 index 0000000..fbc36df --- /dev/null +++ b/app/api/routes/questions.py @@ -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) diff --git a/app/settings.py b/app/settings.py index 64b9ffb..ad48c51 100644 --- a/app/settings.py +++ b/app/settings.py @@ -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.""" diff --git a/app/utils/http_client.py b/app/utils/http_client.py new file mode 100644 index 0000000..76c683a --- /dev/null +++ b/app/utils/http_client.py @@ -0,0 +1,41 @@ +from typing import Any, Dict, Optional + +import requests + + +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. + Args: + method: HTTP method (GET, POST, etc.) + url: Full URL to request + params: Query parameters + data: Form data + json: JSON body + headers: HTTP headers + timeout: Timeout in seconds + Returns: + Response JSON or error dict + """ + 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.RequestException as e: + return {"error": "Request failed", "details": str(e)} diff --git a/entrypoint.sh b/entrypoint.sh index 506653c..2bebe1b 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -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 diff --git a/local_setup.md b/local_setup.md index 68f8bed..52bbd14 100644 --- a/local_setup.md +++ b/local_setup.md @@ -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 From 463f11d97030193db8191a4a437ff9fc2307b183 Mon Sep 17 00:00:00 2001 From: KevinRawal Date: Sat, 23 Aug 2025 16:53:38 +0530 Subject: [PATCH 2/3] added http_client utility, generic exception handeler --- app/main.py | 49 ++++++++++++++++++++++++++++++++++++++- app/schemas/exceptions.py | 10 ++++++++ app/utils/exceptions.py | 49 +++++++++++++++++++++++++++++++++++++++ app/utils/http_client.py | 40 ++++++++++++++++++++++---------- 4 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 app/schemas/exceptions.py create mode 100644 app/utils/exceptions.py diff --git a/app/main.py b/app/main.py index 4105d7a..a611b24 100644 --- a/app/main.py +++ b/app/main.py @@ -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: diff --git a/app/schemas/exceptions.py b/app/schemas/exceptions.py new file mode 100644 index 0000000..1e2383c --- /dev/null +++ b/app/schemas/exceptions.py @@ -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 diff --git a/app/utils/exceptions.py b/app/utils/exceptions.py new file mode 100644 index 0000000..9222b78 --- /dev/null +++ b/app/utils/exceptions.py @@ -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 + ) diff --git a/app/utils/http_client.py b/app/utils/http_client.py index 76c683a..d5c28ba 100644 --- a/app/utils/http_client.py +++ b/app/utils/http_client.py @@ -2,6 +2,9 @@ import requests +from app.utils.exceptions import (AppException, RequestConnectionError, + RequestHTTPError, RequestTimeoutError) + def make_request( method: str, @@ -14,16 +17,6 @@ def make_request( ) -> Dict[str, Any]: """ Generic HTTP request utility. - Args: - method: HTTP method (GET, POST, etc.) - url: Full URL to request - params: Query parameters - data: Form data - json: JSON body - headers: HTTP headers - timeout: Timeout in seconds - Returns: - Response JSON or error dict """ try: response = requests.request( @@ -37,5 +30,28 @@ def make_request( ) response.raise_for_status() return response.json() - except requests.RequestException as e: - return {"error": "Request failed", "details": str(e)} + + 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 From c6b02365e8852112482004fdd3294bf99ca8c4c5 Mon Sep 17 00:00:00 2001 From: KevinRawal Date: Sat, 23 Aug 2025 17:17:53 +0530 Subject: [PATCH 3/3] minor changes --- app/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/main.py b/app/main.py index a611b24..f6e9a5e 100644 --- a/app/main.py +++ b/app/main.py @@ -103,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