Coverage for src / dataknobs_common / serialization.py: 36%

42 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-08 17:37 -0700

1"""Serialization protocols and utilities for dataknobs packages. 

2 

3This module provides standard interfaces for objects that can be serialized 

4to and from dictionaries. This enables consistent serialization patterns 

5across all dataknobs packages. 

6 

7The serialization framework supports: 

8- Type-safe protocols for serializable objects 

9- Utility functions for serialization/deserialization 

10- Runtime type checking with isinstance() 

11- Integration with dataclasses and custom classes 

12 

13Example: 

14 ```python 

15 from dataknobs_common.serialization import Serializable 

16 from dataclasses import dataclass 

17 

18 @dataclass 

19 class User: 

20 name: str 

21 email: str 

22 

23 def to_dict(self) -> dict: 

24 return {"name": self.name, "email": self.email} 

25 

26 @classmethod 

27 def from_dict(cls, data: dict) -> "User": 

28 return cls(name=data["name"], email=data["email"]) 

29 

30 # Type checking works 

31 user = User("Alice", "alice@example.com") 

32 assert isinstance(user, Serializable) # True 

33 

34 # Use utilities 

35 from dataknobs_common.serialization import serialize, deserialize 

36 

37 data = serialize(user) 

38 restored = deserialize(User, data) 

39 ``` 

40""" 

41 

42from typing import Any, Dict, Protocol, Type, TypeVar, runtime_checkable 

43 

44from dataknobs_common.exceptions import SerializationError 

45 

46T = TypeVar("T") 

47 

48 

49@runtime_checkable 

50class Serializable(Protocol): 

51 """Protocol for objects that can be serialized to/from dict. 

52 

53 Implement this protocol by providing to_dict() and from_dict() methods. 

54 The @runtime_checkable decorator allows isinstance() checks at runtime. 

55 

56 Methods: 

57 to_dict: Convert object to dictionary representation 

58 from_dict: Create object from dictionary representation 

59 

60 Example: 

61 ```python 

62 class MyClass: 

63 def __init__(self, value: str): 

64 self.value = value 

65 

66 def to_dict(self) -> dict: 

67 return {"value": self.value} 

68 

69 @classmethod 

70 def from_dict(cls, data: dict) -> "MyClass": 

71 return cls(data["value"]) 

72 

73 obj = MyClass("test") 

74 isinstance(obj, Serializable) 

75 # True 

76 ``` 

77 """ 

78 

79 def to_dict(self) -> Dict[str, Any]: 

80 """Convert object to dictionary representation. 

81 

82 Returns: 

83 Dictionary with serialized data 

84 

85 Raises: 

86 SerializationError: If serialization fails 

87 """ 

88 ... 

89 

90 @classmethod 

91 def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: 

92 """Create object from dictionary representation. 

93 

94 Args: 

95 data: Dictionary with serialized data 

96 

97 Returns: 

98 Deserialized object instance 

99 

100 Raises: 

101 SerializationError: If deserialization fails 

102 """ 

103 ... 

104 

105 

106def serialize(obj: Any) -> Dict[str, Any]: 

107 """Serialize an object to dictionary. 

108 

109 Convenience function that calls to_dict() with error handling. 

110 

111 Args: 

112 obj: Object to serialize (must have to_dict method) 

113 

114 Returns: 

115 Serialized dictionary 

116 

117 Raises: 

118 SerializationError: If object doesn't support serialization or serialization fails 

119 

120 Example: 

121 ```python 

122 class Point: 

123 def __init__(self, x: int, y: int): 

124 self.x, self.y = x, y 

125 def to_dict(self): 

126 return {"x": self.x, "y": self.y} 

127 

128 point = Point(10, 20) 

129 data = serialize(point) 

130 # {'x': 10, 'y': 20} 

131 ``` 

132 """ 

133 if not hasattr(obj, "to_dict"): 

134 raise SerializationError( 

135 f"Object of type {type(obj).__name__} is not serializable (missing to_dict method)", 

136 context={"type": type(obj).__name__, "object": str(obj)}, 

137 ) 

138 

139 try: 

140 result = obj.to_dict() 

141 if not isinstance(result, dict): 

142 raise SerializationError( 

143 f"to_dict() must return a dict, got {type(result).__name__}", 

144 context={"type": type(obj).__name__, "result_type": type(result).__name__}, 

145 ) 

146 return result 

147 except Exception as e: 

148 if isinstance(e, SerializationError): 

149 raise 

