File: serializer.py

package info (click to toggle)
python-param 2.1.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,048 kB
  • sloc: python: 17,980; makefile: 3
file content (328 lines) | stat: -rw-r--r-- 11,530 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
"""
Classes used to support string serialization of Parameters and
Parameterized objects.
"""

import json
import textwrap

class UnserializableException(Exception):
    pass

class UnsafeserializableException(Exception):
    pass

def JSONNullable(json_type):
    "Express a JSON schema type as nullable to easily support Parameters that allow_None"
    return {'anyOf': [ json_type, {'type': 'null'}] }



class Serialization:
    """
    Base class used to implement different types of serialization.
    """

    @classmethod
    def schema(cls, pobj, subset=None):
        raise NotImplementedError        # noqa: unimplemented method

    @classmethod
    def serialize_parameters(cls, pobj, subset=None):
        """
        Serialize the parameters on a Parameterized object into a
        single serialized object, e.g. a JSON string.
        """
        raise NotImplementedError        # noqa: unimplemented method

    @classmethod
    def deserialize_parameters(cls, pobj, serialized, subset=None):
        """
        Deserialize a serialized object representing one or
        more Parameters into a dictionary of parameter values.
        """
        raise NotImplementedError        # noqa: unimplemented method

    @classmethod
    def serialize_parameter_value(cls, pobj, pname):
        """
        Serialize a single parameter value.
        """
        raise NotImplementedError        # noqa: unimplemented method

    @classmethod
    def deserialize_parameter_value(cls, pobj, pname, value):
        """
        Deserialize a single parameter value.
        """
        raise NotImplementedError        # noqa: unimplemented method


