Coretex
codable.py
1 # Copyright (C) 2023 Coretex LLC
2 
3 # This file is part of Coretex.ai
4 
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU Affero General Public License for more details.
14 
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <https://www.gnu.org/licenses/>.
17 
18 from typing import Any, Optional, Type, Dict, Tuple
19 from typing_extensions import Self
20 from datetime import datetime
21 from enum import Enum
22 from uuid import UUID
23 
24 import inflection
25 
26 from .descriptor import KeyDescriptor
27 from ..utils.date import DATE_FORMAT, decodeDate
28 
29 
30 class Codable:
31 
32  """
33  Class whose subclasses can be serialized/deserialized into/from a JSON format object
34  """
35 
36  @classmethod
37  def _keyDescriptors(cls) -> Dict[str, KeyDescriptor]:
38  """
39  Defines a custom mapping for python objects to json field
40  Key of the dictionary represents a name of the field in python
41  Value of the dictionary represents how should the object be serialized/deserialized
42 
43  Returns
44  -------
45  Dict[str, KeyDescriptor] -> Dictionary of objects describing translation
46  """
47 
48  return {}
49 
50  @classmethod
51  def __keyDescriptorByJsonName(cls, jsonName: str) -> Tuple[Optional[str], Optional[KeyDescriptor]]:
52  for key, value in cls._keyDescriptors_keyDescriptors().items():
53  if value.jsonName == jsonName:
54  return key, value
55 
56  return None, None
57 
58  @classmethod
59  def __keyDescriptorByPythonName(cls, pythonName: str) -> Optional[KeyDescriptor]:
60  if not pythonName in cls._keyDescriptors_keyDescriptors().keys():
61  return None
62 
63  return cls._keyDescriptors_keyDescriptors()[pythonName]
64 
65  # - Encoding
66 
67  def __encodeKey(self, key: str) -> str:
68  descriptor = self.__class__.__keyDescriptorByPythonName(key)
69 
70  if descriptor is None or descriptor.jsonName is None:
71  return inflection.underscore(key)
72 
73  return descriptor.jsonName
74 
75  def _encodeValue(self, key: str, value: Any) -> Any:
76  """
77  Encodes python value into a json property
78  Custom field serialization can be implemented by overriding this method
79 
80  Parameters
81  ----------
82  key : str
83  python object variable name
84  value : Any
85  json object represented as an object from standard python library
86 
87  Returns
88  -------
89  Any -> encoded value of the object
90  """
91 
92  descriptor = self.__class__.__keyDescriptorByPythonName(key)
93 
94  if descriptor is None or descriptor.pythonType is None:
95  return value
96 
97  if issubclass(descriptor.pythonType, Enum):
98  if descriptor.isList():
99  return [element.value for element in value]
100 
101  return value.value
102 
103  if issubclass(descriptor.pythonType, UUID):
104  if descriptor.isList():
105  return [str(element) for element in value]
106 
107  return str(value)
108 
109  if issubclass(descriptor.pythonType, Codable):
110  if descriptor.isList():
111  return [descriptor.pythonType.encode(element) for element in value]
112 
113  return descriptor.pythonType.encode(value)
114 
115  if issubclass(descriptor.pythonType, datetime):
116  if descriptor.isList():
117  return [element.strftime(DATE_FORMAT) for element in value]
118 
119  return value.strftime(DATE_FORMAT)
120 
121  return value
122 
123  def encode(self) -> Dict[str, Any]:
124  """
125  Encodes python object into dictionary which contains
126  only values representable by standard python library/types
127 
128  Returns
129  -------
130  Dict[str, Any] -> encoded object which can be serialized into json string
131  """
132 
133  encodedObject: Dict[str, Any] = {}
134 
135  for key, value in self.__dict__.items():
136  descriptor = self.__class__.__keyDescriptorByPythonName(key)
137 
138  # skip ignored fields for encoding
139  if descriptor is not None and not descriptor.isEncodable:
140  # print(f">> [Coretex] Skipping encoding for field: {key}")
141  continue
142 
143  encodedKey = self.__encodeKey__encodeKey(key)
144  encodedValue = self._encodeValue_encodeValue(key, value)
145 
146  encodedObject[encodedKey] = encodedValue
147 
148  return encodedObject
149 
150  # - Decoding
151 
152  @classmethod
153  def __decodeKey(cls, key: str) -> str:
154  descriptorKey, _ = cls.__keyDescriptorByJsonName__keyDescriptorByJsonName(key)
155 
156  if descriptorKey is None:
157  return inflection.camelize(key, False)
158 
159  return descriptorKey
160 
161  @classmethod
162  def _decodeValue(cls, key: str, value: Any) -> Any:
163  """
164  Decodes a value of a single json field
165  Custom logic can be implemented by overriding this method
166 
167  Parameters
168  ----------
169  key : str
170  name of the json field
171  value : Any
172  value of the json field
173 
174  Returns
175  -------
176  Any -> decoded value of the json field
177  """
178 
179  _, descriptor = cls.__keyDescriptorByJsonName__keyDescriptorByJsonName(key)
180 
181  if descriptor is None or descriptor.pythonType is None:
182  return value
183 
184  if issubclass(descriptor.pythonType, Enum):
185  if descriptor.isList() and descriptor.collectionType is not None:
186  return descriptor.collectionType([descriptor.pythonType(element) for element in value])
187 
188  return descriptor.pythonType(value)
189 
190  if issubclass(descriptor.pythonType, UUID):
191  if descriptor.isList() and descriptor.collectionType is not None:
192  return descriptor.collectionType([descriptor.pythonType(element) for element in value])
193 
194  return descriptor.pythonType(value)
195 
196  if issubclass(descriptor.pythonType, Codable):
197  if descriptor.isList() and descriptor.collectionType is not None:
198  return descriptor.collectionType([descriptor.pythonType.decode(element) for element in value])
199 
200  return descriptor.pythonType.decode(value)
201 
202  if issubclass(descriptor.pythonType, datetime):
203  if descriptor.isList() and descriptor.collectionType is not None:
204  return descriptor.collectionType([decodeDate(element) for element in value])
205 
206  return decodeDate(value)
207 
208  return value
209 
210  def _updateFields(self, encodedObject: Dict[str, Any]) -> None:
211  """
212  Updates the properties of object with new values
213 
214  Parameters
215  ----------
216  encodedObject : Dict[str, Any]
217  json encoded object
218  """
219 
220  for key, value in encodedObject.items():
221  _, descriptor = self.__class__.__keyDescriptorByJsonName(key)
222 
223  # skip ignored fields for deserialization
224  if descriptor is not None and not descriptor.isDecodable:
225  # print(f">> [Coretex] Skipping decoding for field: {key}")
226  continue
227 
228  decodedKey = self.__decodeKey__decodeKey(key)
229  self.__dict__[decodedKey] = self._decodeValue_decodeValue(key, value)
230 
231  def onDecode(self) -> None:
232  """
233  Callback which is called once the object has been decoded
234  """
235 
236  pass
237 
238  @classmethod
239  def decode(cls, encodedObject: Dict[str, Any]) -> Self:
240  """
241  Decodes the json object into a python object
242 
243  Parameters
244  ----------
245  encodedObject : Dict[str, Any]
246  json encoded object
247 
248  Returns
249  -------
250  Self -> Decoded python object
251  """
252  obj = cls()
253 
254  obj._updateFields(encodedObject)
255  obj.onDecode()
256 
257  return obj
Dict[str, KeyDescriptor] _keyDescriptors(cls)
Definition: codable.py:37
Dict[str, Any] encode(self)
Definition: codable.py:123
Any _encodeValue(self, str key, Any value)
Definition: codable.py:75
str __decodeKey(cls, str key)
Definition: codable.py:153
Self decode(cls, Dict[str, Any] encodedObject)
Definition: codable.py:239
Tuple[Optional[str], Optional[KeyDescriptor]] __keyDescriptorByJsonName(cls, str jsonName)
Definition: codable.py:51
Any _decodeValue(cls, str key, Any value)
Definition: codable.py:162
str __encodeKey(self, str key)
Definition: codable.py:67