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 decimal import Decimal 13from pandas import Timestamp 14from shapely.geometry.base import BaseGeometry 15from shapely.geometry import mapping 16from sqlalchemy_utils.types.choice import Choice 17from pumpwood_communication.exceptions import ( 18 PumpWoodException, PumpWoodNotImplementedError) 19from pumpwood_communication.type import ( 20 PumpwoodSentinel, PumpwoodDataclassMixin, MISSING) 21 22 23def default_encoder(obj): 24 """Serialize complex objects.""" 25 # Return None if object is NaN 26 if not isinstance(obj, (pd.DataFrame, pd.Series, np.ndarray, list, dict)): 27 if pd.isna(obj): 28 return None 29 30 if isinstance(obj, (datetime, Timestamp, date, 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 return obj.tolist() 38 if isinstance(obj, np.generic): 39 return obj.item() 40 if isinstance(obj, Decimal): 41 return float(obj) 42 if isinstance(obj, BaseGeometry): 43 if obj.is_empty: 44 return None 45 else: 46 return mapping(obj) 47 if isinstance(obj, Choice): 48 return obj.code 49 if isinstance(obj, set): 50 return list(obj) 51 52 ######################################################### 53 # TODO: Adjust convertion of decimal to preseve precision 54 # There is lost of precision when converting decimal to float, 55 # but Decimal is not currently parsiable using orjson 56 if isinstance(obj, Decimal): 57 return float(obj) 58 59 ################################### 60 # Serialize Pumpwood expecial types 61 if isinstance(obj, PumpwoodDataclassMixin): 62 return obj.to_dict() 63 if isinstance(obj, PumpwoodSentinel): 64 return obj.value() 65 else: 66 raise TypeError( 67 "Unserializable object {} of type {}".format(obj, type(obj))) 68 69 70class PumpWoodJSONEncoder(JSONEncoder): 71 """PumpWood default serializer. 72 73 Treat not simple python types to facilitate at serialization of 74 pandas, numpy, data, datetime and other data types. 75 """ 76 77 def default(self, obj): 78 """Serialize complex objects.""" 79 return default_encoder(obj) 80 81 82def pumpJsonDump(x: any, sort_keys: bool = False, # NOQA 83 indent: Union[int, bool] = None): 84 """Dump a Json to python object. 85 86 Args: 87 x (any): 88 Object to be serialized using PumpWoodJSONEncoder encoder. 89 sort_keys (bool): 90 If json serialized data should have its keys sorted. This option 91 makes serialization return of data reproductable. 92 indent (int): 93 Pass indent argument to simplejson dumps. 94 """ 95 # Compatibility with simplejson serialization 96 is_indent = indent is not None 97 if sort_keys and is_indent: 98 return orjson.dumps(x, default=default_encoder, option=( 99 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 100 orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 | 101 orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATACLASS)) 102 elif sort_keys: 103 return orjson.dumps(x, default=default_encoder, option=( 104 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 105 orjson.OPT_SORT_KEYS | orjson.OPT_SERIALIZE_NUMPY | 106 orjson.OPT_PASSTHROUGH_DATACLASS)) 107 elif is_indent: 108 return orjson.dumps(x, default=default_encoder, option=( 109 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 110 orjson.OPT_INDENT_2 | orjson.OPT_SERIALIZE_NUMPY | 111 orjson.OPT_PASSTHROUGH_DATACLASS)) 112 else: 113 return orjson.dumps(x, default=default_encoder, option=( 114 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 115 orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATACLASS)) 116 117 118class CompositePkBase64Converter: 119 """Convert composite primary keys in base64 dictionary.""" 120 121 @staticmethod 122 def get_attribute(obj: Any, att: str) -> Any: 123 """Get attribute from object or dictinary. 124 125 Args: 126 obj (Any): 127 Object or a dictinary. 128 att (str): 129 Name of the attribute/key that will be used to return 130 the value. 131 132 Return: 133 Return object/dictionary value associated with attribute. 134 """ 135 if type(obj) is dict: 136 return obj.get(att, MISSING) 137 else: 138 return getattr(obj, att, MISSING) 139 140 @classmethod 141 def dump(cls, obj: object | dict, 142 primary_keys: Union[str, List[str], Dict[str, str]] 143 ) -> Union[str, int]: 144 """Convert primary keys and composite to a single value. 145 146 Treat cases when more than one column are used as primary keys, 147 at this cases, a base64 used on url serialization of the dictionary 148 is returned. 149 150 Args: 151 obj: 152 SQLAlchemy object or dictionary with data to build the 153 forenging key. 154 primary_keys (Union[str, List[str], Dict[str, str]): 155 As string, a list or a dictionary leading to different 156 behaviour. 157 - **str:** It will return the value associated with object 158 attribute. 159 - **List[str]:** If list has length equal to 1, it will have 160 same behaviour as str. If greater than 1, it will be 161 returned a base64 encoded dictionary with the keys at 162 primary_keys. 163 - **Dict[str, str]:** Dictionary to map object fields to 164 other keys. This is usefull when querying related fields 165 by composite forenging keys to match original data fieds. 166 167 Returns: 168 If the primary key is unique, return the value of the primary 169 key, if is have more than one column as primary key, return 170 a dictionary of the primary keys encoded as base64 url safe. 171 """ 172 if obj is None: 173 return None 174 175 missing_keys = [] 176 return_value = None 177 if isinstance(primary_keys, str): 178 key_value = cls.get_attribute(obj, primary_keys) 179 if key_value != MISSING: 180 return_value = key_value 181 else: 182 missing_keys.append(primary_keys) 183 184 elif isinstance(primary_keys, list): 185 if len(primary_keys) == 1: 186 key_value = cls.get_attribute(obj, primary_keys[0]) 187 if key_value != MISSING: 188 return key_value 189 else: 190 missing_keys.append(primary_keys[0]) 191 else: 192 composite_pk_dict = {} 193 for pk_col in primary_keys: 194 key_value = cls.get_attribute(obj, pk_col) 195 if key_value != MISSING: 196 composite_pk_dict[pk_col] = key_value 197 else: 198 missing_keys.append(pk_col) 199 return_value = composite_pk_dict 200 201 # Map object values to other, this is used when builds forenging 202 # key references and request related field using microservice. 203 elif isinstance(primary_keys, dict): 204 # Treat the case when the dictinary is only an id->value 205 composite_pk_dict = {} 206 for key, value in primary_keys.items(): 207 key_value = cls.get_attribute(obj, key) 208 if key_value != MISSING: 209 composite_pk_dict[value] = key_value 210 else: 211 missing_keys.append(key) 212 return_value = composite_pk_dict 213 214 # Check if some missing keys are there 215 if len(missing_keys) != 0: 216 msg = ( 217 "Some keys were not found on object/dict to create " 218 "the composite forenging key. " 219 "Primary Keys: {primary_keys}; " 220 "Missing keys: {missing_keys}") 221 raise PumpWoodException(msg, payload={ 222 "primary_keys": list(primary_keys.keys()), 223 "missing_keys": missing_keys}) 224 225 if isinstance(return_value, dict): 226 if {'id'} == set(return_value.keys()): 227 return return_value['id'] 228 return cls.dump_dict(primary_key_dict=return_value) 229 else: 230 return return_value 231 232 @classmethod 233 def validate_primary_key_dict(cls, primary_key_dict: dict): 234 """.""" 235 if not isinstance(primary_key_dict, dict): 236 msg = "primary_key_dict must be a dictionary. Received: {type}" 237 raise PumpWoodNotImplementedError( 238 msg, payload={'type': type(primary_key_dict).__name__}) 239 240 for key, value in primary_key_dict.items(): 241 if isinstance(value, (dict, list, tuple)): 242 msg = ( 243 "primary_key_dict must be flat dictionary. " 244 "Nested type[{type}] found at key [{key}]") 245 raise PumpWoodException( 246 msg.format(), 247 payload={ 248 'primary_key_dict': primary_key_dict, 249 'type': type(value).__name__, 250 'key': key}) 251 252 @classmethod 253 def dump_dict(cls, primary_key_dict: dict) -> str: 254 """Dump a primary key dictionary to base64 url safe string. 255 256 Args: 257 primary_key_dict (dict): 258 Dictionary with the primary key values. 259 260 Returns: 261 str: 262 Base64 url safe string of the primary key dictionary. 263 """ 264 cls.validate_primary_key_dict(primary_key_dict=primary_key_dict) 265 composite_pk_str = pumpJsonDump(primary_key_dict) 266 base64_composite_pk = base64.urlsafe_b64encode(composite_pk_str)\ 267 .decode() 268 return base64_composite_pk 269 270 @staticmethod 271 def load(value: Union[str, int]) -> Union[int, dict]: 272 """Convert encoded primary keys to values. 273 274 If the primary key is a string, try to transform it to dictionary 275 decoding json base64 to a dictionary. 276 277 Args: 278 value: 279 Primary key value as an integer or as a base64 280 encoded json dictionary. 281 282 Return: 283 Return the primary key as integer if possible, or try to decoded 284 it to a dictionary from a base64 encoded json. 285 """ 286 # Try to convert value to integer 287 try: 288 float_value = float(value) 289 if float_value.is_integer(): 290 return int(float_value) 291 else: 292 msg = "[{value}] value is a float, but not integer." 293 raise PumpWoodException(msg, payload={"value": value}) 294 295 # If not possible, try to decode a base64 JSON dictionary 296 except Exception as e1: 297 try: 298 return json.loads(base64.urlsafe_b64decode(value)) 299 except Exception as e2: 300 msg = ( 301 "[{value}] value is not an integer and could no be " 302 "decoded as a base64 encoded json dictionary. Value=") 303 raise PumpWoodException( 304 message=msg, payload={ 305 "value": value, 306 "exception_int": str(e1), 307 "exception_base64": str(e2)})
24def default_encoder(obj): 25 """Serialize complex objects.""" 26 # Return None if object is NaN 27 if not isinstance(obj, (pd.DataFrame, pd.Series, np.ndarray, list, dict)): 28 if pd.isna(obj): 29 return None 30 31 if isinstance(obj, (datetime, Timestamp, date, time)): 32 return obj.isoformat() 33 if isinstance(obj, np.ndarray): 34 return obj.tolist() 35 if isinstance(obj, pd.DataFrame): 36 return obj.to_dict('records') 37 if isinstance(obj, pd.Series): 38 return obj.tolist() 39 if isinstance(obj, np.generic): 40 return obj.item() 41 if isinstance(obj, Decimal): 42 return float(obj) 43 if isinstance(obj, BaseGeometry): 44 if obj.is_empty: 45 return None 46 else: 47 return mapping(obj) 48 if isinstance(obj, Choice): 49 return obj.code 50 if isinstance(obj, set): 51 return list(obj) 52 53 ######################################################### 54 # TODO: Adjust convertion of decimal to preseve precision 55 # There is lost of precision when converting decimal to float, 56 # but Decimal is not currently parsiable using orjson 57 if isinstance(obj, Decimal): 58 return float(obj) 59 60 ################################### 61 # Serialize Pumpwood expecial types 62 if isinstance(obj, PumpwoodDataclassMixin): 63 return obj.to_dict() 64 if isinstance(obj, PumpwoodSentinel): 65 return obj.value() 66 else: 67 raise TypeError( 68 "Unserializable object {} of type {}".format(obj, type(obj)))
Serialize complex objects.
71class PumpWoodJSONEncoder(JSONEncoder): 72 """PumpWood default serializer. 73 74 Treat not simple python types to facilitate at serialization of 75 pandas, numpy, data, datetime and other data types. 76 """ 77 78 def default(self, obj): 79 """Serialize complex objects.""" 80 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.
83def pumpJsonDump(x: any, sort_keys: bool = False, # NOQA 84 indent: Union[int, bool] = None): 85 """Dump a Json to python object. 86 87 Args: 88 x (any): 89 Object to be serialized using PumpWoodJSONEncoder encoder. 90 sort_keys (bool): 91 If json serialized data should have its keys sorted. This option 92 makes serialization return of data reproductable. 93 indent (int): 94 Pass indent argument to simplejson dumps. 95 """ 96 # Compatibility with simplejson serialization 97 is_indent = indent is not None 98 if sort_keys and is_indent: 99 return orjson.dumps(x, default=default_encoder, option=( 100 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 101 orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 | 102 orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATACLASS)) 103 elif sort_keys: 104 return orjson.dumps(x, default=default_encoder, option=( 105 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 106 orjson.OPT_SORT_KEYS | orjson.OPT_SERIALIZE_NUMPY | 107 orjson.OPT_PASSTHROUGH_DATACLASS)) 108 elif is_indent: 109 return orjson.dumps(x, default=default_encoder, option=( 110 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 111 orjson.OPT_INDENT_2 | orjson.OPT_SERIALIZE_NUMPY | 112 orjson.OPT_PASSTHROUGH_DATACLASS)) 113 else: 114 return orjson.dumps(x, default=default_encoder, option=( 115 orjson.OPT_NAIVE_UTC | orjson.OPT_NON_STR_KEYS | 116 orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATACLASS))
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.
119class CompositePkBase64Converter: 120 """Convert composite primary keys in base64 dictionary.""" 121 122 @staticmethod 123 def get_attribute(obj: Any, att: str) -> Any: 124 """Get attribute from object or dictinary. 125 126 Args: 127 obj (Any): 128 Object or a dictinary. 129 att (str): 130 Name of the attribute/key that will be used to return 131 the value. 132 133 Return: 134 Return object/dictionary value associated with attribute. 135 """ 136 if type(obj) is dict: 137 return obj.get(att, MISSING) 138 else: 139 return getattr(obj, att, MISSING) 140 141 @classmethod 142 def dump(cls, obj: object | dict, 143 primary_keys: Union[str, List[str], Dict[str, str]] 144 ) -> Union[str, int]: 145 """Convert primary keys and composite to a single value. 146 147 Treat cases when more than one column are used as primary keys, 148 at this cases, a base64 used on url serialization of the dictionary 149 is returned. 150 151 Args: 152 obj: 153 SQLAlchemy object or dictionary with data to build the 154 forenging key. 155 primary_keys (Union[str, List[str], Dict[str, str]): 156 As string, a list or a dictionary leading to different 157 behaviour. 158 - **str:** It will return the value associated with object 159 attribute. 160 - **List[str]:** If list has length equal to 1, it will have 161 same behaviour as str. If greater than 1, it will be 162 returned a base64 encoded dictionary with the keys at 163 primary_keys. 164 - **Dict[str, str]:** Dictionary to map object fields to 165 other keys. This is usefull when querying related fields 166 by composite forenging keys to match original data fieds. 167 168 Returns: 169 If the primary key is unique, return the value of the primary 170 key, if is have more than one column as primary key, return 171 a dictionary of the primary keys encoded as base64 url safe. 172 """ 173 if obj is None: 174 return None 175 176 missing_keys = [] 177 return_value = None 178 if isinstance(primary_keys, str): 179 key_value = cls.get_attribute(obj, primary_keys) 180 if key_value != MISSING: 181 return_value = key_value 182 else: 183 missing_keys.append(primary_keys) 184 185 elif isinstance(primary_keys, list): 186 if len(primary_keys) == 1: 187 key_value = cls.get_attribute(obj, primary_keys[0]) 188 if key_value != MISSING: 189 return key_value 190 else: 191 missing_keys.append(primary_keys[0]) 192 else: 193 composite_pk_dict = {} 194 for pk_col in primary_keys: 195 key_value = cls.get_attribute(obj, pk_col) 196 if key_value != MISSING: 197 composite_pk_dict[pk_col] = key_value 198 else: 199 missing_keys.append(pk_col) 200 return_value = composite_pk_dict 201 202 # Map object values to other, this is used when builds forenging 203 # key references and request related field using microservice. 204 elif isinstance(primary_keys, dict): 205 # Treat the case when the dictinary is only an id->value 206 composite_pk_dict = {} 207 for key, value in primary_keys.items(): 208 key_value = cls.get_attribute(obj, key) 209 if key_value != MISSING: 210 composite_pk_dict[value] = key_value 211 else: 212 missing_keys.append(key) 213 return_value = composite_pk_dict 214 215 # Check if some missing keys are there 216 if len(missing_keys) != 0: 217 msg = ( 218 "Some keys were not found on object/dict to create " 219 "the composite forenging key. " 220 "Primary Keys: {primary_keys}; " 221 "Missing keys: {missing_keys}") 222 raise PumpWoodException(msg, payload={ 223 "primary_keys": list(primary_keys.keys()), 224 "missing_keys": missing_keys}) 225 226 if isinstance(return_value, dict): 227 if {'id'} == set(return_value.keys()): 228 return return_value['id'] 229 return cls.dump_dict(primary_key_dict=return_value) 230 else: 231 return return_value 232 233 @classmethod 234 def validate_primary_key_dict(cls, primary_key_dict: dict): 235 """.""" 236 if not isinstance(primary_key_dict, dict): 237 msg = "primary_key_dict must be a dictionary. Received: {type}" 238 raise PumpWoodNotImplementedError( 239 msg, payload={'type': type(primary_key_dict).__name__}) 240 241 for key, value in primary_key_dict.items(): 242 if isinstance(value, (dict, list, tuple)): 243 msg = ( 244 "primary_key_dict must be flat dictionary. " 245 "Nested type[{type}] found at key [{key}]") 246 raise PumpWoodException( 247 msg.format(), 248 payload={ 249 'primary_key_dict': primary_key_dict, 250 'type': type(value).__name__, 251 'key': key}) 252 253 @classmethod 254 def dump_dict(cls, primary_key_dict: dict) -> str: 255 """Dump a primary key dictionary to base64 url safe string. 256 257 Args: 258 primary_key_dict (dict): 259 Dictionary with the primary key values. 260 261 Returns: 262 str: 263 Base64 url safe string of the primary key dictionary. 264 """ 265 cls.validate_primary_key_dict(primary_key_dict=primary_key_dict) 266 composite_pk_str = pumpJsonDump(primary_key_dict) 267 base64_composite_pk = base64.urlsafe_b64encode(composite_pk_str)\ 268 .decode() 269 return base64_composite_pk 270 271 @staticmethod 272 def load(value: Union[str, int]) -> Union[int, dict]: 273 """Convert encoded primary keys to values. 274 275 If the primary key is a string, try to transform it to dictionary 276 decoding json base64 to a dictionary. 277 278 Args: 279 value: 280 Primary key value as an integer or as a base64 281 encoded json dictionary. 282 283 Return: 284 Return the primary key as integer if possible, or try to decoded 285 it to a dictionary from a base64 encoded json. 286 """ 287 # Try to convert value to integer 288 try: 289 float_value = float(value) 290 if float_value.is_integer(): 291 return int(float_value) 292 else: 293 msg = "[{value}] value is a float, but not integer." 294 raise PumpWoodException(msg, payload={"value": value}) 295 296 # If not possible, try to decode a base64 JSON dictionary 297 except Exception as e1: 298 try: 299 return json.loads(base64.urlsafe_b64decode(value)) 300 except Exception as e2: 301 msg = ( 302 "[{value}] value is not an integer and could no be " 303 "decoded as a base64 encoded json dictionary. Value=") 304 raise PumpWoodException( 305 message=msg, payload={ 306 "value": value, 307 "exception_int": str(e1), 308 "exception_base64": str(e2)})
Convert composite primary keys in base64 dictionary.
122 @staticmethod 123 def get_attribute(obj: Any, att: str) -> Any: 124 """Get attribute from object or dictinary. 125 126 Args: 127 obj (Any): 128 Object or a dictinary. 129 att (str): 130 Name of the attribute/key that will be used to return 131 the value. 132 133 Return: 134 Return object/dictionary value associated with attribute. 135 """ 136 if type(obj) is dict: 137 return obj.get(att, MISSING) 138 else: 139 return getattr(obj, att, MISSING)
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.
141 @classmethod 142 def dump(cls, obj: object | dict, 143 primary_keys: Union[str, List[str], Dict[str, str]] 144 ) -> Union[str, int]: 145 """Convert primary keys and composite to a single value. 146 147 Treat cases when more than one column are used as primary keys, 148 at this cases, a base64 used on url serialization of the dictionary 149 is returned. 150 151 Args: 152 obj: 153 SQLAlchemy object or dictionary with data to build the 154 forenging key. 155 primary_keys (Union[str, List[str], Dict[str, str]): 156 As string, a list or a dictionary leading to different 157 behaviour. 158 - **str:** It will return the value associated with object 159 attribute. 160 - **List[str]:** If list has length equal to 1, it will have 161 same behaviour as str. If greater than 1, it will be 162 returned a base64 encoded dictionary with the keys at 163 primary_keys. 164 - **Dict[str, str]:** Dictionary to map object fields to 165 other keys. This is usefull when querying related fields 166 by composite forenging keys to match original data fieds. 167 168 Returns: 169 If the primary key is unique, return the value of the primary 170 key, if is have more than one column as primary key, return 171 a dictionary of the primary keys encoded as base64 url safe. 172 """ 173 if obj is None: 174 return None 175 176 missing_keys = [] 177 return_value = None 178 if isinstance(primary_keys, str): 179 key_value = cls.get_attribute(obj, primary_keys) 180 if key_value != MISSING: 181 return_value = key_value 182 else: 183 missing_keys.append(primary_keys) 184 185 elif isinstance(primary_keys, list): 186 if len(primary_keys) == 1: 187 key_value = cls.get_attribute(obj, primary_keys[0]) 188 if key_value != MISSING: 189 return key_value 190 else: 191 missing_keys.append(primary_keys[0]) 192 else: 193 composite_pk_dict = {} 194 for pk_col in primary_keys: 195 key_value = cls.get_attribute(obj, pk_col) 196 if key_value != MISSING: 197 composite_pk_dict[pk_col] = key_value 198 else: 199 missing_keys.append(pk_col) 200 return_value = composite_pk_dict 201 202 # Map object values to other, this is used when builds forenging 203 # key references and request related field using microservice. 204 elif isinstance(primary_keys, dict): 205 # Treat the case when the dictinary is only an id->value 206 composite_pk_dict = {} 207 for key, value in primary_keys.items(): 208 key_value = cls.get_attribute(obj, key) 209 if key_value != MISSING: 210 composite_pk_dict[value] = key_value 211 else: 212 missing_keys.append(key) 213 return_value = composite_pk_dict 214 215 # Check if some missing keys are there 216 if len(missing_keys) != 0: 217 msg = ( 218 "Some keys were not found on object/dict to create " 219 "the composite forenging key. " 220 "Primary Keys: {primary_keys}; " 221 "Missing keys: {missing_keys}") 222 raise PumpWoodException(msg, payload={ 223 "primary_keys": list(primary_keys.keys()), 224 "missing_keys": missing_keys}) 225 226 if isinstance(return_value, dict): 227 if {'id'} == set(return_value.keys()): 228 return return_value['id'] 229 return cls.dump_dict(primary_key_dict=return_value) 230 else: 231 return return_value
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 or dictionary with data to build the forenging key.
- 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 length 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.
233 @classmethod 234 def validate_primary_key_dict(cls, primary_key_dict: dict): 235 """.""" 236 if not isinstance(primary_key_dict, dict): 237 msg = "primary_key_dict must be a dictionary. Received: {type}" 238 raise PumpWoodNotImplementedError( 239 msg, payload={'type': type(primary_key_dict).__name__}) 240 241 for key, value in primary_key_dict.items(): 242 if isinstance(value, (dict, list, tuple)): 243 msg = ( 244 "primary_key_dict must be flat dictionary. " 245 "Nested type[{type}] found at key [{key}]") 246 raise PumpWoodException( 247 msg.format(), 248 payload={ 249 'primary_key_dict': primary_key_dict, 250 'type': type(value).__name__, 251 'key': key})
.
253 @classmethod 254 def dump_dict(cls, primary_key_dict: dict) -> str: 255 """Dump a primary key dictionary to base64 url safe string. 256 257 Args: 258 primary_key_dict (dict): 259 Dictionary with the primary key values. 260 261 Returns: 262 str: 263 Base64 url safe string of the primary key dictionary. 264 """ 265 cls.validate_primary_key_dict(primary_key_dict=primary_key_dict) 266 composite_pk_str = pumpJsonDump(primary_key_dict) 267 base64_composite_pk = base64.urlsafe_b64encode(composite_pk_str)\ 268 .decode() 269 return base64_composite_pk
Dump a primary key dictionary to base64 url safe string.
Arguments:
- primary_key_dict (dict): Dictionary with the primary key values.
Returns:
str: Base64 url safe string of the primary key dictionary.
271 @staticmethod 272 def load(value: Union[str, int]) -> Union[int, dict]: 273 """Convert encoded primary keys to values. 274 275 If the primary key is a string, try to transform it to dictionary 276 decoding json base64 to a dictionary. 277 278 Args: 279 value: 280 Primary key value as an integer or as a base64 281 encoded json dictionary. 282 283 Return: 284 Return the primary key as integer if possible, or try to decoded 285 it to a dictionary from a base64 encoded json. 286 """ 287 # Try to convert value to integer 288 try: 289 float_value = float(value) 290 if float_value.is_integer(): 291 return int(float_value) 292 else: 293 msg = "[{value}] value is a float, but not integer." 294 raise PumpWoodException(msg, payload={"value": value}) 295 296 # If not possible, try to decode a base64 JSON dictionary 297 except Exception as e1: 298 try: 299 return json.loads(base64.urlsafe_b64decode(value)) 300 except Exception as e2: 301 msg = ( 302 "[{value}] value is not an integer and could no be " 303 "decoded as a base64 encoded json dictionary. Value=") 304 raise PumpWoodException( 305 message=msg, payload={ 306 "value": value, 307 "exception_int": str(e1), 308 "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.