Completed
Pull Request — master (#694)
by Eric
02:14
created

SerializationFacade   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 128
Duplicated Lines 0 %

Importance

Changes 4
Bugs 1 Features 1
Metric Value
c 4
b 1
f 1
dl 0
loc 128
rs 10
wmc 21

13 Methods

Rating   Name   Duplication   Size   Complexity  
A deserialize() 0 16 2
A __encode() 0 6 1
A get_type() 0 5 1
A __init__() 0 9 3
A serialize() 0 11 1
A __check_serializer_api() 0 7 2
A __get_last_serialized_part() 0 8 2
A __decode() 0 6 1
A __get_serializer() 0 11 2
A __get_type_pattern() 0 5 1
A use() 0 8 1
A __check_type() 0 8 2
A register() 0 11 2
1
# -*- coding: utf-8 -*-
2
3
# from __future__ import unicode_literals
4
5
import json
6
import inspect
7
import re
8
import pickle
9
import base64
10
import sys
11
import os
12
13
from abc import ABCMeta, abstractmethod
14
from cookiecutter.exceptions import UnknownSerializerType, \
15
    BadSerializedStringFormat, InvalidSerializerType, InvalidType
16
17
18
def get_context():
19
    """
20
    high level context provider API to be used by hooks
21
    it reads serialized context from stdin
22
    """
23
    serializer = get_persistent(SerializationFacade)
24
    if not serializer:
25
        serializer = SerializationFacade()
26
    return serializer.deserialize(sys.stdin.readlines()[0])
27
28
29
def put_context(context):
30
    """
31
    high level context updater API to be used by hooks
32
    it serializes a given context object
33
    :param context: context dictionary
34
    """
35
    serializer = get_persistent(SerializationFacade)
36
    if not serializer:
37
        serializer = SerializationFacade()
38
    print(serializer.serialize(context))
39
40
41
def make_persistent(subject):
42
    """
43
    make a given subject persistent between processes
44
    :param subject: subject to persist
45
    """
46
    serializer = SerializationFacade().use('pickle')
47
    id = subject.__class__.__name__
48
    os.environ[id] = serializer.serialize(subject)
49
50
51
def get_persistent(klass):
52
    """
53
    get a persistent subject from a given class
54
    :param klass: subject klass to retrieve if it is persistent
55
    """
56
    id = klass.__name__
57
    subject = None
58
    if id in os.environ:
59
        serializer = SerializationFacade().use('pickle')
60
        subject = serializer.deserialize(os.environ[id])
61
62
    return subject
63
64
65
def remove_persisent(klass):
66
    """
67
    remove a persistent subject record
68
    :param klass: subject klass to remove
69
    """
70
    id = klass.__name__
71
    if id in os.environ:
72
        del(os.environ[id])
73
74
75
class AbstractSerializer(object):
76
    """
77
    abstract base class for serializers
78
    """
79
    __metaclass__ = ABCMeta
80
81
    def serialize(self, subject):
82
        """
83
        serialize a given subject to its bytes string representation
84
        :param subject: the subject to serialize
85
        """
86
        serialized = self._do_serialize(subject)
87
88
        return base64.encodestring(self.__encode(serialized))
89
90
    def deserialize(self, bstring):
91
        """
92
        deserialize a given bytes string to its Python object
93
        :param bstring: the bytes string to deserialize
94
        """
95
        serialized = base64.decodestring(bstring)
96
97
        return self._do_deserialize(self.__decode(serialized))
98
99
    @abstractmethod
100
    def _do_serialize(self, subject):
101
        """
102
        abstract method to be implemented by serializer
103
        it should do the serialization
104
        :param subject: the subject to serialize
105
        """
106
107
    @abstractmethod
108
    def _do_deserialize(self, string):
109
        """
110
        abstract method to be implemented by serializer
111
        it should do the deserialization
112
        :param string: the string to deserialize
113
        """
114
115
    def __encode(self, subject):
116
        """
117
        sanitize encoding for a given subject, if needed
118
        :param subject: the subject to treat
119
        """
120
        try:
121
            sanitized = subject.encode()
122
        except:
123
            sanitized = subject
124
125
        return sanitized
126
127
    def __decode(self, subject):
128
        """
129
        revert encoding sanitization for a given subject, if it has been done
130
        previously
131
        :param subject: the subject to treat
132
        """
133
        try:
134
            original = subject.decode()
135
        except:
136
            original = subject
137
138
        return original
139
140
141
class JsonSerializer(AbstractSerializer):
142
    """
143
    The JSON serializer is the default serializer registered by the
144
    serialization facade
145
    It can serve as an example of the API that must implement custom
146
    serializers
147
    """
148
149
    def _do_serialize(self, subject):
150
        """
151
        serialize a given subject to its JSON representation
152
        :param subject: the subject to serialize
153
        """
154
        return json.dumps(subject)
155
156
    def _do_deserialize(self, string):
157
        """
158
        deserialize a given JSON string to its Python object
159
        :param string: the string to deserialize
160
        """
161
        return json.loads(string)
162
163
164
class PickleSerializer(AbstractSerializer):
165
    """
166
    The Pickle serializer should be used to serialize objects
167
    """
168
169
    def _do_serialize(self, subject):
170
        """
171
        serialize a given subject to its string representation
172
        :param subject: the subject to serialize
173
        """
174
        return pickle.dumps(subject, 2)
175
176
    def _do_deserialize(self, string):
177
        """
178
        deserialize a given string to its Python object
179
        :param string: the string to deserialize
180
        """
181
        return pickle.loads(string)
182
183
184
class SerializationFacade(object):
185
    """
186
    The SerializationFacade is the public API that customers should use.
187
    """
188
189
    def __init__(self, serializers=None):
190
        self.__serializers = {}
191
        self.__current_type = 'json'
192
        self.register('json', JsonSerializer)
193
        self.register('pickle', PickleSerializer)
194
195
        if serializers is not None:
196
            for type in serializers:
197
                self.register(type, serializers[type])
198
199
    def serialize(self, subject):
200
        """
201
        serialize a given subject using a specific type (JSON by default)
202
        :param subject: the subject to serialize
203
        :param type: the serializer type to use
204
        """
205
        return self.__current_type + '|' \
206
            + self.__decode(
207
                self.__get_serializer(
208
                    self.__current_type).serialize(subject)) \
209
            + '$'
210
211
    def deserialize(self, string):
212
        """
213
        deserialize a given string which must be of the form:
214
        serializer_type|serialized_object
215
        :param string: the string to deserialize
216
        """
217
        parts = self.__get_last_serialized_part(string).split('|')
218
219
        if len(parts) < 2:
220
            raise BadSerializedStringFormat(
221
                message='Serialized string should be of the form '
222
                'serializer_type|serialized_string$'
223
            )
224
225
        return self.__get_serializer(parts[0]).deserialize(
226
            self.__encode(parts[1]))
227
228
    def use(self, type):
229
        """
230
        define the type of the serializer to use
231
        :param type: the serializer type
232
        """
233
        self.__current_type = type
234
235
        return self
236
237
    def get_type(self):
238
        """
239
        get the type of the current serializer
240
        """
241
        return self.__current_type
242
243
    def register(self, type, serializer):
244
        """
245
        register a custom serializer
246
        :param type: type of the serializer to register
247
        :param serializer: custom serializer to register
248
        """
249
        self.__check_type(type)
250
        oserializer = serializer() if inspect.isclass(
251
            serializer) else serializer
252
        self.__check_serializer_api(oserializer)
253
        self.__serializers[type] = oserializer
254
255
    def __get_type_pattern(self):
256
        """
257
        get the validation pattern for serializer types
258
        """
259
        return '[a-zA-Z_][a-zA-Z_][a-zA-Z_0-9\-\.]+'
260
261
    def __get_serializer(self, type):
262
        """
263
        get the serializer of a given type
264
        :param type: type of the target serializer
265
        """
266
        if type in self.__serializers:
267
            self.__current_type = type
268
            return self.__serializers[type]
269
270
        else:
271
            raise UnknownSerializerType(type)
272
273
    def __check_serializer_api(self, serializer):
274
        """
275
        ensure that a given serializer implements the expected API
276
        :param serializer: serializer to check its public interface
277
        """
278
        if not isinstance(serializer, AbstractSerializer):
279
            raise InvalidType(AbstractSerializer.__name__)
280
281
    def __check_type(self, type):
282
        """
283
        ensure a given type is well formed and does not contain invalid chars
284
        :param type: the type to check
285
        """
286
        pattern = '^' + self.__get_type_pattern() + '$'
287
        if not re.match(pattern, type):
288
            raise InvalidSerializerType()
289
290
    def __get_last_serialized_part(self, string):
291
        """
292
        extract the last serialized part found in a mixed string
293
        """
294
        pattern = self.__get_type_pattern() + '\|[^\$]+'
295
        serialized_parts = re.findall(pattern, string)
296
297
        return serialized_parts[-1] if serialized_parts else string
298
299
    def __decode(self, serialized):
300
        """
301
        prepare the serialized string for crossprocessing
302
        :param serialized: serialized string to treat
303
        """
304
        return serialized.decode().replace("\n", "*")
305
306
    def __encode(self, encoded):
307
        """
308
        prepare the encoded string for deserialization
309
        :param encoded: encoded string to treat
310
        """
311
        return encoded.replace("*", "\n").encode()
312