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