Feature/windows notifications (#7)

Reviewed-on: #7
Co-authored-by: Florian <flo@meergans.org>
This commit was merged in pull request #7.
This commit is contained in:
florian authored and florian committed 2026-10-13 07:51:36 +00:00
1 parent cd26070a3b
commit 2b69f3146e
6 files changed
+241 -74

No files matched your search

+92 -34
View File
@@ -1,29 +1,39 @@
from fastapi import FastAPI, Depends, HTTPException, Request, Response
import asyncio
import os
from contextlib import asynccontextmanager
from typing import Dict
import httpx
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.security.api_key import APIKeyHeader
from pydantic import BaseModel, Field
from starlette.exceptions import HTTPException as StarletteHTTPException
from typing import Dict
from pydantic import BaseModel
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
from rabbitmq_handler import RabbitMQProducer
import uvicorn
from contextlib import asynccontextmanager
from db_module import close_connection_pool, create_connection_pool, get_db, start_healthcheck_thread
from metrics_server import REQUEST_COUNTER
import asyncio
from uvicorn_logger_config import LOGGING_CONFIG
from rabbitmq_handler import RabbitMQProducer
from secret_manager import cleanup_secret_files
from simple_logger_handler import LOG_LEVEL, setup_logger
from uvicorn_logger_config import LOGGING_CONFIG
from validator import verify_api_key
logger = setup_logger(__name__)
producer = RabbitMQProducer()
api_key_header_internal = APIKeyHeader(name="X-API-Key-Internal")
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):
@@ -34,25 +44,42 @@ async def lifespan(app: FastAPI):
start_healthcheck_thread()
logger.info("MySQL healthcheck thread started.")
logger.info("Starting RabbitMQ producer...")
await producer.connect()
app.state.rmq_producer = producer
logger.info("[FastAPI] RabbitMQ producer initialized.")
producer = RabbitMQProducer()
PULL_SERVICE_URL = os.getenv(
"BACKEND_PULL_NOTIFICATIONS_URL",
"http://backend-pull-notifications.app-notifications.svc.cluster.local:8101",
)
SECRET_PATHS = frozenset({
"/etc/secrets/db_username",
"/etc/secrets/db_password",
"/etc/secrets/rmq_username",
"/etc/secrets/rmq_password"
})
cleanup_secret_files(SECRET_PATHS)
try:
logger.info("Starting RabbitMQ producer...")
await producer.connect()
app.state.rmq_producer = producer
logger.info("[FastAPI] RabbitMQ producer initialized.")
yield
logger.info("Closing RabbitMQ producer...")
await producer.close()
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.")
logger.info("Closing MySQL connection pool...")
close_connection_pool()
SECRET_PATHS = frozenset({
"/etc/secrets/db_username",
"/etc/secrets/db_password",
"/etc/secrets/rmq_username",
"/etc/secrets/rmq_password"
})
cleanup_secret_files(SECRET_PATHS)
yield
finally:
logger.info("Closing RabbitMQ producer...")
await producer.close()
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",
@@ -78,10 +105,10 @@ async def prometheus_middleware(request: Request, call_next):
return response
def verify_api_key_dependency_internal(db=Depends(get_db), api_key: str = Depends(api_key_header_internal)) -> str:
logger.debug(f"Verifying internal API key: {api_key}")
cursor = db.cursor()
cursor.execute("SELECT program_name, api_key FROM internal_api_keys WHERE status = 'active'")
for program_name, hashed_key in cursor.fetchall():
with db.cursor() as cursor:
cursor.execute("SELECT program_name, api_key FROM internal_api_keys WHERE status = 'active'")
rows = cursor.fetchall()
for program_name, hashed_key in rows:
if verify_api_key(api_key=api_key, hashed=hashed_key):
logger.debug(f"API key verified for program: {program_name}")
return program_name
@@ -101,7 +128,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,
@@ -118,6 +145,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") from e
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)