150 raise SerializationError( 

151 f"Failed to serialize {type(obj).__name__}: {e}", 

152 context={"type": type(obj).__name__, "error": str(e)}, 

153 ) from e 

154 

155 

156def deserialize(cls: Type[T], data: Dict[str, Any]) -> T: 

157 """Deserialize dictionary into an object. 

158 

159 Convenience function that calls from_dict() with error handling. 

160 

161 Args: 

162 cls: Class to deserialize into (must have from_dict classmethod) 

163 data: Dictionary with serialized data 

164 

165 Returns: 

166 Deserialized object instance 

167 

168 Raises: 

169 SerializationError: If class doesn't support deserialization or deserialization fails 

170 

171 Example: 

172 ```python 

173 class Point: 

174 def __init__(self, x: int, y: int): 

175 self.x, self.y = x, y 

176 @classmethod 

177 def from_dict(cls, data: dict): 

178 return cls(data["x"], data["y"]) 

179 

180 data = {"x": 10, "y": 20} 

181 point = deserialize(Point, data) 

182 # point.x, point.y 

183 # (10, 20) 

184 ``` 

185 """ 

186 if not hasattr(cls, "from_dict"): 

187 raise SerializationError( 

188 f"Class {cls.__name__} is not deserializable (missing from_dict classmethod)", 

189 context={"class": cls.__name__}, 

190 ) 

191 

192 if not isinstance(data, dict): 

193 raise SerializationError( 

194 f"Data must be a dict, got {type(data).__name__}", 

195 context={"class": cls.__name__, "data_type": type(data).__name__}, 

196 ) 

197 

198 try: 

199 return cls.from_dict(data) 

200 except Exception as e: 

201 if isinstance(e, SerializationError): 

202 raise 

203 raise SerializationError( 

204 f"Failed to deserialize {cls.__name__}: {e}", 

205 context={"class": cls.__name__, "error": str(e), "data": data}, 

206 ) from e 

207 

208 

209def serialize_list(items: list[Any]) -> list[Dict[str, Any]]: 

210 """Serialize a list of objects to list of dictionaries. 

211 

212 Args: 

213 items: List of serializable objects 

214 

215 Returns: 

216 List of serialized dictionaries 

217 

218 Raises: 

219 SerializationError: If any item cannot be serialized 

220 

221 Example: 

222 ```python 

223 items = [Point(1, 2), Point(3, 4)] 

224 data_list = serialize_list(items) 

225 len(data_list) 

226 # 2 

227 ``` 

228 """ 

229 return [serialize(item) for item in items] 

230 

231 

232def deserialize_list(cls: Type[T], data_list: list[Dict[str, Any]]) -> list[T]: 

233 """Deserialize a list of dictionaries into objects. 

234 

235 Args: 

236 cls: Class to deserialize into 

237 data_list: List of serialized dictionaries 

238 

239 Returns: 

240 List of deserialized objects 

241 

242 Raises: 

243 SerializationError: If any item cannot be deserialized 

244 

245 Example: 

246 ```python 

247 data_list = [{"x": 1, "y": 2}, {"x": 3, "y": 4}] 

248 points = deserialize_list(Point, data_list) 

249 len(points) 

250 # 2 

251 ``` 

252 """ 

253 return [deserialize(cls, data) for data in data_list] 

254 

255 

256def is_serializable(obj: Any) -> bool: 

257 """Check if an object is serializable. 

258 

259 Args: 

260 obj: Object to check 

261 

262 Returns: 

263 True if object has to_dict method 

264 

265 Example: 

266 ```python 

267 class Point: 

268 def to_dict(self): return {} 

269 

270 is_serializable(Point()) 

271 # True 

272 is_serializable("string") 

273 # False 

274 ``` 

275 """ 

276 return isinstance(obj, Serializable) or hasattr(obj, "to_dict") 

277 

278 

279def is_deserializable(cls: Type) -> bool: 

280 """Check if a class is deserializable. 

281 

282 Args: 

283 cls: Class to check 

284 

285 Returns: 

286 True if class has from_dict classmethod 

287 

288 Example: 

289 ```python 

290 class Point: 

291 @classmethod 

292 def from_dict(cls, data): return cls() 

293 

294 is_deserializable(Point) 

295 # True 

296 is_deserializable(str) 

297 # False 

298 ``` 

299 """ 

300 return hasattr(cls, "from_dict") 

301 

302 

303__all__ = [ 

304 "Serializable", 

305 "serialize", 

306 "deserialize", 

307 "serialize_list", 

308 "deserialize_list", 

309 "is_serializable", 

310 "is_deserializable", 

311]