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())