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)})
def default_encoder(obj):
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.

class PumpWoodJSONEncoder(simplejson.encoder.JSONEncoder):
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.

def default(self, obj):
64    def default(self, obj):
65        """Serialize complex objects."""
66        return default_encoder(obj)

Serialize complex objects.

def pumpJsonDump( x: <built-in function any>, sort_keys: bool = False, indent: Union[int, bool] = None):
 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.
class CompositePkBase64Converter:
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.

@staticmethod
def get_attribute(obj: Any, att: str) -> Any:
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.

@classmethod
def dump( cls, obj, primary_keys: Union[str, List[str], Dict[str, str]]) -> Union[str, int]:
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.

@staticmethod
def load(value: Union[str, int]) -> Union[int, dict]:
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.