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

Serialize complex objects.

def pumpJsonDump( x: <built-in function any>, sort_keys: bool = True, indent: int = None):
63def pumpJsonDump(x: any, sort_keys: bool = True, indent: int = None): # NOQA
64    """Dump a Json to python object.
65
66    Args:
67        x (any):
68            Object to be serialized using PumpWoodJSONEncoder encoder.
69        sort_keys (bool):
70            If json serialized data should have its keys sorted. This option
71            makes serialization return of data reproductable.
72        indent (int):
73            Pass indent argument to simplejson dumps.
74    """
75    return json.dumps(
76        x, cls=PumpWoodJSONEncoder, ignore_nan=True,
77        sort_keys=sort_keys, indent=indent)

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:
 80class CompositePkBase64Converter:
 81    """Convert composite primary keys in base64 dictionary."""
 82
 83    @staticmethod
 84    def dump(obj, primary_keys: Union[str, List[str], Dict[str, str]]
 85             ) -> Union[str, int]:
 86        """Convert primary keys and composite to a single value.
 87
 88        Treat cases when more than one column are used as primary keys,
 89        at this cases, a base64 used on url serialization of the dictionary
 90        is returned.
 91
 92        Args:
 93            obj:
 94                SQLAlchemy object.
 95            primary_keys (Union[str, List[str], Dict[str, str]):
 96                As string, a list or a dictionary leading to different
 97                behaviour.
 98                - **str:** It will return the value associated with object
 99                    attribute.
100                - **List[str]:** If list has lenght equal to 1, it will have
101                    same behaviour as str. If greater than 1, it will be
102                    returned a base64 encoded dictionary with the keys at
103                    primary_keys.
104                - **Dict[str, str]:** Dictionary to map object fields to
105                    other keys. This is usefull when querying related fields
106                    by composite forenging keys to match original data fieds.
107
108        Returns:
109            If the primary key is unique, return the value of the primary
110            key, if is have more than one column as primary key, return
111            a dictionary of the primary keys encoded as base64 url safe.
112        """
113        if type(primary_keys) is str:
114            return getattr(obj, primary_keys)
115
116        elif type(primary_keys) is list:
117            if len(primary_keys) == 1:
118                return getattr(obj, primary_keys[0])
119            else:
120                # Will return a None value if all composite primary keys are
121                # None
122                is_all_none = False
123                composite_pk_dict = {}
124                for pk_col in primary_keys:
125                    temp_pk_value = getattr(obj, pk_col)
126                    is_all_none = is_all_none or (temp_pk_value is None)
127                    composite_pk_dict[pk_col] = temp_pk_value
128                if is_all_none:
129                    return None
130
131                # If not all primary keys are None, them serialize it and
132                # convert to a base64 dictionary to be used as PK
133                composite_pk_str = pumpJsonDump(composite_pk_dict)
134                return base64.urlsafe_b64encode(
135                    composite_pk_str.encode()).decode()
136
137        # Map object values to other, this is used when builds forenging
138        # key references and request related field using microservice.
139        elif type(primary_keys) is dict:
140            # Will return a None value if all composite primary keys are
141            # None
142            is_all_none = False
143            composite_pk_dict = {}
144            for key, value in primary_keys.items():
145                temp_pk_value = getattr(obj, key)
146                is_all_none = is_all_none or (temp_pk_value is None)
147                composite_pk_dict[value] = temp_pk_value
148            if is_all_none:
149                return None
150
151            # If not all primary keys are None, them serialize it and
152            # convert to a base64 dictionary to be used as PK. Using
153            # dictionary will map values before converting to base64
154            # dictionary
155            composite_pk_str = pumpJsonDump(composite_pk_dict)
156            base64_composite_pk = base64.urlsafe_b64encode(
157                composite_pk_str.encode()).decode()
158            return base64_composite_pk
159
160        # This will raise error if primary_keys type is not implemented
161        else:
162            msg = (
163                "CompositePkBase64Converter.dump argument primary_keys "
164                "is not a list of strings or a map dictionary. Type "
165                "[{arg_type}]").format(arg_type=type(primary_keys))
166            raise PumpWoodNotImplementedError(message=msg)
167
168    @staticmethod
169    def load(value: Union[str, int]) -> Union[int, dict]:
170        """Convert encoded primary keys to values.
171
172        If the primary key is a string, try to transform it to dictionary
173        decoding json base64 to a dictionary.
174
175        Args:
176            value:
177                Primary key value as an integer or as a base64
178                encoded json dictionary.
179
180        Return:
181            Return the primary key as integer if possible, or try to decoded
182            it to a dictionary from a base64 encoded json.
183        """
184        # Try to convert value to integer
185        try:
186            float_value = float(value)
187            if float_value.is_integer():
188                return int(float_value)
189            else:
190                msg = "[{value}] value is a float, but not integer."
191                raise PumpWoodException(msg, payload={"value": value})
192
193        # If not possible, try to decode a base64 JSON dictionary
194        except Exception as e1:
195            try:
196                return json.loads(base64.b64decode(value))
197            except Exception as e2:
198                msg = (
199                    "[{value}] value is not an integer and could no be "
200                    "decoded as a base64 encoded json dictionary. Value=")
201                raise PumpWoodException(
202                    message=msg, payload={
203                        "value": value,
204                        "exception_int": str(e1),
205                        "exception_base64": str(e2)})

