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
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 def title(self) -> str:
113 return self._title
114
115 def __eq__(self, other: Any) -> bool:
116 if isinstance(other, Chapter):
117 return hash(other) == hash(self)
118 return False
119
120 def __hash__(self) -> int:
121 return hash((self.number, self.title))
122
123
124@serializable.serializable_class
125class Publisher:
126
127 def __init__(self, *, name: str, address: Optional[str] = None, email: Optional[str] = None) -> None:
128 self._name = name
129 self._address = address
130 self._email = email
131
132 @property
133 def name(self) -> str:
134 return self._name
135
136 @property
137 @serializable.view(SchemaVersion2)
138 @serializable.view(SchemaVersion4)
139 def address(self) -> Optional[str]:
140 return self._address
141
142 @property
143 @serializable.include_none(SchemaVersion2)
144 @serializable.include_none(SchemaVersion3, 'RUBBISH')
145 def email(self) -> Optional[str]:
146 return self._email
147
148 def __eq__(self, other: object) -> bool:
149 if isinstance(other, Publisher):
150 return hash(other) == hash(self)
151 return False
152
153 def __hash__(self) -> int:
154 return hash((self.name, self.address, self.email))
155
156
157@unique
158class BookType(Enum):
159 FICTION = 'fiction'
160 NON_FICTION = 'non-fiction'
161
162
163@serializable.serializable_class(name='edition')
164class BookEdition:
165
166 def __init__(self, *, number: int, name: str) -> None:
167 self._number = number
168 self._name = name
169
170 @property
171 @serializable.xml_attribute()
172 def number(self) -> int:
173 return self._number
174
175 @property
176 @serializable.xml_name('.')
177 def name(self) -> str:
178 return self._name
179
180 def __eq__(self, other: object) -> bool:
181 if isinstance(other, BookEdition):
182 return hash(other) == hash(self)
183 return False
184
185 def __hash__(self) -> int:
186 return hash((self.number, self.name))
187
188
189@serializable.serializable_class
190class BookReference:
191
192 def __init__(self, *, ref: str, references: Optional[Iterable['BookReference']] = None) -> None:
193 self.ref = ref
194 self.references = set(references or {})
195
196 @property
197 @serializable.json_name('reference')
198 @serializable.xml_attribute()
199 def ref(self) -> str:
200 return self._ref
201
202 @ref.setter
203 def ref(self, ref: str) -> None:
204 self._ref = ref
205
206 @property
207 @serializable.json_name('refersTo')
208 @serializable.type_mapping(ReferenceReferences)
209 @serializable.xml_array(serializable.XmlArraySerializationType.FLAT, 'reference')
210 def references(self) -> Set['BookReference']:
211 return self._references
212
213 @references.setter
214 def references(self, references: Iterable['BookReference']) -> None:
215 self._references = set(references)
216
217 def __eq__(self, other: object) -> bool:
218 if isinstance(other, BookReference):
219 return hash(other) == hash(self)
220 return False
221
222 def __hash__(self) -> int:
223 return hash((self.ref, tuple(self.references)))
224
225 def __repr__(self) -> str:
226 return f'<BookReference ref={self.ref}, targets={len(self.references)}>'
227
228
229@serializable.serializable_class
230class StockId(serializable.helpers.BaseHelper):
231
232 def __init__(self, id: str) -> None:
233 self._id = id
234
235 @property
236 @serializable.json_name('.')
237 @serializable.xml_name('.')
238 def id(self) -> str:
239 return self._id
240
241 @classmethod
242 def serialize(cls, o: Any) -> str:
243 if isinstance(o, StockId):
244 return str(o)
245 raise Exception(
246 f'Attempt to serialize a non-StockId: {o!r}')
247
248 @classmethod
249 def deserialize(cls, o: Any) -> 'StockId':
250 try:
251 return StockId(id=str(o))
252 except ValueError as err:
253 raise Exception(
254 f'StockId string supplied does not parse: {o!r}'
255 ) from err
256
257 def __eq__(self, other: Any) -> bool:
258 if isinstance(other, StockId):
259 return hash(other) == hash(self)
260 return False
261
262 def __lt__(self, other: Any) -> bool:
263 if isinstance(other, StockId):
264 return self._id < other._id
265 return NotImplemented
266
267 def __hash__(self) -> int:
268 return hash(self._id)
269
270 def __repr__(self) -> str:
271 return f'<StockId {self._id}>'
272
273 def __str__(self) -> str:
274 return self._id
275
276
277@serializable.serializable_class(name='bigbook',
278 ignore_during_deserialization=['something_to_be_ignored', 'ignore_me', 'ignored'])
279class Book:
280
281 def __init__(self, title: str, isbn: str, publish_date: date, authors: Iterable[str],
282 publisher: Optional[Publisher] = None, chapters: Optional[Iterable[Chapter]] = None,
283 edition: Optional[BookEdition] = None, type: BookType = BookType.FICTION,
284 id: Optional[UUID] = None, references: Optional[Iterable[BookReference]] = None,
285 rating: Optional[Decimal] = None, stock_ids: Optional[Iterable[StockId]] = None) -> None:
286 self._id = id or uuid4()
287 self._title = title
288 self._isbn = isbn
289 self._edition = edition
290 self._publish_date = publish_date
291 self._authors = set(authors)
292 self._publisher = publisher
293 self.chapters = list(chapters or [])
294 self._type = type
295 self.references = set(references or [])
296 self.rating = Decimal('NaN') if rating is None else rating
297 self._stock_ids = set(stock_ids or [])
298
299 @property
300 @serializable.xml_sequence(1)
301 def id(self) -> UUID:
302 return self._id
303
304 @property
305 @serializable.xml_sequence(2)
306 @serializable.type_mapping(TitleMapper)
307 def title(self) -> str:
308 return self._title
309
310 @property
311 @serializable.json_name('isbn_number')
312 @serializable.xml_attribute()
313 @serializable.xml_name('isbn_number')
314 def isbn(self) -> str:
315 return self._isbn
316
317 @property
318 @serializable.xml_sequence(3)
319 def edition(self) -> Optional[BookEdition]:
320 return self._edition
321
322 @property
323 @serializable.xml_sequence(4)
324 @serializable.type_mapping(Iso8601Date)
325 def publish_date(self) -> date:
326 return self._publish_date
327
328 @property
329 @serializable.xml_array(XmlArraySerializationType.FLAT, 'author')
330 @serializable.xml_sequence(5)
331 def authors(self) -> Set[str]:
332 return self._authors
333
334 @property
335 @serializable.xml_sequence(7)
336 def publisher(self) -> Optional[Publisher]:
337 return self._publisher
338
339 @property
340 @serializable.xml_array(XmlArraySerializationType.NESTED, 'chapter')
341 @serializable.xml_sequence(8)
342 def chapters(self) -> List[Chapter]:
343 return self._chapters
344
345 @chapters.setter
346 def chapters(self, chapters: Iterable[Chapter]) -> None:
347 self._chapters = list(chapters)
348
349 @property
350 @serializable.xml_sequence(6)
351 def type(self) -> BookType:
352 return self._type
353
354 @property
355 @serializable.view(SchemaVersion4)
356 @serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'reference')
357 @serializable.xml_sequence(7)
358 def references(self) -> Set[BookReference]:
359 return self._references
360
361 @references.setter
362 def references(self, references: Iterable[BookReference]) -> None:
363 self._references = set(references)
364
365 @property
366 @serializable.xml_sequence(20)
367 def rating(self) -> Decimal:
368 return self._rating
369
370 @rating.setter
371 def rating(self, rating: Decimal) -> None:
372 self._rating = rating
373
374 @property
375 @serializable.view(SchemaVersion4)
376 @serializable.xml_array(XmlArraySerializationType.FLAT, 'stockId')
377 @serializable.xml_sequence(21)
378 def stock_ids(self) -> Set[StockId]:
379 return self._stock_ids
380
381
382ThePhoenixProject_v1 = Book(
383 title='The Phoenix Project', isbn='978-1942788294', publish_date=date(year=2018, month=4, day=16),
384 authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
385 publisher=Publisher(name='IT Revolution Press LLC'),
386 edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
387 id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
388 rating=Decimal('9.8')
389)
390
391ThePhoenixProject_v1.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
392ThePhoenixProject_v1.chapters.append(Chapter(number=2, title='Tuesday, September 2'))
393ThePhoenixProject_v1.chapters.append(Chapter(number=3, title='Tuesday, September 2'))
394ThePhoenixProject_v1.chapters.append(Chapter(number=4, title='Wednesday, September 3'))
395
396ThePhoenixProject_v2 = Book(
397 title='The Phoenix Project', isbn='978-1942788294', publish_date=date(year=2018, month=4, day=16),
398 authors=['Gene Kim', 'Kevin Behr', 'George Spafford'],
399 publisher=Publisher(name='IT Revolution Press LLC', address='10 Downing Street'),
400 edition=BookEdition(number=5, name='5th Anniversary Limited Edition'),
401 id=UUID('f3758bf0-0ff7-4366-a5e5-c209d4352b2d'),
402 rating=Decimal('9.8'),
403 stock_ids=[StockId('stock-id-1'), StockId('stock-id-2')]
404)
405
406ThePhoenixProject_v2.chapters.append(Chapter(number=1, title='Tuesday, September 2'))
407ThePhoenixProject_v2.chapters.append(Chapter(number=2, title='Tuesday, September 2'))
408ThePhoenixProject_v2.chapters.append(Chapter(number=3, title='Tuesday, September 2'))
409ThePhoenixProject_v2.chapters.append(Chapter(number=4, title='Wednesday, September 3'))
410
411SubRef1 = BookReference(ref='sub-ref-1')
412SubRef2 = BookReference(ref='sub-ref-2')
413SubRef3 = BookReference(ref='sub-ref-3')
414
415Ref1 = BookReference(ref='my-ref-1')
416Ref2 = BookReference(ref='my-ref-2', references=[SubRef1, SubRef3])
417Ref3 = BookReference(ref='my-ref-3', references=[SubRef2])
418
419ThePhoenixProject_v2.references = {Ref3, Ref2, Ref1}
420
421ThePhoenixProject = ThePhoenixProject_v2
422
423if __name__ == '__main__':
424 tpp_as_xml = ThePhoenixProject.as_xml() # type:ignore[attr-defined]
425 tpp_as_json = ThePhoenixProject.as_json() # type:ignore[attr-defined]
426 print(tpp_as_xml, tpp_as_json, sep='\n\n')
427
428 import io
429 import json
430
431 tpp_from_xml = ThePhoenixProject.from_xml( # type:ignore[attr-defined]
432 io.StringIO(tpp_as_xml))
433 tpp_from_json = ThePhoenixProject.from_json( # type:ignore[attr-defined]
434 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.
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())