154 lines
5.3 KiB
Python
154 lines
5.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 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 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_string
|
|
# for example, return `A` if the type_string 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_string 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)
|