183 lines
6.3 KiB
Python
183 lines
6.3 KiB
Python
"""
|
|
This module provides serialization and deserialization functions for Pydantic models that extend BaseModel
|
|
"""
|
|
|
|
import importlib
|
|
import json
|
|
import uuid
|
|
from datetime import date, datetime
|
|
from types import UnionType
|
|
from typing import Any, Dict, Union, get_args, get_origin
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from amqp.adapter.logging_utils import logging_debug
|
|
|
|
|
|
def serialize_object(obj: BaseModel) -> str:
|
|
"""
|
|
Serializes an R2RSerializable object to a string in the format
|
|
"<ClassName>:{key1:value1, key2:value2,...}". Handles UUIDs and datetimes,
|
|
and nested Pydantic models.
|
|
|
|
Args:
|
|
obj: The BaseModel object to serialize.
|
|
|
|
Returns:
|
|
A string representation of the object.
|
|
"""
|
|
class_name = obj.__class__.__name__
|
|
module_name = obj.__class__.__module__
|
|
obj_dict = {}
|
|
for key, _ in obj.model_dump().items():
|
|
# here we use the original value
|
|
# the one returned by model_dump will convert everything into dict recursively
|
|
value = getattr(obj, key)
|
|
if isinstance(value, uuid.UUID):
|
|
obj_dict[key] = str(value)
|
|
elif isinstance(value, datetime):
|
|
obj_dict[key] = value.isoformat()
|
|
elif isinstance(value, dict): # Ensure dictionaries are handled
|
|
obj_dict[key] = value
|
|
elif isinstance(value, BaseModel):
|
|
obj_dict[key] = serialize_object(value) # Recursively serialize
|
|
elif isinstance(value, list):
|
|
serialized_list = []
|
|
for item in value:
|
|
if isinstance(item, BaseModel):
|
|
serialized_list.append(serialize_object(item))
|
|
else:
|
|
serialized_list.append(item)
|
|
obj_dict[key] = serialized_list
|
|
else:
|
|
obj_dict[key] = value
|
|
|
|
dict_string = json.dumps(obj_dict)
|
|
return f"{module_name}.{class_name}:{dict_string}"
|
|
|
|
|
|
def python_type_to_json(data):
|
|
"""
|
|
Convert Python objects (dict, list, tuple, set, datetime, etc.) to a JSON string.
|
|
Handles common non-JSON-serializable types like datetime, date, and sets.
|
|
|
|
Args:
|
|
data: Python object to be converted (dict, list, tuple, set, etc.)
|
|
|
|
Returns:
|
|
str: JSON string representation of the input data
|
|
|
|
Raises:
|
|
TypeError: If the object contains non-serializable types (e.g., custom classes)
|
|
"""
|
|
|
|
def default_serializer(obj):
|
|
"""Custom serializer for non-JSON-serializable objects."""
|
|
if isinstance(obj, (datetime, date)):
|
|
return obj.isoformat() # Convert datetime/date to ISO string
|
|
elif isinstance(obj, set):
|
|
return list(obj) # Convert sets to lists
|
|
elif hasattr(obj, "__dict__"):
|
|
return obj.__dict__ # Attempt to serialize custom objects
|
|
else:
|
|
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
|
|
|
|
return json.dumps(data, default=default_serializer, indent=0, ensure_ascii=False)
|
|
|
|
|
|
def try_deserialize_object(data: str) -> BaseModel | str:
|
|
"""
|
|
Try deserialize object, if failed, return the input string as-is.
|
|
"""
|
|
if not isinstance(data, str):
|
|
return data
|
|
try:
|
|
return deserialize_object(data)
|
|
except Exception as e:
|
|
logging_debug(f"Failed to deserialize object: {e}")
|
|
return data
|
|
|
|
|
|
def deserialize_object(data: str) -> BaseModel:
|
|
"""
|
|
Deserializes a string in the format "<ClassName>:{key1:value1, key2:value2,...}"
|
|
back into an BaseModel object. Handles UUIDs, datetimes, and
|
|
nested Pydantic models.
|
|
|
|
Args:
|
|
data: The string to deserialize.
|
|
|
|
Returns:
|
|
An R2RSerializable object.
|
|
"""
|
|
class_path, dict_str = data.split(":", 1)
|
|
module_path, class_name = class_path.rsplit(".", 1)
|
|
|
|
obj_dict: Dict[str, Any] = json.loads(dict_str)
|
|
|
|
logging_debug(f"Deserialized object: {class_path}")
|
|
if "[" in class_name:
|
|
class_name = class_name.split("[")[0]
|
|
logging_debug(f"trying object: {class_name}")
|
|
module = importlib.import_module(module_path)
|
|
cls = getattr(module, class_name)
|
|
if cls is None:
|
|
raise ValueError(f"Unknown class: {class_name}")
|
|
|
|
# process nested model, dict and list
|
|
for key, value in obj_dict.items():
|
|
key_type: type[Any] = cls.model_fields[key].annotation
|
|
# handle union with none, this will give the true type
|
|
# for example, return `A` if the type is `A | None`
|
|
if get_origin(key_type) is UnionType or get_origin(key_type) is Union:
|
|
union_types = get_args(key_type)
|
|
if len(union_types) > 2:
|
|
# if there are multiple unions, like `A | B | None`
|
|
# then we have no way to figure out which one to use
|
|
raise ValueError(f"Unsupported Union type: {key_type}")
|
|
# return the none-None one as the type for processing
|
|
if union_types[0] is type(None):
|
|
key_type = union_types[1]
|
|
else:
|
|
key_type = union_types[0]
|
|
# skip if the value is None
|
|
if value is None:
|
|
continue
|
|
|
|
if key_type == uuid.UUID:
|
|
obj_dict[key] = uuid.UUID(value)
|
|
elif key_type == datetime:
|
|
obj_dict[key] = datetime.fromisoformat(value)
|
|
elif key_type == dict:
|
|
obj_dict[key] = value
|
|
elif issubclass(key_type, BaseModel):
|
|
obj_dict[key] = deserialize_object(value)
|
|
elif key_type == list:
|
|
obj_dict[key] = list(map(try_deserialize_object, value))
|
|
|
|
return cls(**obj_dict)
|
|
|
|
|
|
def parse_url_query(url):
|
|
"""
|
|
Parses a HTTP GET URL and returns (context_path, query_params_dict)
|
|
|
|
Args:
|
|
url: The URL string to parse (e.g., "http://example.com/path?foo=1&bar=2&foo=3")
|
|
|
|
Returns:
|
|
tuple: (context_path, query_params_dict)
|
|
- context_path: The path part of the URL (e.g., "/path")
|
|
- query_params_dict: Dictionary of query parameters where values are always lists
|
|
(e.g., {"foo": ["1", "3"], "bar": ["2"]})
|
|
"""
|
|
parsed = urlparse(url)
|
|
query_params = parse_qs(parsed.query)
|
|
|
|
# Convert values from lists to single values when there's only one value
|
|
# If you prefer to always keep values as lists, remove this comprehension
|
|
simplified_params = {k: v[0] if len(v) == 1 else v for k, v in query_params.items()}
|
|
|
|
return (parsed.path, simplified_params)
|