lodum.yaml
1# SPDX-FileCopyrightText: 2025-present Michael R. Bernstein <zopemaven@gmail.com> 2# 3# SPDX-License-Identifier: Apache-2.0 4import io 5from typing import Any, Dict, Iterator, Type, TypeVar 6 7try: 8 from ruamel.yaml import YAML 9 10 yaml_available = True 11except ImportError: 12 YAML = None # type: ignore 13 yaml_available = False 14 15from .core import Loader, BaseDumper, BaseLoader 16from .internal import dump, load, DEFAULT_MAX_SIZE, generate_schema 17from .exception import DeserializationError 18 19T = TypeVar("T") 20yaml: Any = None 21if yaml_available: 22 yaml = YAML(typ="safe") 23 yaml.sort_base_mapping_type_on_output = False 24 25# --- Public API --- 26 27 28def dumps(obj: Any) -> str: 29 """ 30 Encodes a Python object to a YAML string. 31 32 Args: 33 obj: The object to encode. Must be lodum-enabled or a supported type. 34 35 Returns: 36 A YAML string representation of the object. 37 38 Raises: 39 ImportError: If ruamel.yaml is not installed. 40 """ 41 if not yaml_available: 42 raise ImportError( 43 "ruamel.yaml is required for YAML serialization. Install it with 'pip install lodum[yaml]'." 44 ) 45 46 dumper = YamlDumper() 47 dumped_data = dump(obj, dumper) 48 49 with io.StringIO() as string_stream: 50 yaml.dump(dumped_data, string_stream) 51 return string_stream.getvalue() 52 53 54def loads(cls: Type[T], yaml_string: str, max_size: int = DEFAULT_MAX_SIZE) -> T: 55 """ 56 Decodes a YAML string into a Python object of the specified type. 57 58 Args: 59 cls: The class to instantiate. 60 yaml_string: The YAML data to decode. 61 max_size: Maximum allowed size of the input string in bytes. 62 63 Returns: 64 An instance of cls populated with the decoded data. 65 66 Raises: 67 DeserializationError: If the input is invalid or exceeds max_size. 68 ImportError: If ruamel.yaml is not installed. 69 """ 70 if len(yaml_string) > max_size: 71 raise DeserializationError( 72 f"Input size ({len(yaml_string)}) exceeds maximum allowed ({max_size})" 73 ) 74 75 if not yaml_available: 76 raise ImportError( 77 "ruamel.yaml is required for YAML deserialization. Install it with 'pip install lodum[yaml]'." 78 ) 79 80 data = yaml.load(yaml_string) 81 loader = YamlLoader(data) 82 return load(cls, loader) 83 84 85def schema(cls: Type[Any]) -> Dict[str, Any]: 86 """Generates a JSON Schema for a given lodum-enabled class.""" 87 return generate_schema(cls) 88 89 90# --- YAML Dumper Implementation --- 91 92 93class YamlDumper(BaseDumper): 94 """ 95 Encodes Python objects into a YAML-compatible intermediate representation. 96 """ 97 98 def dump_bytes(self, value: bytes) -> Any: 99 # YAML can handle bytes natively if using certain tags, 100 # but for simplicity and cross-format consistency, we'll use base64 like JSON. 101 import base64 102 103 return base64.b64encode(value).decode("ascii") 104 105 106# --- YAML Loader Implementation --- 107 108 109class YamlLoader(BaseLoader): 110 """ 111 Decodes a YAML-compatible intermediate representation into Python objects. 112 """ 113 114 def load_list(self) -> Iterator["Loader"]: 115 if not isinstance(self._data, list): 116 raise DeserializationError( 117 f"Expected list, got {type(self._data).__name__}" 118 ) 119 return (YamlLoader(item) for item in self._data) 120 121 def load_dict(self) -> Iterator[tuple[str, "Loader"]]: 122 if not isinstance(self._data, dict): 123 raise DeserializationError( 124 f"Expected dict, got {type(self._data).__name__}" 125 ) 126 return ((k, YamlLoader(v)) for k, v in self._data.items()) 127 128 def load_bytes_value(self, value: Any) -> bytes: 129 if isinstance(value, bytes): 130 return value 131 if not isinstance(value, str): 132 raise DeserializationError(f"Expected str, got {type(value).__name__}") 133 import base64 134 135 try: 136 return base64.b64decode(value) 137 except Exception as e: 138 raise DeserializationError(f"Failed to decode base64: {e}")
29def dumps(obj: Any) -> str: 30 """ 31 Encodes a Python object to a YAML string. 32 33 Args: 34 obj: The object to encode. Must be lodum-enabled or a supported type. 35 36 Returns: 37 A YAML string representation of the object. 38 39 Raises: 40 ImportError: If ruamel.yaml is not installed. 41 """ 42 if not yaml_available: 43 raise ImportError( 44 "ruamel.yaml is required for YAML serialization. Install it with 'pip install lodum[yaml]'." 45 ) 46 47 dumper = YamlDumper() 48 dumped_data = dump(obj, dumper) 49 50 with io.StringIO() as string_stream: 51 yaml.dump(dumped_data, string_stream) 52 return string_stream.getvalue()
Encodes a Python object to a YAML string.
Args: obj: The object to encode. Must be lodum-enabled or a supported type.
Returns: A YAML string representation of the object.
Raises: ImportError: If ruamel.yaml is not installed.
55def loads(cls: Type[T], yaml_string: str, max_size: int = DEFAULT_MAX_SIZE) -> T: 56 """ 57 Decodes a YAML string into a Python object of the specified type. 58 59 Args: 60 cls: The class to instantiate. 61 yaml_string: The YAML data to decode. 62 max_size: Maximum allowed size of the input string in bytes. 63 64 Returns: 65 An instance of cls populated with the decoded data. 66 67 Raises: 68 DeserializationError: If the input is invalid or exceeds max_size. 69 ImportError: If ruamel.yaml is not installed. 70 """ 71 if len(yaml_string) > max_size: 72 raise DeserializationError( 73 f"Input size ({len(yaml_string)}) exceeds maximum allowed ({max_size})" 74 ) 75 76 if not yaml_available: 77 raise ImportError( 78 "ruamel.yaml is required for YAML deserialization. Install it with 'pip install lodum[yaml]'." 79 ) 80 81 data = yaml.load(yaml_string) 82 loader = YamlLoader(data) 83 return load(cls, loader)
Decodes a YAML string into a Python object of the specified type.
Args: cls: The class to instantiate. yaml_string: The YAML data to decode. max_size: Maximum allowed size of the input string in bytes.
Returns: An instance of cls populated with the decoded data.
Raises: DeserializationError: If the input is invalid or exceeds max_size. ImportError: If ruamel.yaml is not installed.
86def schema(cls: Type[Any]) -> Dict[str, Any]: 87 """Generates a JSON Schema for a given lodum-enabled class.""" 88 return generate_schema(cls)
Generates a JSON Schema for a given lodum-enabled class.
94class YamlDumper(BaseDumper): 95 """ 96 Encodes Python objects into a YAML-compatible intermediate representation. 97 """ 98 99 def dump_bytes(self, value: bytes) -> Any: 100 # YAML can handle bytes natively if using certain tags, 101 # but for simplicity and cross-format consistency, we'll use base64 like JSON. 102 import base64 103 104 return base64.b64encode(value).decode("ascii")
Encodes Python objects into a YAML-compatible intermediate representation.
110class YamlLoader(BaseLoader): 111 """ 112 Decodes a YAML-compatible intermediate representation into Python objects. 113 """ 114 115 def load_list(self) -> Iterator["Loader"]: 116 if not isinstance(self._data, list): 117 raise DeserializationError( 118 f"Expected list, got {type(self._data).__name__}" 119 ) 120 return (YamlLoader(item) for item in self._data) 121 122 def load_dict(self) -> Iterator[tuple[str, "Loader"]]: 123 if not isinstance(self._data, dict): 124 raise DeserializationError( 125 f"Expected dict, got {type(self._data).__name__}" 126 ) 127 return ((k, YamlLoader(v)) for k, v in self._data.items()) 128 129 def load_bytes_value(self, value: Any) -> bytes: 130 if isinstance(value, bytes): 131 return value 132 if not isinstance(value, str): 133 raise DeserializationError(f"Expected str, got {type(value).__name__}") 134 import base64 135 136 try: 137 return base64.b64decode(value) 138 except Exception as e: 139 raise DeserializationError(f"Failed to decode base64: {e}")
Decodes a YAML-compatible intermediate representation into Python objects.
129 def load_bytes_value(self, value: Any) -> bytes: 130 if isinstance(value, bytes): 131 return value 132 if not isinstance(value, str): 133 raise DeserializationError(f"Expected str, got {type(value).__name__}") 134 import base64 135 136 try: 137 return base64.b64decode(value) 138 except Exception as e: 139 raise DeserializationError(f"Failed to decode base64: {e}")