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