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
100class BookEditionHelper(BaseHelper):
101
102    @classmethod
103    def serialize(cls, o: Any) -> Optional[int]:
104        return o \
105            if isinstance(o, int) and o > 0 \
106            else None
107
108    @classmethod
109    def deserialize(cls, o: Any) -> int:
110        try:
111            return int(o)
112        except Exception:
113            return 1
114
115
116@serializable.serializable_class
117class Chapter:
118
119    def __init__(self, *, number: int, title: str) -> None:
120        self._number = number
121        self._title = title
122
123    @property
124    def number(self) -> int:
125        return self._number
126
127    @property
128    @serializable.xml_string(XmlStringSerializationType.TOKEN)
129    def title(self) -> str:
130        return self._title
131
132    def __eq__(self, other: Any) -> bool:
133        if isinstance(other, Chapter):
134            return hash(other) == hash(self)
135        return False
136
137    def __hash__(self) -> int:
138        return hash((self.number, self.title))
139
140
141@serializable.serializable_class
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    @serializable.view(SchemaVersion2)
155    @serializable.view(SchemaVersion4)
156    def address(self) -> Optional[str]:
157        return self._address
158
159    @property
160    @serializable.include_none(SchemaVersion2)
161    @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@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    @serializable.xml_attribute()
189    @serializable.type_mapping(BookEditionHelper)
190    def number(self) -> int:
191        return self._number
192
193    @property
194    @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@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    @serializable.json_name('reference')
216    @serializable.xml_attribute()
217    @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    @serializable.json_name('refersTo')
227    @serializable.type_mapping(ReferenceReferences)
228    @serializable.xml_array(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@serializable.serializable_class
249class StockId(serializable.helpers.BaseHelper):
250
251    def __init__(self, id: str) -> None:
252        self._id = id
253
254    @property
255    @serializable.json_name('.')
256    @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@serializable.serializable_class(name='bigbook',
297                                 ignore_during_deserialization=['something_to_be_ignored', 'ignore_me', 'ignored'])
298class Book:
299
300    def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[str],
301                 publisher: Optional[Publisher] = None, chapters: Optional[Iterable[Chapter]] = None,
302                 edition: Optional[BookEdition] = None, type: BookType = BookType.FICTION,
303                 id: Optional[UUID] = None, references: Optional[Iterable[BookReference]] = None,
304                 rating: Optional[Decimal] = None, stock_ids: Optional[Iterable[StockId]] = None) -> None:
305        self._id = id or uuid4()
306        self._title = title
307        self._isbn = isbn
308        self._edition = edition
309        self._publish_date = publish_date
310        self._authors = set(authors)
311        self._publisher = publisher
312        self.chapters = list(chapters or [])
313        self._type = type
314        self.references = set(references or [])
315        self.rating = Decimal('NaN') if rating is None else rating
316        self._stock_ids = set(stock_ids or [])
317
318    @property
319    @serializable.xml_sequence(1)
320    def id(self) -> UUID:
321        return self._id
322
323    @property
324    @serializable.xml_sequence(2)
325    @serializable.type_mapping(TitleMapper)
326    @serializable.xml_string(XmlStringSerializationType.TOKEN)
327    def title(self) -> str:
328        return self._title
329
330    @property
331    @serializable.json_name('isbn_number')
332    @serializable.xml_attribute()
333    @serializable.xml_name('isbn_number')
334    def isbn(self) -> str:
335        return self._isbn
336
337    @property
338    @serializable.xml_sequence(3)
339    def edition(self) -> Optional[BookEdition]:
340        return self._edition
341
342    @property
343    @serializable.xml_sequence(4)
344    @serializable.type_mapping(Iso8601Date)
345    def publish_date(self) -> date:
346        return self._publish_date
347
348    @property
349    @serializable.xml_array(XmlArraySerializationType.FLAT, 'author')
350    @serializable.xml_string(XmlStringSerializationType.NORMALIZED_STRING)
351    @serializable.xml_sequence(5)
352    def authors(self) -> Set[str]:
353        return self._authors
354
355    @property
356    @serializable.xml_sequence(7)
357    def publisher(self) -> Optional[Publisher]:
358        return self._publisher
359
360    @property
361    @serializable.xml_array(XmlArraySerializationType.NESTED, 'chapter')
362    @serializable.xml_sequence(8)
363    def chapters(self) -> List[Chapter]:
364        return self._chapters
365
366    @chapters.setter
367    def chapters(self, chapters: Iterable[Chapter]) -> None:
368        self._chapters = list(chapters)
369
370    @property
371    @serializable.xml_sequence(6)
372    def type(self) -> BookType:
373        return self._type
374
375    @property
376    @serializable.view(SchemaVersion4)
377    @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
378    @serializable.xml_sequence(7)
379    def references(self) -> Set[BookReference]:
380        return self._references
381
382    @references.setter
383    def references(self, references: Iterable[BookReference]) -> None:
384        self._references = set(references)
385
386    @property
387    @serializable.xml_sequence(20)
388    def rating(self) -> Decimal:
389        return self._rating
390
391    @rating.setter
392    def rating(self, rating: Decimal) -> None:
393        self._rating = rating
394
395    @property
396    @serializable.view(SchemaVersion4)
397    @serializable.xml_array(XmlArraySerializationType.FLAT, 'stockId')
398    @serializable.xml_sequence(21)
399    def stock_ids(self) -> Set[StockId]:
400        return self._stock_ids
401
402
403# region ThePhoenixProject_v2
404
405
406ThePhoenixProject_v1 = Book(
407    title='The Phoenix Project',
408    isbn='978-1942788294',
409    publish_date=date(year=2018, month=4, day=16),
410    authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
411    publisher=Publisher(name='IT Revolution Press LLC'),
412    edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
413    id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
414    rating=Decimal('9.8')
415)
416
417ThePhoenixProject_v1.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
418ThePhoenixProject_v1.chapters.append(Chapter(number=2, title='Tuesday, September 2'))
419ThePhoenixProject_v1.chapters.append(Chapter(number=3, title='Tuesday, September 2'))
420ThePhoenixProject_v1.chapters.append(Chapter(number=4, title='Wednesday, September 3'))
421
422# endregion ThePhoenixProject_v2
423
424# region ThePhoenixProject_v2
425
426ThePhoenixProject_v2 = Book(
427    title='The Phoenix Project',
428    isbn='978-1942788294',
429    publish_date=date(year=2018, month=4, day=16),
430    authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
431    publisher=Publisher(name='IT Revolution Press LLC', address='10 Downing Street'),
432    edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
433    id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
434    rating=Decimal('9.8'),
435    stock_ids=[StockId('stock-id-1'), StockId('stock-id-2')]
436)
437
438ThePhoenixProject_v2.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
439ThePhoenixProject_v2.chapters.append(Chapter(number=2, title='Tuesday, September 2'))
440ThePhoenixProject_v2.chapters.append(Chapter(number=3, title='Tuesday, September 2'))
441ThePhoenixProject_v2.chapters.append(Chapter(number=4, title='Wednesday, September 3'))
442
443SubRef1 = BookReference(ref='sub-ref-1')
444SubRef2 = BookReference(ref='sub-ref-2')
445SubRef3 = BookReference(ref='sub-ref-3')
446
447Ref1 = BookReference(ref='my-ref-1')
448Ref2 = BookReference(ref='my-ref-2', references=[SubRef1, SubRef3])
449Ref3 = BookReference(ref='my-ref-3', references=[SubRef2])
450
451ThePhoenixProject_v2.references = {Ref3, Ref2, Ref1}
452
453# endregion ThePhoenixProject_v2
454
455ThePhoenixProject = ThePhoenixProject_v2
456
457# region ThePhoenixProject_unnormalized
458
459# a case where the `normalizedString` and `token` transformation must come into play
460ThePhoenixProject_unnormalized = Book(
461    title='The \n Phoenix Project  ',
462    isbn='978-1942788294',
463    publish_date=date(year=2018, month=4, day=16),
464    authors=['Gene Kim', 'Kevin\r\nBehr', 'George\tSpafford'],
465    publisher=Publisher(name='IT Revolution Press LLC', address='10 Downing Street'),
466    edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
467    id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
468    rating=Decimal('9.8'),
469    stock_ids=[StockId('stock-id-1'), StockId('stock-id-2')]
470)
471
472ThePhoenixProject_unnormalized.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
473ThePhoenixProject_unnormalized.chapters.append(Chapter(number=2, title='Tuesday,\tSeptember 2'))
474ThePhoenixProject_unnormalized.chapters.append(Chapter(number=3, title='Tuesday,\r\nSeptember 2'))
475ThePhoenixProject_unnormalized.chapters.append(Chapter(number=4, title='Wednesday,\rSeptember\n3'))
476
477SubRef1 = BookReference(ref='  sub-ref-1  ')
478SubRef2 = BookReference(ref='\rsub-ref-2\t')
479SubRef3 = BookReference(ref='\nsub-ref-3\r\n')
480
481Ref1 = BookReference(ref='\r\nmy-ref-1')
482Ref2 = BookReference(ref='\tmy-ref-2', references=[SubRef1, SubRef3])
483Ref3 = BookReference(ref='   my-ref-3\n', references=[SubRef2])
484
485ThePhoenixProject_unnormalized.references = {Ref3, Ref2, Ref1}
486
487# endregion ThePhoenixProject_unnormalized
488
489# region ThePhoenixProject_attr_serialized_none
490
491# a case where an attribute is serialized to `None` and deserialized from it
492ThePhoenixProject_attr_serialized_none = Book(
493    title='The Phoenix Project',
494    isbn='978-1942788294',
495    publish_date=date(year=2018, month=4, day=16),
496    authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
497    publisher=Publisher(name='IT Revolution Press LLC'),
498    edition=BookEdition(number=0, name='Preview Edition'),
499    id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
500    rating=Decimal('9.8')
501)
502
503# endregion ThePhoenixProject_attr_serialized_none
504
505if __name__ == '__main__':
506    tpp_as_xml = ThePhoenixProject.as_xml()  # type:ignore[attr-defined]
507    tpp_as_json = ThePhoenixProject.as_json()  # type:ignore[attr-defined]
508    print(tpp_as_xml, tpp_as_json, sep='\n\n')
509
510    import io
511    import json
512
513    tpp_from_xml = ThePhoenixProject.from_xml(  # type:ignore[attr-defined]
514        io.StringIO(tpp_as_xml))
515    tpp_from_json = ThePhoenixProject.from_json(  # type:ignore[attr-defined]
516        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())