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

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

2

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

4 to and from dictionaries. This enables consistent serialization patterns

5 across all dataknobs packages.

6

7 The 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

13 Example:

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

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

43

44 from dataknobs_common.exceptions import SerializationError

45

46 T = TypeVar("T")

47

48

49@runtime_checkable

50 class 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

106 def 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

156 def 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

209 def 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

232 def 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

256 def 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

279 def 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]