diff --git a/backend/src/api/packet_api.py b/backend/src/api/packet_api.py index 2a02f5f..ea06b50 100644 --- a/backend/src/api/packet_api.py +++ b/backend/src/api/packet_api.py @@ -3,8 +3,10 @@ import asyncio import base64 import json import logging -from typing import Optional, Any, Dict, List +from typing import Optional, Any, Dict, List, Union +from pydantic import BaseModel +from src.Models.packets import PacketDBModel from fastapi import APIRouter, Query, WebSocket, WebSocketDisconnect, HTTPException from fastapi.responses import JSONResponse @@ -14,30 +16,75 @@ logger = logging.getLogger("packets_router") router = APIRouter() -def _serialize_row_for_json(row: Dict[str, Any]) -> Dict[str, Any]: +def _serialize_row_for_json(row: Union[Dict[str, Any], PacketDBModel, BaseModel]) -> Dict[str, Any]: """ - Convert DB row / pkt_info to a JSON-serializable dict. - - If 'raw' is bytes, produce 'raw_b64' and drop 'raw'. - - Fallback to str() for unknown/unserializable values. + Convert a DB row or PacketDBModel into a JSON-serializable dict. + + - If `row` is a Pydantic model (PacketDBModel or BaseModel), use `.dict()` to get a plain dict. + - If `raw` is bytes/bytearray, produce `raw_b64` and drop `raw`. + - If `raw_b64` already exists, do not re-encode. + - For values that cannot be JSON serialized, fall back to str(value). """ + # If given a Pydantic model, convert to dict first + if isinstance(row, BaseModel): + d: Dict[str, Any] = row.dict(by_alias=True, exclude_none=True) + else: + # copy to avoid mutating caller's dict + d = dict(row) + + # If raw_b64 already present, prefer it. If raw present and bytes, convert. + raw_val = d.get("raw") + if raw_val is not None and isinstance(raw_val, (bytes, bytearray)): + # convert to base64 string and remove raw + try: + d["raw_b64"] = base64.b64encode(raw_val).decode("ascii") + d.pop("raw", None) + except Exception: + # keep raw as str fallback + try: + d["raw_b64"] = base64.b64encode(bytes(raw_val)).decode("ascii") + d.pop("raw", None) + except Exception: + logger.exception("Failed to base64-encode raw bytes for row id=%s", d.get("id")) + d["raw_b64"] = str(raw_val) + d.pop("raw", None) + + # Ensure final dict is JSON-safe: try json.dumps on each value, fallback to str() out: Dict[str, Any] = {} - for k, v in row.items(): - if k == "raw" and isinstance(v, (bytes, bytearray)): - out["raw_b64"] = base64.b64encode(v).decode("ascii") - continue - # try to JSON serialize the value directly + for k, v in d.items(): + # skip any private/internal keys if needed (optional) + # if k.startswith("_"): + # continue + + # raw_b64: ensure it's a str + if k == "raw_b64" and isinstance(v, (bytes, bytearray)): + try: + out["raw_b64"] = base64.b64encode(v).decode("ascii") + continue + except Exception: + out["raw_b64"] = str(v) + continue + + # JSON-serializable check try: json.dumps({k: v}) out[k] = v except (TypeError, ValueError): - out[k] = str(v) + # convert non-serializable to string representation + try: + out[k] = str(v) + except Exception: + out[k] = "" return out -async def _serialize_rows(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: +async def _serialize_rows(rows: List[Union[Dict[str, Any], PacketDBModel]]) -> List[Dict[str, Any]]: + """ + Serialize a list of DB rows or PacketDBModel instances into JSON-ready dicts. + Keeps the same order as input. + """ return [_serialize_row_for_json(r) for r in rows] - @router.get("/packets") async def get_packets(limit: int = Query(100, ge=1, le=10000)): """