""" 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 ":{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 ":{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)