class JSONSerialization(Serialization):
    """
    Class responsible for specifying JSON serialization, deserialization
    and JSON schemas for Parameters and Parameterized classes and
    objects.
    """

    unserializable_parameter_types = ['Callable']

    json_schema_literal_types = {
        int:'integer', float:'number', str:'string',
        type(None): 'null'
    }

    @classmethod
    def loads(cls, serialized):
        return json.loads(serialized)

    @classmethod
    def dumps(cls, obj):
        return json.dumps(obj)

    @classmethod
    def schema(cls, pobj, safe=False, subset=None):
        schema = {}
        for name, p in pobj.param.objects('existing').items():
            if subset is not None and name not in subset:
                continue
            schema[name] = p.schema(safe=safe)
            if p.doc:
                schema[name]['description'] = textwrap.dedent(p.doc).replace('\n', ' ').strip()
            if p.label:
                schema[name]['title'] = p.label
        return schema

    @classmethod
    def serialize_parameters(cls, pobj, subset=None):
        components = {}
        for name, p in pobj.param.objects('existing').items():
            if subset is not None and name not in subset:
                continue
            value = pobj.param.get_value_generator(name)
            components[name] = p.serialize(value)
        return cls.dumps(components)

    @classmethod
    def deserialize_parameters(cls, pobj, serialization, subset=None):
        deserialized = cls.loads(serialization)
        components = {}
        for name, value in deserialized.items():
            if subset is not None and name not in subset:
                continue
            deserialized = pobj.param[name].deserialize(value)
            components[name] = deserialized
        return components

    # Parameter level methods

    @classmethod
    def _get_method(cls, ptype, suffix):
        "Returns specialized method if available, otherwise None"
        method_name = ptype.lower()+'_' + suffix
        return getattr(cls, method_name, None)

    @classmethod
    def param_schema(cls, ptype, p, safe=False, subset=None):
        if ptype in cls.unserializable_parameter_types:
            raise UnserializableException
        dispatch_method = cls._get_method(ptype, 'schema')
        if dispatch_method:
            schema = dispatch_method(p, safe=safe)
        else:
            schema = {'type': ptype.lower()}
        return JSONNullable(schema) if p.allow_None else schema

    @classmethod
    def serialize_parameter_value(cls, pobj, pname):
        value = pobj.param.get_value_generator(pname)
        return cls.dumps(pobj.param[pname].serialize(value))

    @classmethod
    def deserialize_parameter_value(cls, pobj, pname, value):
        value = cls.loads(value)
        return pobj.param[pname].deserialize(value)

    # Custom Schemas

    @classmethod
    def class__schema(cls, class_, safe=False):
        from .parameterized import Parameterized
        if isinstance(class_, tuple):
            return {'anyOf': [cls.class__schema(cls_) for cls_ in class_]}
        elif class_ in cls.json_schema_literal_types:
            return {'type': cls.json_schema_literal_types[class_]}
        elif issubclass(class_, Parameterized):
            return {'type': 'object', 'properties': class_.param.schema(safe)}
        else:
            return {'type': 'object'}

    @classmethod
    def array_schema(cls, p, safe=False):
        if safe is True:
            msg = ('Array is not guaranteed to be safe for '
                   'serialization as the dtype is unknown')
            raise UnsafeserializableException(msg)
        return {'type': 'array'}

    @classmethod
    def classselector_schema(cls, p, safe=False):
        return cls.class__schema(p.class_, safe=safe)

    @classmethod
    def dict_schema(cls, p, safe=False):
        if safe is True:
            msg = ('Dict is not guaranteed to be safe for '
                   'serialization as the key and value types are unknown')
            raise UnsafeserializableException(msg)
        return {'type': 'object'}

    @classmethod
    def date_schema(cls, p, safe=False):
        return {'type': 'string', 'format': 'date-time'}

    @classmethod
    def calendardate_schema(cls, p, safe=False):
        return {'type': 'string', 'format': 'date'}

    @classmethod
    def tuple_schema(cls, p, safe=False):
        schema = {'type': 'array'}
        if p.length is not None:
            schema['minItems'] =  p.length
            schema['maxItems'] =  p.length
        return schema

    @classmethod
    def number_schema(cls, p, safe=False):
        schema = {'type': p.__class__.__name__.lower() }
        return cls.declare_numeric_bounds(schema, p.bounds, p.inclusive_bounds)

    @classmethod
    def declare_numeric_bounds(cls, schema, bounds, inclusive_bounds):
        "Given an applicable numeric schema, augment with bounds information"
        if bounds is not None:
            (low, high) = bounds
            if low is not None:
                key = 'minimum' if inclusive_bounds[0] else 'exclusiveMinimum'
                schema[key] = low
            if high is not None:
                key = 'maximum' if inclusive_bounds[1] else 'exclusiveMaximum'
                schema[key] = high
        return schema

    @classmethod
    def integer_schema(cls, p, safe=False):
        return cls.number_schema(p)

    @classmethod
    def numerictuple_schema(cls, p, safe=False):
        schema = cls.tuple_schema(p, safe=safe)
        schema['additionalItems'] = {'type': 'number'}
        return schema

    @classmethod
    def xycoordinates_schema(cls, p, safe=False):
        return cls.numerictuple_schema(p, safe=safe)

    @classmethod
    def range_schema(cls, p, safe=False):
        schema = cls.tuple_schema(p, safe=safe)
        bounded_number = cls.declare_numeric_bounds(
            {'type': 'number'}, p.bounds, p.inclusive_bounds)
        schema['additionalItems'] = bounded_number
        return schema

    @classmethod
    def list_schema(cls, p, safe=False):
        schema = {'type': 'array'}
        if safe is True and p.item_type is None:
            msg = ('List without a class specified cannot be guaranteed '
                   'to be safe for serialization')
            raise UnsafeserializableException(msg)
        if p.class_ is not None:
            schema['items'] = cls.class__schema(p.item_type, safe=safe)
        return schema

    @classmethod
    def objectselector_schema(cls, p, safe=False):
        try:
            allowed_types = [{'type': cls.json_schema_literal_types[type(obj)]}
                             for obj in p.objects]
            schema = {'anyOf': allowed_types}
            schema['enum'] = p.objects
            return schema
        except:
            if safe is True:
                msg = ('ObjectSelector cannot be guaranteed to be safe for '
                       'serialization due to unserializable type in objects')
                raise UnsafeserializableException(msg)
            return {}

    @classmethod
    def selector_schema(cls, p, safe=False):
        try:
            allowed_types = [{'type': cls.json_schema_literal_types[type(obj)]}
                             for obj in p.objects.values()]
            schema = {'anyOf': allowed_types}
            schema['enum'] = p.objects
            return schema
        except:
            if safe is True:
                msg = ('Selector cannot be guaranteed to be safe for '
                       'serialization due to unserializable type in objects')
                raise UnsafeserializableException(msg)
            return {}

    @classmethod
    def listselector_schema(cls, p, safe=False):
        if p.objects is None:
            if safe is True:
                msg = ('ListSelector cannot be guaranteed to be safe for '
                       'serialization as allowed objects unspecified')
            return {'type': 'array'}
        for obj in p.objects:
            if type(obj) not in cls.json_schema_literal_types:
                msg = 'ListSelector cannot serialize type %s' % type(obj)
                raise UnserializableException(msg)
        return {'type': 'array', 'items': {'enum': p.objects}}

    @classmethod
    def dataframe_schema(cls, p, safe=False):
        schema = {'type': 'array'}
        if safe is True:
            msg = ('DataFrame is not guaranteed to be safe for '
                   'serialization as the column dtypes are unknown')
            raise UnsafeserializableException(msg)
        if p.columns is None:
            schema['items'] = {'type': 'object'}
            return schema

        mincols, maxcols = None, None
        if isinstance(p.columns, int):
            mincols, maxcols = p.columns, p.columns
        elif isinstance(p.columns, tuple):
            mincols, maxcols = p.columns

        if isinstance(p.columns, int) or isinstance(p.columns, tuple):
            schema['items'] =  {'type': 'object', 'minItems': mincols,
                                'maxItems': maxcols}

        if isinstance(p.columns, list) or isinstance(p.columns, set):
            literal_types = [{'type':el} for el in cls.json_schema_literal_types.values()]
            allowable_types = {'anyOf': literal_types}
            properties = {name: allowable_types for name in p.columns}
            schema['items'] =  {'type': 'object', 'properties': properties}

        minrows, maxrows = None, None
        if isinstance(p.rows, int):
            minrows, maxrows = p.rows, p.rows
        elif isinstance(p.rows, tuple):
            minrows, maxrows = p.rows

        if minrows is not None:
            schema['minItems'] = minrows
        if maxrows is not None:
            schema['maxItems'] = maxrows

        return schema