Files
xiaozhi-esp32-server/main/manager-api-fastapi/app/core/serialization.py
T

106 lines
3.4 KiB
Python

from __future__ import annotations
import dataclasses
import re
from collections.abc import Mapping, Sequence
from datetime import date, datetime, time
from decimal import Decimal
from enum import Enum
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
from pydantic import BaseModel
from app.core.config import get_settings
_SNAKE_PART = re.compile(r"_([a-zA-Z0-9])")
_LONG_FIELD_NAMES = {
"id",
"userId",
"creator",
"updater",
"createUserId",
"updateUserId",
"createDateTimestamp",
"createTime",
"createTimeFrom",
"createTimeTo",
"fileSize",
"lastConnectedAtTimestamp",
"pid",
"reportTime",
"size",
"timestamp",
"tokenCount",
"tokenNum",
"totalDocCount",
"totalTokenCount",
"updateTime",
}
class JavaMap(dict[str, Any]):
"""Marker for Java ``Map`` payloads whose keys Jackson leaves untouched."""
def preserve_java_map_keys(value: Any) -> Any:
"""Recursively mark a dynamic Java Map/List graph as key-preserving."""
if isinstance(value, Mapping):
return JavaMap({str(key): preserve_java_map_keys(item) for key, item in value.items()})
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
return [preserve_java_map_keys(item) for item in value]
return value
def snake_to_camel(value: str) -> str:
return _SNAKE_PART.sub(lambda match: match.group(1).upper(), value)
def _is_long_field(name: str | None) -> bool:
if not name:
return False
return name in _LONG_FIELD_NAMES or name.endswith("Id") or name.endswith("Ids")
def java_compatible(value: Any, *, field_name: str | None = None) -> Any:
if value is None or isinstance(value, str | bool | float):
return value
if isinstance(value, BaseModel):
return java_compatible(value.model_dump(by_alias=True, exclude_unset=False), field_name=field_name)
if dataclasses.is_dataclass(value) and not isinstance(value, type):
return java_compatible(dataclasses.asdict(value), field_name=field_name)
if isinstance(value, Enum):
return java_compatible(value.value, field_name=field_name)
if isinstance(value, datetime):
timezone = ZoneInfo(get_settings().timezone)
localized = value.astimezone(timezone) if value.tzinfo else value
return localized.strftime("%Y-%m-%d %H:%M:%S")
if isinstance(value, date):
return value.strftime("%Y-%m-%d")
if isinstance(value, time):
return value.strftime("%H:%M:%S")
if isinstance(value, Decimal):
return float(value)
if isinstance(value, int):
return str(value) if _is_long_field(field_name) or not -(2**31) <= value < 2**31 else value
if isinstance(value, bytes):
return value
if isinstance(value, Path):
return str(value)
if isinstance(value, JavaMap):
return {
str(raw_key): java_compatible(item, field_name=snake_to_camel(str(raw_key)))
for raw_key, item in value.items()
}
if isinstance(value, Mapping):
result: dict[str, Any] = {}
for raw_key, item in value.items():
key = snake_to_camel(str(raw_key))
result[key] = java_compatible(item, field_name=key)
return result
if isinstance(value, Sequence) and not isinstance(value, str | bytes | bytearray):
return [java_compatible(item, field_name=field_name) for item in value]
return value