Convert composite primary keys in base64 dictionary.

@staticmethod
def dump( obj, primary_keys: Union[str, List[str], Dict[str, str]]) -> Union[str, int]:
 83    @staticmethod
 84    def dump(obj, primary_keys: Union[str, List[str], Dict[str, str]]
 85             ) -> Union[str, int]:
 86        """Convert primary keys and composite to a single value.
 87
 88        Treat cases when more than one column are used as primary keys,
 89        at this cases, a base64 used on url serialization of the dictionary
 90        is returned.
 91
 92        Args:
 93            obj:
 94                SQLAlchemy object.
 95            primary_keys (Union[str, List[str], Dict[str, str]):
 96                As string, a list or a dictionary leading to different
 97                behaviour.
 98                - **str:** It will return the value associated with object
 99                    attribute.
100                - **List[str]:** If list has lenght equal to 1, it will have
101                    same behaviour as str. If greater than 1, it will be
102                    returned a base64 encoded dictionary with the keys at
103                    primary_keys.
104                - **Dict[str, str]:** Dictionary to map object fields to
105                    other keys. This is usefull when querying related fields
106                    by composite forenging keys to match original data fieds.
107
108        Returns:
109            If the primary key is unique, return the value of the primary
110            key, if is have more than one column as primary key, return
111            a dictionary of the primary keys encoded as base64 url safe.
112        """
113        if type(primary_keys) is str:
114            return getattr(obj, primary_keys)
115
116        elif type(primary_keys) is list:
117            if len(primary_keys) == 1:
118                return getattr(obj, primary_keys[0])
119            else:
120                # Will return a None value if all composite primary keys are
121                # None
122                is_all_none = False
123                composite_pk_dict = {}
124                for pk_col in primary_keys:
125                    temp_pk_value = getattr(obj, pk_col)
126                    is_all_none = is_all_none or (temp_pk_value is None)
127                    composite_pk_dict[pk_col] = temp_pk_value
128                if is_all_none:
129                    return None
130
131                # If not all primary keys are None, them serialize it and
132                # convert to a base64 dictionary to be used as PK
133                composite_pk_str = pumpJsonDump(composite_pk_dict)
134                return base64.urlsafe_b64encode(
135                    composite_pk_str.encode()).decode()
136
137        # Map object values to other, this is used when builds forenging
138        # key references and request related field using microservice.
139        elif type(primary_keys) is dict:
140            # Will return a None value if all composite primary keys are
141            # None
142            is_all_none = False
143            composite_pk_dict = {}
144            for key, value in primary_keys.items():
145                temp_pk_value = getattr(obj, key)
146                is_all_none = is_all_none or (temp_pk_value is None)
147                composite_pk_dict[value] = temp_pk_value
148            if is_all_none:
149                return None
150
151            # If not all primary keys are None, them serialize it and
152            # convert to a base64 dictionary to be used as PK. Using
153            # dictionary will map values before converting to base64
154            # dictionary
155            composite_pk_str = pumpJsonDump(composite_pk_dict)
156            base64_composite_pk = base64.urlsafe_b64encode(
157                composite_pk_str.encode()).decode()
158            return base64_composite_pk
159
160        # This will raise error if primary_keys type is not implemented
161        else:
162            msg = (
163                "CompositePkBase64Converter.dump argument primary_keys "
164                "is not a list of strings or a map dictionary. Type "
165                "[{arg_type}]").format(arg_type=type(primary_keys))
166            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]:
168    @staticmethod
169    def load(value: Union[str, int]) -> Union[int, dict]:
170        """Convert encoded primary keys to values.
171
172        If the primary key is a string, try to transform it to dictionary
173        decoding json base64 to a dictionary.
174
175        Args:
176            value:
177                Primary key value as an integer or as a base64
178                encoded json dictionary.
179
180        Return:
181            Return the primary key as integer if possible, or try to decoded
182            it to a dictionary from a base64 encoded json.
183        """
184        # Try to convert value to integer
185        try:
186            float_value = float(value)
187            if float_value.is_integer():
188                return int(float_value)
189            else:
190                msg = "[{value}] value is a float, but not integer."
191                raise PumpWoodException(msg, payload={"value": value})
192
193        # If not possible, try to decode a base64 JSON dictionary
194        except Exception as e1:
195            try:
196                return json.loads(base64.b64decode(value))
197            except Exception as e2:
198                msg = (
199                    "[{value}] value is not an integer and could no be "
200                    "decoded as a base64 encoded json dictionary. Value=")
201                raise PumpWoodException(
202                    message=msg, payload={
203                        "value": value,
204                        "exception_int": str(e1),
205                        "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.