diff --git a/requirements.txt b/requirements.txt index 4315fff..c0b23f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,10 +5,13 @@ annotated-types==0.7.0 anyio==4.11.0 argon2-cffi==25.1.0 argon2-cffi-bindings==25.1.0 +certifi==2026.7.22 cffi==2.0.0 click==8.3.0 fastapi==0.119.0 h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 idna==3.11 lib-db-module==0.2.0 lib-secret-manager==0.2.0 diff --git a/src/main.py b/src/main.py index 145f07c..18477f8 100644 --- a/src/main.py +++ b/src/main.py @@ -3,7 +3,7 @@ from fastapi.responses import JSONResponse from fastapi.security.api_key import APIKeyHeader from starlette.exceptions import HTTPException as StarletteHTTPException from typing import Dict -from pydantic import BaseModel +from pydantic import BaseModel, Field from validator import verify_api_key from db_module import get_db, create_connection_pool, close_connection_pool, start_healthcheck_thread from simple_logger_handler import setup_logger, LOG_LEVEL @@ -14,15 +14,29 @@ from metrics_server import REQUEST_COUNTER import asyncio from uvicorn_logger_config import LOGGING_CONFIG from secret_manager import cleanup_secret_files +import httpx +import os + logger = setup_logger(__name__) api_key_header_internal = APIKeyHeader(name="X-API-Key-Internal") +PULL_SERVICE_URL = os.getenv( + "BACKEND_PULL_NOTIFICATIONS_URL", + "http://backend-pull-notifications.app-notifications.svc.cluster.local:8101", +) +MAX_PAGES = 5 class Notification(BaseModel): - receipent_user_id : int + receipent_user_id : int = Field(gt=0) #ToDo fix among all services message : Dict +class PullResponse(BaseModel): + messages: list[dict] + count: int + has_more: bool +class PullRequest(BaseModel): + user_id: int = Field(gt=0) @asynccontextmanager async def lifespan(app: FastAPI): @@ -41,6 +55,12 @@ async def lifespan(app: FastAPI): app.state.rmq_producer = producer logger.info("[FastAPI] RabbitMQ producer initialized.") + app.state.http_client = httpx.AsyncClient ( + base_url=PULL_SERVICE_URL, + timeout=httpx.Timeout(10.0,connect=3.0) + ) + logger.info("[FastAPI] HTTP client initialized.") + SECRET_PATHS = frozenset({ "/etc/secrets/db_username", "/etc/secrets/db_password", @@ -57,6 +77,9 @@ async def lifespan(app: FastAPI): logger.info("Closing MySQL connection pool...") close_connection_pool() + await app.state.http_client.aclose() + logger.info("[FastAPI] Closing Http client") + api = FastAPI( title="Internal Notifier API", description="API to forward messages to RabbitMQ", @@ -104,7 +127,7 @@ async def custom_http_exception_handler(request,exc): content={"detail": exc.detail} ) -@api.post("/internal/receive-notifications") +@api.post("/internal/receive-notifications") #ToDo fix among all services async def receive_notifications( notification_data: Notification, request: Request, @@ -121,6 +144,37 @@ async def receive_notifications( logger.info("Successfully queued for delivery to RMQ") return {"status": "queued"} +@api.post("/internal/pull-notifications", response_model=PullResponse) +async def pull_notifications( + pull_request: PullRequest, + request: Request, + program_name: str = Depends(verify_api_key_dependency_internal), +): + user_id = pull_request.user_id + logger.debug(f"[FastAPI] User id {user_id} is requesting messages") + client: httpx.AsyncClient = request.app.state.http_client + + messages: list[dict] = [] + has_more = False + + try: + for _ in range(MAX_PAGES): + resp = await client.post("/internal/pull-notifications", json={"user_id": user_id}) + resp.raise_for_status() + data = PullResponse.model_validate(resp.json()) + + messages.extend(data.messages) + has_more = data.has_more + if not has_more: + break + except (httpx.HTTPError, ValueError) as e: + logger.error(f"[FastAPI] Pull service failed for user_id={user_id}: {e!r}") + status = 504 if isinstance(e, httpx.TimeoutException) else 502 + raise HTTPException(status_code=status, detail="Notification service unavailable") + + logger.info(f"[FastAPI] Returning {len(messages)} message(s) for user_id={user_id}, has_more={has_more}") + return PullResponse(messages=messages, count=len(messages), has_more=has_more) + async def start_servers(): logger.debug("Starting FastAPI and metrics servers") config_main = uvicorn.Config("main:api", host="0.0.0.0", port=8101, log_level=LOG_LEVEL.lower(), log_config=LOGGING_CONFIG)