pumpwood_communication.serializers
Miscellaneous to help with serializers in communication.
1"""Miscellaneous to help with serializers in communication.""" 2import base64 3import simplejson as json 4import orjson 5import numpy as np 6import pandas as pd 7from typing import List, Union, Dict, Any 8from simplejson import JSONEncoder 9from datetime import datetime 10from datetime import date 11from datetime import time 12from pandas import Timestamp 13from shapely.geometry.base import BaseGeometry 14from shapely.geometry import mapping 15from sqlalchemy_utils.types.choice import Choice 16from pumpwood_communication.exceptions import ( 17 PumpWoodException, PumpWoodNotImplementedError) 18 19 20def default_encoder(obj): 21 """Serialize complex objects.""" 22 # Return None if object is NaN 23 if isinstance(obj, datetime): 24 return obj.isoformat() 25 if isinstance(obj, Timestamp): 26 return obj.isoformat() 27 if isinstance(obj, date): 28 return obj.isoformat() 29 if isinstance(obj, time): 30 return obj.isoformat() 31 if isinstance(obj, np.ndarray): 32 return obj.tolist() 33 if isinstance(obj, pd.DataFrame): 34 return obj.to_dict('records') 35 if isinstance(obj, pd.Series): 36 obj.dtype == Timestamp 37 return obj.tolist() 38 if isinstance(obj, np.generic): 39 return obj.item() 40 if isinstance(obj, BaseGeometry): 41 if obj.is_empty: 42 return None 43 else: 44 return mapping(obj) 45 if isinstance(obj, BaseGeometry): 46 return mapping(obj) 47 if isinstance(obj, Choice): 48 return obj.code 49 if isinstance(obj, set): 50 return list(obj) 51 else: 52 raise TypeError( 53 "Unserializable object {} of type {}".format(obj, type(obj))) 54 55 56class PumpWoodJSONEncoder(JSONEncoder): 57 """PumpWood default serializer. 58 59 Treat not simple python types to facilitate at serialization of 60 pandas, numpy, data, datetime and other data types. 61 """ 62 63 def default(self, obj): 64 """Serialize complex objects.""" 65 return default_encoder(obj) 66 67 68def pumpJsonDump(x: any, sort_keys: bool = False, # NOQA 69 indent: Union[int, bool] = None): 70 """Dump a Json to python object. 71 72 Args: 73 x (any): 74 Object to be serialized using PumpWoodJSONEncoder encoder. 75 sort_keys (bool): 76 If json serialized data should have its keys sorted. This option 77 makes serialization return of data reproductable. 78 indent (int): 79 Pass indent argument to simplejson dumps. 80 """ 81 # Compatibility with simplejson serialization 82 is_indent = indent is not None 83 if sort_keys and is_indent: 84 return orjson.dumps(x, default=default_encoder, option=( 85 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 86 orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 | 87 orjson.OPT_SERIALIZE_NUMPY)) 88 elif sort_keys: 89 return orjson.dumps(x, default=default_encoder, option=( 90 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 91 orjson.OPT_SORT_KEYS | orjson.OPT_SERIALIZE_NUMPY)) 92 elif is_indent: 93 return orjson.dumps(x, default=default_encoder, option=( 94 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 95 orjson.OPT_INDENT_2 | orjson.OPT_SERIALIZE_NUMPY)) 96 else: 97 return orjson.dumps(x, default=default_encoder, option=( 98 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 99 orjson.OPT_SERIALIZE_NUMPY)) 100 101 102class CompositePkBase64Converter: 103 """Convert composite primary keys in base64 dictionary.""" 104 105 @staticmethod 106 def get_attribute(obj: Any, att: str) -> Any: 107 """Get attribute from object or dictinary. 108 109 Args: 110 obj (Any): 111 Object or a dictinary. 112 att (str): 113 Name of the attribute/key that will be used to return 114 the value. 115 116 Return: 117 Return object/dictionary value associated with attribute. 118 """ 119 temp_pk_value = None 120 if type(obj) is dict: 121 temp_pk_value = obj[att] 122 else: 123 temp_pk_value = getattr(obj, att) 124 return temp_pk_value 125 126 @classmethod 127 def dump(cls, obj, primary_keys: Union[str, List[str], Dict[str, str]] 128 ) -> Union[str, int]: 129 """Convert primary keys and composite to a single value. 130 131 Treat cases when more than one column are used as primary keys, 132 at this cases, a base64 used on url serialization of the dictionary 133 is returned. 134 135 Args: 136 obj: 137 SQLAlchemy object. 138 primary_keys (Union[str, List[str], Dict[str, str]): 139 As string, a list or a dictionary leading to different 140 behaviour. 141 - **str:** It will return the value associated with object 142 attribute. 143 - **List[str]:** If list has lenght equal to 1, it will have 144 same behaviour as str. If greater than 1, it will be 145 returned a base64 encoded dictionary with the keys at 146 primary_keys. 147 - **Dict[str, str]:** Dictionary to map object fields to 148 other keys. This is usefull when querying related fields 149 by composite forenging keys to match original data fieds. 150 151 Returns: 152 If the primary key is unique, return the value of the primary 153 key, if is have more than one column as primary key, return 154 a dictionary of the primary keys encoded as base64 url safe. 155 """ 156 if type(primary_keys) is str: 157 return getattr(obj, primary_keys) 158 159 elif type(primary_keys) is list: 160 if len(primary_keys) == 1: 161 return getattr(obj, primary_keys[0]) 162 else: 163 # Will return a None value if all composite primary keys are 164 # None 165 is_all_none = False 166 composite_pk_dict = {} 167 for pk_col in primary_keys: 168 temp_pk_value = cls.get_attribute(obj, pk_col) 169 is_all_none = is_all_none or (temp_pk_value is None) 170 composite_pk_dict[pk_col] = temp_pk_value 171 if is_all_none: 172 return None 173 174 # If not all primary keys are None, them serialize it and 175 # convert to a base64 dictionary to be used as PK 176 composite_pk_str = pumpJsonDump(composite_pk_dict) 177 return base64.urlsafe_b64encode( 178 composite_pk_str.encode()).decode() 179 180 # Map object values to other, this is used when builds forenging 181 # key references and request related field using microservice. 182 elif type(primary_keys) is dict: 183 # Will return a None value if all composite primary keys are 184 # None 185 is_all_none = False 186 composite_pk_dict = {} 187 for key, value in primary_keys.items(): 188 temp_pk_value = cls.get_attribute(obj, key) 189 is_all_none = is_all_none or (temp_pk_value is None) 190 composite_pk_dict[value] = temp_pk_value 191 if is_all_none: 192 return None 193 194 # If not all primary keys are None, them serialize it and 195 # convert to a base64 dictionary to be used as PK. Using 196 # dictionary will map values before converting to base64 197 # dictionary 198 composite_pk_str = pumpJsonDump(composite_pk_dict) 199 base64_composite_pk = base64.urlsafe_b64encode( 200 composite_pk_str.encode()).decode() 201 return base64_composite_pk 202 203 # This will raise error if primary_keys type is not implemented 204 else: 205 msg = ( 206 "CompositePkBase64Converter.dump argument primary_keys " 207 "is not a list of strings or a map dictionary. Type " 208 "[{arg_type}]").format(arg_type=type(primary_keys)) 209 raise PumpWoodNotImplementedError(message=msg) 210 211 @staticmethod 212 def load(value: Union[str, int]) -> Union[int, dict]: 213 """Convert encoded primary keys to values. 214 215 If the primary key is a string, try to transform it to dictionary 216 decoding json base64 to a dictionary. 217 218 Args: 219 value: 220 Primary key value as an integer or as a base64 221 encoded json dictionary. 222 223 Return: 224 Return the primary key as integer if possible, or try to decoded 225 it to a dictionary from a base64 encoded json. 226 """ 227 # Try to convert value to integer 228 try: 229 float_value = float(value) 230 if float_value.is_integer(): 231 return int(float_value) 232 else: 233 msg = "[{value}] value is a float, but not integer." 234 raise PumpWoodException(msg, payload={"value": value}) 235 236 # If not possible, try to decode a base64 JSON dictionary 237 except Exception as e1: 238 try: 239 return json.loads(base64.b64decode(value)) 240 except Exception as e2: 241 msg = ( 242 "[{value}] value is not an integer and could no be " 243 "decoded as a base64 encoded json dictionary. Value=") 244 raise PumpWoodException( 245 message=msg, payload={ 246 "value": value, 247 "exception_int": str(e1), 248 "exception_base64": str(e2)})
21def default_encoder(obj): 22 """Serialize complex objects.""" 23 # Return None if object is NaN 24 if isinstance(obj, datetime): 25 return obj.isoformat() 26 if isinstance(obj, Timestamp): 27 return obj.isoformat() 28 if isinstance(obj, date): 29 return obj.isoformat() 30 if isinstance(obj, time): 31 return obj.isoformat() 32 if isinstance(obj, np.ndarray): 33 return obj.tolist() 34 if isinstance(obj, pd.DataFrame): 35 return obj.to_dict('records') 36 if isinstance(obj, pd.Series): 37 obj.dtype == Timestamp 38 return obj.tolist() 39 if isinstance(obj, np.generic): 40 return obj.item() 41 if isinstance(obj, BaseGeometry): 42 if obj.is_empty: 43 return None 44 else: 45 return mapping(obj) 46 if isinstance(obj, BaseGeometry): 47 return mapping(obj) 48 if isinstance(obj, Choice): 49 return obj.code 50 if isinstance(obj, set): 51 return list(obj) 52 else: 53 raise TypeError( 54 "Unserializable object {} of type {}".format(obj, type(obj)))
Serialize complex objects.
57class PumpWoodJSONEncoder(JSONEncoder): 58 """PumpWood default serializer. 59 60 Treat not simple python types to facilitate at serialization of 61 pandas, numpy, data, datetime and other data types. 62 """ 63 64 def default(self, obj): 65 """Serialize complex objects.""" 66 return default_encoder(obj)
PumpWood default serializer.
Treat not simple python types to facilitate at serialization of pandas, numpy, data, datetime and other data types.
69def pumpJsonDump(x: any, sort_keys: bool = False, # NOQA 70 indent: Union[int, bool] = None): 71 """Dump a Json to python object. 72 73 Args: 74 x (any): 75 Object to be serialized using PumpWoodJSONEncoder encoder. 76 sort_keys (bool): 77 If json serialized data should have its keys sorted. This option 78 makes serialization return of data reproductable. 79 indent (int): 80 Pass indent argument to simplejson dumps. 81 """ 82 # Compatibility with simplejson serialization 83 is_indent = indent is not None 84 if sort_keys and is_indent: 85 return orjson.dumps(x, default=default_encoder, option=( 86 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 87 orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 | 88 orjson.OPT_SERIALIZE_NUMPY)) 89 elif sort_keys: 90 return orjson.dumps(x, default=default_encoder, option=( 91 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 92 orjson.OPT_SORT_KEYS | orjson.OPT_SERIALIZE_NUMPY)) 93 elif is_indent: 94 return orjson.dumps(x, default=default_encoder, option=( 95 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 96 orjson.OPT_INDENT_2 | orjson.OPT_SERIALIZE_NUMPY)) 97 else: 98 return orjson.dumps(x, default=default_encoder, option=( 99 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 100 orjson.OPT_SERIALIZE_NUMPY))
Dump a Json to python object.
Arguments:
- x (any): Object to be serialized using PumpWoodJSONEncoder encoder.
- sort_keys (bool): If json serialized data should have its keys sorted. This option makes serialization return of data reproductable.
- indent (int): Pass indent argument to simplejson dumps.
103class CompositePkBase64Converter: 104 """Convert composite primary keys in base64 dictionary.""" 105 106 @staticmethod 107 def get_attribute(obj: Any, att: str) -> Any: 108 """Get attribute from object or dictinary. 109 110 Args: 111 obj (Any): 112 Object or a dictinary. 113 att (str): 114 Name of the attribute/key that will be used to return 115 the value. 116 117 Return: 118 Return object/dictionary value associated with attribute. 119 """ 120 temp_pk_value = None 121 if type(obj) is dict: 122 temp_pk_value = obj[att] 123 else: 124 temp_pk_value = getattr(obj, att) 125 return temp_pk_value 126 127 @classmethod 128 def dump(cls, obj, primary_keys: Union[str, List[str], Dict[str, str]] 129 ) -> Union[str, int]: 130 """Convert primary keys and composite to a single value. 131 132 Treat cases when more than one column are used as primary keys, 133 at this cases, a base64 used on url serialization of the dictionary 134 is returned. 135 136 Args: 137 obj: 138 SQLAlchemy object. 139 primary_keys (Union[str, List[str], Dict[str, str]): 140 As string, a list or a dictionary leading to different 141 behaviour. 142 - **str:** It will return the value associated with object 143 attribute. 144 - **List[str]:** If list has lenght equal to 1, it will have 145 same behaviour as str. If greater than 1, it will be 146 returned a base64 encoded dictionary with the keys at 147 primary_keys. 148 - **Dict[str, str]:** Dictionary to map object fields to 149 other keys. This is usefull when querying related fields 150 by composite forenging keys to match original data fieds. 151 152 Returns: 153 If the primary key is unique, return the value of the primary 154 key, if is have more than one column as primary key, return 155 a dictionary of the primary keys encoded as base64 url safe. 156 """ 157 if type(primary_keys) is str: 158 return getattr(obj, primary_keys) 159 160 elif type(primary_keys) is list: 161 if len(primary_keys) == 1: 162 return getattr(obj, primary_keys[0]) 163 else: 164 # Will return a None value if all composite primary keys are 165 # None 166 is_all_none = False 167 composite_pk_dict = {} 168 for pk_col in primary_keys: 169 temp_pk_value = cls.get_attribute(obj, pk_col) 170 is_all_none = is_all_none or (temp_pk_value is None) 171 composite_pk_dict[pk_col] = temp_pk_value 172 if is_all_none: 173 return None 174 175 # If not all primary keys are None, them serialize it and 176 # convert to a base64 dictionary to be used as PK 177 composite_pk_str = pumpJsonDump(composite_pk_dict) 178 return base64.urlsafe_b64encode( 179 composite_pk_str.encode()).decode() 180 181 # Map object values to other, this is used when builds forenging 182 # key references and request related field using microservice. 183 elif type(primary_keys) is dict: 184 # Will return a None value if all composite primary keys are 185 # None 186 is_all_none = False 187 composite_pk_dict = {} 188 for key, value in primary_keys.items(): 189 temp_pk_value = cls.get_attribute(obj, key) 190 is_all_none = is_all_none or (temp_pk_value is None) 191 composite_pk_dict[value] = temp_pk_value 192 if is_all_none: 193 return None 194 195 # If not all primary keys are None, them serialize it and 196 # convert to a base64 dictionary to be used as PK. Using 197 # dictionary will map values before converting to base64 198 # dictionary 199 composite_pk_str = pumpJsonDump(composite_pk_dict) 200 base64_composite_pk = base64.urlsafe_b64encode( 201 composite_pk_str.encode()).decode() 202 return base64_composite_pk 203 204 # This will raise error if primary_keys type is not implemented 205 else: 206 msg = ( 207 "CompositePkBase64Converter.dump argument primary_keys " 208 "is not a list of strings or a map dictionary. Type " 209 "[{arg_type}]").format(arg_type=type(primary_keys)) 210 raise PumpWoodNotImplementedError(message=msg) 211 212 @staticmethod 213 def load(value: Union[str, int]) -> Union[int, dict]: 214 """Convert encoded primary keys to values. 215 216 If the primary key is a string, try to transform it to dictionary 217 decoding json base64 to a dictionary. 218 219 Args: 220 value: 221 Primary key value as an integer or as a base64 222 encoded json dictionary. 223 224 Return: 225 Return the primary key as integer if possible, or try to decoded 226 it to a dictionary from a base64 encoded json. 227 """ 228 # Try to convert value to integer 229 try: 230 float_value = float(value) 231 if float_value.is_integer(): 232 return int(float_value) 233 else: 234 msg = "[{value}] value is a float, but not integer." 235 raise PumpWoodException(msg, payload={"value": value}) 236 237 # If not possible, try to decode a base64 JSON dictionary 238 except Exception as e1: 239 try: 240 return json.loads(base64.b64decode(value)) 241 except Exception as e2: 242 msg = ( 243 "[{value}] value is not an integer and could no be " 244 "decoded as a base64 encoded json dictionary. Value=") 245 raise PumpWoodException( 246 message=msg, payload={ 247 "value": value, 248 "exception_int": str(e1), 249 "exception_base64": str(e2)})
Convert composite primary keys in base64 dictionary.
106 @staticmethod 107 def get_attribute(obj: Any, att: str) -> Any: 108 """Get attribute from object or dictinary. 109 110 Args: 111 obj (Any): 112 Object or a dictinary. 113 att (str): 114 Name of the attribute/key that will be used to return 115 the value. 116 117 Return: 118 Return object/dictionary value associated with attribute. 119 """ 120 temp_pk_value = None 121 if type(obj) is dict: 122 temp_pk_value = obj[att] 123 else: 124 temp_pk_value = getattr(obj, att) 125 return temp_pk_value
Get attribute from object or dictinary.
Arguments:
- obj (Any): Object or a dictinary.
- att (str): Name of the attribute/key that will be used to return the value.
Return:
Return object/dictionary value associated with attribute.
127 @classmethod 128 def dump(cls, obj, primary_keys: Union[str, List[str], Dict[str, str]] 129 ) -> Union[str, int]: 130 """Convert primary keys and composite to a single value. 131 132 Treat cases when more than one column are used as primary keys, 133 at this cases, a base64 used on url serialization of the dictionary 134 is returned. 135 136 Args: 137 obj: 138 SQLAlchemy object. 139 primary_keys (Union[str, List[str], Dict[str, str]): 140 As string, a list or a dictionary leading to different 141 behaviour. 142 - **str:** It will return the value associated with object 143 attribute. 144 - **List[str]:** If list has lenght equal to 1, it will have 145 same behaviour as str. If greater than 1, it will be 146 returned a base64 encoded dictionary with the keys at 147 primary_keys. 148 - **Dict[str, str]:** Dictionary to map object fields to 149 other keys. This is usefull when querying related fields 150 by composite forenging keys to match original data fieds. 151 152 Returns: 153 If the primary key is unique, return the value of the primary 154 key, if is have more than one column as primary key, return 155 a dictionary of the primary keys encoded as base64 url safe. 156 """ 157 if type(primary_keys) is str: 158 return getattr(obj, primary_keys) 159 160 elif type(primary_keys) is list: 161 if len(primary_keys) == 1: 162 return getattr(obj, primary_keys[0]) 163 else: 164 # Will return a None value if all composite primary keys are 165 # None 166 is_all_none = False 167 composite_pk_dict = {} 168 for pk_col in primary_keys: 169 temp_pk_value = cls.get_attribute(obj, pk_col) 170 is_all_none = is_all_none or (temp_pk_value is None) 171 composite_pk_dict[pk_col] = temp_pk_value 172 if is_all_none: 173 return None 174 175 # If not all primary keys are None, them serialize it and 176 # convert to a base64 dictionary to be used as PK 177 composite_pk_str = pumpJsonDump(composite_pk_dict) 178 return base64.urlsafe_b64encode( 179 composite_pk_str.encode()).decode() 180 181 # Map object values to other, this is used when builds forenging 182 # key references and request related field using microservice. 183 elif type(primary_keys) is dict: 184 # Will return a None value if all composite primary keys are 185 # None 186 is_all_none = False 187 composite_pk_dict = {} 188 for key, value in primary_keys.items(): 189 temp_pk_value = cls.get_attribute(obj, key) 190 is_all_none = is_all_none or (temp_pk_value is None) 191 composite_pk_dict[value] = temp_pk_value 192 if is_all_none: 193 return None 194 195 # If not all primary keys are None, them serialize it and 196 # convert to a base64 dictionary to be used as PK. Using 197 # dictionary will map values before converting to base64 198 # dictionary 199 composite_pk_str = pumpJsonDump(composite_pk_dict) 200 base64_composite_pk = base64.urlsafe_b64encode( 201 composite_pk_str.encode()).decode() 202 return base64_composite_pk 203 204 # This will raise error if primary_keys type is not implemented 205 else: 206 msg = ( 207 "CompositePkBase64Converter.dump argument primary_keys " 208 "is not a list of strings or a map dictionary. Type " 209 "[{arg_type}]").format(arg_type=type(primary_keys)) 210 raise PumpWoodNotImplementedError(message=msg)
Convert primary keys and composite to a single value.
Treat cases when more than one column are used as primary keys, at this cases, a base64 used on url serialization of the dictionary is returned.
Arguments:
- obj: SQLAlchemy object.
- primary_keys (Union[str, List[str], Dict[str, str]): As string, a list or a dictionary leading to different
behaviour.
- str: It will return the value associated with object attribute.
- List[str]: If list has lenght equal to 1, it will have same behaviour as str. If greater than 1, it will be returned a base64 encoded dictionary with the keys at primary_keys.
- Dict[str, str]: Dictionary to map object fields to other keys. This is usefull when querying related fields by composite forenging keys to match original data fieds.
Returns:
If the primary key is unique, return the value of the primary key, if is have more than one column as primary key, return a dictionary of the primary keys encoded as base64 url safe.
212 @staticmethod 213 def load(value: Union[str, int]) -> Union[int, dict]: 214 """Convert encoded primary keys to values. 215 216 If the primary key is a string, try to transform it to dictionary 217 decoding json base64 to a dictionary. 218 219 Args: 220 value: 221 Primary key value as an integer or as a base64 222 encoded json dictionary. 223 224 Return: 225 Return the primary key as integer if possible, or try to decoded 226 it to a dictionary from a base64 encoded json. 227 """ 228 # Try to convert value to integer 229 try: 230 float_value = float(value) 231 if float_value.is_integer(): 232 return int(float_value) 233 else: 234 msg = "[{value}] value is a float, but not integer." 235 raise PumpWoodException(msg, payload={"value": value}) 236 237 # If not possible, try to decode a base64 JSON dictionary 238 except Exception as e1: 239 try: 240 return json.loads(base64.b64decode(value)) 241 except Exception as e2: 242 msg = ( 243 "[{value}] value is not an integer and could no be " 244 "decoded as a base64 encoded json dictionary. Value=") 245 raise PumpWoodException( 246 message=msg, payload={ 247 "value": value, 248 "exception_int": str(e1), 249 "exception_base64": str(e2)})
Convert encoded primary keys to values.
If the primary key is a string, try to transform it to dictionary decoding json base64 to a dictionary.
Arguments:
- value: Primary key value as an integer or as a base64 encoded json dictionary.
Return:
Return the primary key as integer if possible, or try to decoded it to a dictionary from a base64 encoded json.