Examples
Models used in Unit Tests
1# This file is part of py-serializable
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15# SPDX-License-Identifier: Apache-2.0
16# Copyright (c) Paul Horton. All Rights Reserved.
17
18import re
19from datetime import date
20from decimal import Decimal
21from enum import Enum, unique
22from typing import Any, Dict, Iterable, List, Optional, Set, Type
23from uuid import UUID, uuid4
24
25import py_serializable
26from py_serializable import ViewType, XmlArraySerializationType, XmlStringSerializationType
27from py_serializable.helpers import BaseHelper, Iso8601Date
28
29"""
30Model classes used in unit tests and examples.
31"""
32
33
34class SchemaVersion1(ViewType):
35 pass
36
37
38class SchemaVersion2(ViewType):
39 pass
40
41
42class SchemaVersion3(ViewType):
43 pass
44
45
46class SchemaVersion4(ViewType):
47 pass
48
49
50SCHEMAVERSION_MAP: Dict[int, Type[ViewType]] = {
51 1: SchemaVersion1,
52 2: SchemaVersion2,
53 3: SchemaVersion3,
54 4: SchemaVersion4,
55}
56
57
58class ReferenceReferences(BaseHelper):
59
60 @classmethod
61 def serialize(cls, o: Any) -> Set[str]:
62 if isinstance(o, set):
63 return set(map(lambda i: str(i.ref), o))
64
65 raise ValueError(f'Attempt to serialize a non-set: {o.__class__}')
66
67 @classmethod
68 def deserialize(cls, o: Any) -> Set['BookReference']:
69 print(f'Deserializing {o} ({type(o)})')
70 references: Set['BookReference'] = set()
71 if isinstance(o, list):
72 for v in o:
73 references.add(BookReference(ref=v))
74 return references
75
76 raise ValueError(f'Attempt to deserialize a non-set: {o.__class__}')
77
78
79class TitleMapper(BaseHelper):
80
81 @classmethod
82 def json_serialize(cls, o: str) -> str:
83 return f'{{J}} {o}'
84
85 @classmethod
86 def json_deserialize(cls, o: str) -> str:
87 return re.sub(r'^\{J} ', '', o)
88
89 @classmethod
90 def xml_serialize(cls, o: str) -> str:
91 return f'{{X}} {o}'
92
93 @classmethod
94 def xml_deserialize(cls, o: str) -> str:
95 return re.sub(r'^\{X} ', '', o)
96
97
98class BookEditionHelper(BaseHelper):
99
100 @classmethod
101 def serialize(cls, o: Any) -> Optional[int]:
102 return o \
103 if isinstance(o, int) and o > 0 \
104 else None
105
106 @classmethod
107 def deserialize(cls, o: Any) -> int:
108 try:
109 return int(o)
110 except Exception:
111 return 1
112
113
114@py_serializable.serializable_class
115class Chapter:
116
117 def __init__(self, *, number: int, title: str) -> None:
118 self._number = number
119 self._title = title
120
121 @property
122 def number(self) -> int:
123 return self._number
124
125 @property
126 @py_serializable.xml_string(XmlStringSerializationType.TOKEN)
127 def title(self) -> str:
128 return self._title
129
130 def __eq__(self, other: Any) -> bool:
131 if isinstance(other, Chapter):
132 return hash(other) == hash(self)
133 return False
134
135 def __hash__(self) -> int:
136 return hash((self.number, self.title))
137
138
139@py_serializable.serializable_class(
140 ignore_unknown_during_deserialization=True
141)
142class Publisher:
143
144 def __init__(self, *, name: str, address: Optional[str] = None, email: Optional[str] = None) -> None:
145 self._name = name
146 self._address = address
147 self._email = email
148
149 @property
150 def name(self) -> str:
151 return self._name
152
153 @property
154 @py_serializable.view(SchemaVersion2)
155 @py_serializable.view(SchemaVersion4)
156 def address(self) -> Optional[str]:
157 return self._address
158
159 @property
160 @py_serializable.include_none(SchemaVersion2)
161 @py_serializable.include_none(SchemaVersion3, 'RUBBISH')
162 def email(self) -> Optional[str]:
163 return self._email
164
165 def __eq__(self, other: object) -> bool:
166 if isinstance(other, Publisher):
167 return hash(other) == hash(self)
168 return False
169
170 def __hash__(self) -> int:
171 return hash((self.name, self.address, self.email))
172
173
174@unique
175class BookType(Enum):
176 FICTION = 'fiction'
177 NON_FICTION = 'non-fiction'
178
179
180@py_serializable.serializable_class(name='edition')
181class BookEdition:
182
183 def __init__(self, *, number: int, name: str) -> None:
184 self._number = number
185 self._name = name
186
187 @property
188 @py_serializable.xml_attribute()
189 @py_serializable.type_mapping(BookEditionHelper)
190 def number(self) -> int:
191 return self._number
192
193 @property
194 @py_serializable.xml_name('.')
195 def name(self) -> str:
196 return self._name
197
198 def __eq__(self, other: object) -> bool:
199 if isinstance(other, BookEdition):
200 return hash(other) == hash(self)
201 return False
202
203 def __hash__(self) -> int:
204 return hash((self.number, self.name))
205
206
207@py_serializable.serializable_class
208class BookReference:
209
210 def __init__(self, *, ref: str, references: Optional[Iterable['BookReference']] = None) -> None:
211 self.ref = ref
212 self.references = set(references or {})
213
214 @property
215 @py_serializable.json_name('reference')
216 @py_serializable.xml_attribute()
217 @py_serializable.xml_string(XmlStringSerializationType.TOKEN)
218 def ref(self) -> str:
219 return self._ref
220
221 @ref.setter
222 def ref(self, ref: str) -> None:
223 self._ref = ref
224
225 @property
226 @py_serializable.json_name('refersTo')
227 @py_serializable.type_mapping(ReferenceReferences)
228 @py_serializable.xml_array(py_serializable.XmlArraySerializationType.FLAT, 'reference')
229 def references(self) -> Set['BookReference']:
230 return self._references
231
232 @references.setter
233 def references(self, references: Iterable['BookReference']) -> None:
234 self._references = set(references)
235
236 def __eq__(self, other: object) -> bool:
237 if isinstance(other, BookReference):
238 return hash(other) == hash(self)
239 return False
240
241 def __hash__(self) -> int:
242 return hash((self.ref, tuple(self.references)))
243
244 def __repr__(self) -> str:
245 return f'<BookReference ref={self.ref}, targets={len(self.references)}>'
246
247
248@py_serializable.serializable_class
249class StockId(py_serializable.helpers.BaseHelper):
250
251 def __init__(self, id: str) -> None:
252 self._id = id
253
254 @property
255 @py_serializable.json_name('.')
256 @py_serializable.xml_name('.')
257 def id(self) -> str:
258 return self._id
259
260 @classmethod
261 def serialize(cls, o: Any) -> str:
262 if isinstance(o, StockId):
263 return str(o)
264 raise Exception(
265 f'Attempt to serialize a non-StockId: {o!r}')
266
267 @classmethod
268 def deserialize(cls, o: Any) -> 'StockId':
269 try:
270 return StockId(id=str(o))
271 except ValueError as err:
272 raise Exception(
273 f'StockId string supplied does not parse: {o!r}'
274 ) from err
275
276 def __eq__(self, other: Any) -> bool:
277 if isinstance(other, StockId):
278 return hash(other) == hash(self)
279 return False
280
281 def __lt__(self, other: Any) -> bool:
282 if isinstance(other, StockId):
283 return self._id < other._id
284 return NotImplemented
285
286 def __hash__(self) -> int:
287 return hash(self._id)
288
289 def __repr__(self) -> str:
290 return f'<StockId {self._id}>'
291
292 def __str__(self) -> str:
293 return self._id
294
295
296@py_serializable.serializable_class(
297 name='bigbook',
298 ignore_during_deserialization={
299 'something_to_be_ignored', 'ignore_me', 'ignored',
300 '$schema', '$comment',
301 },
302)
303class Book:
304
305 def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[str],
306 publisher: Optional[Publisher] = None, chapters: Optional[Iterable[Chapter]] = None,
307 edition: Optional[BookEdition] = None, type: BookType = BookType.FICTION,
308 id: Optional[UUID] = None, references: Optional[Iterable[BookReference]] = None,
309 rating: Optional[Decimal] = None, stock_ids: Optional[Iterable[StockId]] = None) -> None:
310 self._id = id or uuid4()
311 self._title = title
312 self._isbn = isbn
313 self._edition = edition
314 self._publish_date = publish_date
315 self._authors = set(authors)
316 self._publisher = publisher
317 self.chapters = list(chapters or [])
318 self._type = type
319 self.references = set(references or [])
320 self.rating = Decimal('NaN') if rating is None else rating
321 self._stock_ids = set(stock_ids or [])
322
323 @property
324 @py_serializable.xml_sequence(1)
325 def id(self) -> UUID:
326 return self._id
327
328 @property
329 @py_serializable.xml_sequence(2)
330 @py_serializable.type_mapping(TitleMapper)
331 @py_serializable.xml_string(XmlStringSerializationType.TOKEN)
332 def title(self) -> str:
333 return self._title
334
335 @property
336 @py_serializable.json_name('isbn_number')
337 @py_serializable.xml_attribute()
338 @py_serializable.xml_name('isbn_number')
339 def isbn(self) -> str:
340 return self._isbn
341
342 @property
343 @py_serializable.xml_sequence(3)
344 def edition(self) -> Optional[BookEdition]:
345 return self._edition
346
347 @property
348 @py_serializable.xml_sequence(4)
349 @py_serializable.type_mapping(Iso8601Date)
350 def publish_date(self) -> date:
351 return self._publish_date
352
353 @property
354 @py_serializable.xml_array(XmlArraySerializationType.FLAT, 'author')
355 @py_serializable.xml_string(XmlStringSerializationType.NORMALIZED_STRING)
356 @py_serializable.xml_sequence(5)
357 def authors(self) -> Set[str]:
358 return self._authors
359
360 @property
361 @py_serializable.xml_sequence(7)
362 def publisher(self) -> Optional[Publisher]:
363 return self._publisher
364
365 @property
366 @py_serializable.xml_array(XmlArraySerializationType.NESTED, 'chapter')
367 @py_serializable.xml_sequence(8)
368 def chapters(self) -> List[Chapter]:
369 return self._chapters
370
371 @chapters.setter
372 def chapters(self, chapters: Iterable[Chapter]) -> None:
373 self._chapters = list(chapters)
374
375 @property
376 @py_serializable.xml_sequence(6)
377 def type(self) -> BookType:
378 return self._type
379
380 @property
381 @py_serializable.view(SchemaVersion4)
382 @py_serializable.xml_array(py_serializable.XmlArraySerializationType.NESTED, 'reference')
383 @py_serializable.xml_sequence(7)
384 def references(self) -> Set[BookReference]:
385 return self._references
386
387 @references.setter
388 def references(self, references: Iterable[BookReference]) -> None:
389 self._references = set(references)
390
391 @property
392 @py_serializable.xml_sequence(20)
393 def rating(self) -> Decimal:
394 return self._rating
395
396 @rating.setter
397 def rating(self, rating: Decimal) -> None:
398 self._rating = rating
399
400 @property
401 @py_serializable.view(SchemaVersion4)
402 @py_serializable.xml_array(XmlArraySerializationType.FLAT, 'stockId')
403 @py_serializable.xml_sequence(21)
404 def stock_ids(self) -> Set[StockId]:
405 return self._stock_ids
406
407
408# region ThePhoenixProject_v2
409
410
411ThePhoenixProject_v1 = Book(
412 title='The Phoenix Project',
413 isbn='978-1942788294',
414 publish_date=date(year=2018, month=4, day=16),
415 authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
416 publisher=Publisher(name='IT Revolution Press LLC'),
417 edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
418 id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
419 rating=Decimal('9.8')
420)
421
422ThePhoenixProject_v1.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
423ThePhoenixProject_v1.chapters.append(Chapter(number=2, title='Tuesday, September 2'))
424ThePhoenixProject_v1.chapters.append(Chapter(number=3, title='Tuesday, September 2'))
425ThePhoenixProject_v1.chapters.append(Chapter(number=4, title='Wednesday, September 3'))
426
427# endregion ThePhoenixProject_v2
428
429# region ThePhoenixProject_v2
430
431ThePhoenixProject_v2 = Book(
432 title='The Phoenix Project',
433 isbn='978-1942788294',
434 publish_date=date(year=2018, month=4, day=16),
435 authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
436 publisher=Publisher(name='IT Revolution Press LLC', address='10 Downing Street'),
437 edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
438 id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
439 rating=Decimal('9.8'),
440 stock_ids=[StockId('stock-id-1'), StockId('stock-id-2')]
441)
442
443ThePhoenixProject_v2.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
444ThePhoenixProject_v2.chapters.append(Chapter(number=2, title='Tuesday, September 2'))
445ThePhoenixProject_v2.chapters.append(Chapter(number=3, title='Tuesday, September 2'))
446ThePhoenixProject_v2.chapters.append(Chapter(number=4, title='Wednesday, September 3'))
447
448SubRef1 = BookReference(ref='sub-ref-1')
449SubRef2 = BookReference(ref='sub-ref-2')
450SubRef3 = BookReference(ref='sub-ref-3')
451
452Ref1 = BookReference(ref='my-ref-1')
453Ref2 = BookReference(ref='my-ref-2', references=[SubRef1, SubRef3])
454Ref3 = BookReference(ref='my-ref-3', references=[SubRef2])
455
456ThePhoenixProject_v2.references = {Ref3, Ref2, Ref1}
457
458# endregion ThePhoenixProject_v2
459
460ThePhoenixProject = ThePhoenixProject_v2
461
462# region ThePhoenixProject_unnormalized
463
464# a case where the `normalizedString` and `token` transformation must come into play
465ThePhoenixProject_unnormalized = Book(
466 title='The \n Phoenix Project ',
467 isbn='978-1942788294',
468 publish_date=date(year=2018, month=4, day=16),
469 authors=['Gene Kim', 'Kevin\r\nBehr', 'George\tSpafford'],
470 publisher=Publisher(name='IT Revolution Press LLC', address='10 Downing Street'),
471 edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
472 id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
473 rating=Decimal('9.8'),
474 stock_ids=[StockId('stock-id-1'), StockId('stock-id-2')]
475)
476
477ThePhoenixProject_unnormalized.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
478ThePhoenixProject_unnormalized.chapters.append(Chapter(number=2, title='Tuesday,\tSeptember 2'))
479ThePhoenixProject_unnormalized.chapters.append(Chapter(number=3, title='Tuesday,\r\nSeptember 2'))
480ThePhoenixProject_unnormalized.chapters.append(Chapter(number=4, title='Wednesday,\rSeptember\n3'))
481
482SubRef1 = BookReference(ref=' sub-ref-1 ')
483SubRef2 = BookReference(ref='\rsub-ref-2\t')
484SubRef3 = BookReference(ref='\nsub-ref-3\r\n')
485
486Ref1 = BookReference(ref='\r\nmy-ref-1')
487Ref2 = BookReference(ref='\tmy-ref-2', references=[SubRef1, SubRef3])
488Ref3 = BookReference(ref=' my-ref-3\n', references=[SubRef2])
489
490ThePhoenixProject_unnormalized.references = {Ref3, Ref2, Ref1}
491
492# endregion ThePhoenixProject_unnormalized
493
494# region ThePhoenixProject_attr_serialized_none
495
496# a case where an attribute is serialized to `None` and deserialized from it
497ThePhoenixProject_attr_serialized_none = Book(
498 title='The Phoenix Project',
499 isbn='978-1942788294',
500 publish_date=date(year=2018, month=4, day=16),
501 authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
502 publisher=Publisher(name='IT Revolution Press LLC'),
503 edition=BookEdition(number=0, name='Preview Edition'),
504 id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
505 rating=Decimal('9.8')
506)
507
508# endregion ThePhoenixProject_attr_serialized_none
509
510if __name__ == '__main__':
511 tpp_as_xml = ThePhoenixProject.as_xml() # type:ignore[attr-defined]
512 tpp_as_json = ThePhoenixProject.as_json() # type:ignore[attr-defined]
513 print(tpp_as_xml, tpp_as_json, sep='\n\n')
514
515 import io
516 import json
517
518 tpp_from_xml = ThePhoenixProject.from_xml( # type:ignore[attr-defined]
519 io.StringIO(tpp_as_xml))
520 tpp_from_json = ThePhoenixProject.from_json( # type:ignore[attr-defined]
521 json.loads(tpp_as_json))
Logging and log access
This library utilizes an own instance of Logger, which you may access and add handlers to.
Example: send all logs messages to stdErr
import sys
import logging
import py_serializable
my_log_handler = logging.StreamHandler(sys.stderr)
my_log_handler.setLevel(logging.DEBUG)
my_log_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
py_serializable.logger.addHandler(my_log_handler)
py_serializable.logger.setLevel(my_log_handler.level)
py_serializable.logger.propagate = False
@py_serializable.serializable_class
class Chapter:
def __init__(self, *, number: int, title: str) -> None:
self._number = number
self._title = title
@property
def number(self) -> int:
return self._number
@property
def title(self) -> str:
return self._title
moby_dick_c1 = Chapter(number=1, title='Loomings')
print(moby_dick_c1.as_json())