##// END OF EJS Templates
jsonalchemy: add de_coerce to reverse serialization of list and dict elements from Mutation objects back to original state.
marcink -
r2397:737f97b5 default
parent child Browse files
Show More
@@ -1,265 +1,274 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import collections
21 import collections
22
22
23 import sqlalchemy
23 import sqlalchemy
24 from sqlalchemy import UnicodeText
24 from sqlalchemy import UnicodeText
25 from sqlalchemy.ext.mutable import Mutable
25 from sqlalchemy.ext.mutable import Mutable
26
26
27 from rhodecode.lib.ext_json import json
27 from rhodecode.lib.ext_json import json
28
28
29
29
30 class JsonRaw(unicode):
30 class JsonRaw(unicode):
31 """
31 """
32 Allows interacting with a JSON types field using a raw string.
32 Allows interacting with a JSON types field using a raw string.
33
33
34 For example::
34 For example::
35 db_instance = JsonTable()
35 db_instance = JsonTable()
36 db_instance.enabled = True
36 db_instance.enabled = True
37 db_instance.json_data = JsonRaw('{"a": 4}')
37 db_instance.json_data = JsonRaw('{"a": 4}')
38
38
39 This will bypass serialization/checks, and allow storing
39 This will bypass serialization/checks, and allow storing
40 raw values
40 raw values
41 """
41 """
42 pass
42 pass
43
43
44
44
45 # Set this to the standard dict if Order is not required
45 # Set this to the standard dict if Order is not required
46 DictClass = collections.OrderedDict
46 DictClass = collections.OrderedDict
47
47
48
48
49 class JSONEncodedObj(sqlalchemy.types.TypeDecorator):
49 class JSONEncodedObj(sqlalchemy.types.TypeDecorator):
50 """
50 """
51 Represents an immutable structure as a json-encoded string.
51 Represents an immutable structure as a json-encoded string.
52
52
53 If default is, for example, a dict, then a NULL value in the
53 If default is, for example, a dict, then a NULL value in the
54 database will be exposed as an empty dict.
54 database will be exposed as an empty dict.
55 """
55 """
56
56
57 impl = UnicodeText
57 impl = UnicodeText
58 safe = True
58 safe = True
59
59
60 def __init__(self, *args, **kwargs):
60 def __init__(self, *args, **kwargs):
61 self.default = kwargs.pop('default', None)
61 self.default = kwargs.pop('default', None)
62 self.safe = kwargs.pop('safe_json', self.safe)
62 self.safe = kwargs.pop('safe_json', self.safe)
63 self.dialect_map = kwargs.pop('dialect_map', {})
63 self.dialect_map = kwargs.pop('dialect_map', {})
64 super(JSONEncodedObj, self).__init__(*args, **kwargs)
64 super(JSONEncodedObj, self).__init__(*args, **kwargs)
65
65
66 def load_dialect_impl(self, dialect):
66 def load_dialect_impl(self, dialect):
67 if dialect.name in self.dialect_map:
67 if dialect.name in self.dialect_map:
68 return dialect.type_descriptor(self.dialect_map[dialect.name])
68 return dialect.type_descriptor(self.dialect_map[dialect.name])
69 return dialect.type_descriptor(self.impl)
69 return dialect.type_descriptor(self.impl)
70
70
71 def process_bind_param(self, value, dialect):
71 def process_bind_param(self, value, dialect):
72 if isinstance(value, JsonRaw):
72 if isinstance(value, JsonRaw):
73 value = value
73 value = value
74 elif value is not None:
74 elif value is not None:
75 value = json.dumps(value)
75 value = json.dumps(value)
76 return value
76 return value
77
77
78 def process_result_value(self, value, dialect):
78 def process_result_value(self, value, dialect):
79 if self.default is not None and (not value or value == '""'):
79 if self.default is not None and (not value or value == '""'):
80 return self.default()
80 return self.default()
81
81
82 if value is not None:
82 if value is not None:
83 try:
83 try:
84 value = json.loads(value, object_pairs_hook=DictClass)
84 value = json.loads(value, object_pairs_hook=DictClass)
85 except Exception as e:
85 except Exception as e:
86 if self.safe and self.default is not None:
86 if self.safe and self.default is not None:
87 return self.default()
87 return self.default()
88 else:
88 else:
89 raise
89 raise
90 return value
90 return value
91
91
92
92
93 class MutationObj(Mutable):
93 class MutationObj(Mutable):
94 @classmethod
94 @classmethod
95 def coerce(cls, key, value):
95 def coerce(cls, key, value):
96 if isinstance(value, dict) and not isinstance(value, MutationDict):
96 if isinstance(value, dict) and not isinstance(value, MutationDict):
97 return MutationDict.coerce(key, value)
97 return MutationDict.coerce(key, value)
98 if isinstance(value, list) and not isinstance(value, MutationList):
98 if isinstance(value, list) and not isinstance(value, MutationList):
99 return MutationList.coerce(key, value)
99 return MutationList.coerce(key, value)
100 return value
100 return value
101
101
102 def de_coerce(self):
103 return self
104
102 @classmethod
105 @classmethod
103 def _listen_on_attribute(cls, attribute, coerce, parent_cls):
106 def _listen_on_attribute(cls, attribute, coerce, parent_cls):
104 key = attribute.key
107 key = attribute.key
105 if parent_cls is not attribute.class_:
108 if parent_cls is not attribute.class_:
106 return
109 return
107
110
108 # rely on "propagate" here
111 # rely on "propagate" here
109 parent_cls = attribute.class_
112 parent_cls = attribute.class_
110
113
111 def load(state, *args):
114 def load(state, *args):
112 val = state.dict.get(key, None)
115 val = state.dict.get(key, None)
113 if coerce:
116 if coerce:
114 val = cls.coerce(key, val)
117 val = cls.coerce(key, val)
115 state.dict[key] = val
118 state.dict[key] = val
116 if isinstance(val, cls):
119 if isinstance(val, cls):
117 val._parents[state.obj()] = key
120 val._parents[state.obj()] = key
118
121
119 def set(target, value, oldvalue, initiator):
122 def set(target, value, oldvalue, initiator):
120 if not isinstance(value, cls):
123 if not isinstance(value, cls):
121 value = cls.coerce(key, value)
124 value = cls.coerce(key, value)
122 if isinstance(value, cls):
125 if isinstance(value, cls):
123 value._parents[target.obj()] = key
126 value._parents[target.obj()] = key
124 if isinstance(oldvalue, cls):
127 if isinstance(oldvalue, cls):
125 oldvalue._parents.pop(target.obj(), None)
128 oldvalue._parents.pop(target.obj(), None)
126 return value
129 return value
127
130
128 def pickle(state, state_dict):
131 def pickle(state, state_dict):
129 val = state.dict.get(key, None)
132 val = state.dict.get(key, None)
130 if isinstance(val, cls):
133 if isinstance(val, cls):
131 if 'ext.mutable.values' not in state_dict:
134 if 'ext.mutable.values' not in state_dict:
132 state_dict['ext.mutable.values'] = []
135 state_dict['ext.mutable.values'] = []
133 state_dict['ext.mutable.values'].append(val)
136 state_dict['ext.mutable.values'].append(val)
134
137
135 def unpickle(state, state_dict):
138 def unpickle(state, state_dict):
136 if 'ext.mutable.values' in state_dict:
139 if 'ext.mutable.values' in state_dict:
137 for val in state_dict['ext.mutable.values']:
140 for val in state_dict['ext.mutable.values']:
138 val._parents[state.obj()] = key
141 val._parents[state.obj()] = key
139
142
140 sqlalchemy.event.listen(parent_cls, 'load', load, raw=True,
143 sqlalchemy.event.listen(parent_cls, 'load', load, raw=True,
141 propagate=True)
144 propagate=True)
142 sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True,
145 sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True,
143 propagate=True)
146 propagate=True)
144 sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True,
147 sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True,
145 propagate=True)
148 propagate=True)
146 sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True,
149 sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True,
147 propagate=True)
150 propagate=True)
148 sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True,
151 sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True,
149 propagate=True)
152 propagate=True)
150
153
151
154
152 class MutationDict(MutationObj, DictClass):
155 class MutationDict(MutationObj, DictClass):
153 @classmethod
156 @classmethod
154 def coerce(cls, key, value):
157 def coerce(cls, key, value):
155 """Convert plain dictionary to MutationDict"""
158 """Convert plain dictionary to MutationDict"""
156 self = MutationDict(
159 self = MutationDict(
157 (k, MutationObj.coerce(key, v)) for (k, v) in value.items())
160 (k, MutationObj.coerce(key, v)) for (k, v) in value.items())
158 self._key = key
161 self._key = key
159 return self
162 return self
160
163
164 def de_coerce(self):
165 return dict(self)
166
161 def __setitem__(self, key, value):
167 def __setitem__(self, key, value):
162 # Due to the way OrderedDict works, this is called during __init__.
168 # Due to the way OrderedDict works, this is called during __init__.
163 # At this time we don't have a key set, but what is more, the value
169 # At this time we don't have a key set, but what is more, the value
164 # being set has already been coerced. So special case this and skip.
170 # being set has already been coerced. So special case this and skip.
165 if hasattr(self, '_key'):
171 if hasattr(self, '_key'):
166 value = MutationObj.coerce(self._key, value)
172 value = MutationObj.coerce(self._key, value)
167 DictClass.__setitem__(self, key, value)
173 DictClass.__setitem__(self, key, value)
168 self.changed()
174 self.changed()
169
175
170 def __delitem__(self, key):
176 def __delitem__(self, key):
171 DictClass.__delitem__(self, key)
177 DictClass.__delitem__(self, key)
172 self.changed()
178 self.changed()
173
179
174 def __setstate__(self, state):
180 def __setstate__(self, state):
175 self.__dict__ = state
181 self.__dict__ = state
176
182
177 def __reduce_ex__(self, proto):
183 def __reduce_ex__(self, proto):
178 # support pickling of MutationDicts
184 # support pickling of MutationDicts
179 d = dict(self)
185 d = dict(self)
180 return (self.__class__, (d, ))
186 return (self.__class__, (d,))
181
187
182
188
183 class MutationList(MutationObj, list):
189 class MutationList(MutationObj, list):
184 @classmethod
190 @classmethod
185 def coerce(cls, key, value):
191 def coerce(cls, key, value):
186 """Convert plain list to MutationList"""
192 """Convert plain list to MutationList"""
187 self = MutationList((MutationObj.coerce(key, v) for v in value))
193 self = MutationList((MutationObj.coerce(key, v) for v in value))
188 self._key = key
194 self._key = key
189 return self
195 return self
190
196
197 def de_coerce(self):
198 return list(self)
199
191 def __setitem__(self, idx, value):
200 def __setitem__(self, idx, value):
192 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
201 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
193 self.changed()
202 self.changed()
194
203
195 def __setslice__(self, start, stop, values):
204 def __setslice__(self, start, stop, values):
196 list.__setslice__(self, start, stop,
205 list.__setslice__(self, start, stop,
197 (MutationObj.coerce(self._key, v) for v in values))
206 (MutationObj.coerce(self._key, v) for v in values))
198 self.changed()
207 self.changed()
199
208
200 def __delitem__(self, idx):
209 def __delitem__(self, idx):
201 list.__delitem__(self, idx)
210 list.__delitem__(self, idx)
202 self.changed()
211 self.changed()
203
212
204 def __delslice__(self, start, stop):
213 def __delslice__(self, start, stop):
205 list.__delslice__(self, start, stop)
214 list.__delslice__(self, start, stop)
206 self.changed()
215 self.changed()
207
216
208 def append(self, value):
217 def append(self, value):
209 list.append(self, MutationObj.coerce(self._key, value))
218 list.append(self, MutationObj.coerce(self._key, value))
210 self.changed()
219 self.changed()
211
220
212 def insert(self, idx, value):
221 def insert(self, idx, value):
213 list.insert(self, idx, MutationObj.coerce(self._key, value))
222 list.insert(self, idx, MutationObj.coerce(self._key, value))
214 self.changed()
223 self.changed()
215
224
216 def extend(self, values):
225 def extend(self, values):
217 list.extend(self, (MutationObj.coerce(self._key, v) for v in values))
226 list.extend(self, (MutationObj.coerce(self._key, v) for v in values))
218 self.changed()
227 self.changed()
219
228
220 def pop(self, *args, **kw):
229 def pop(self, *args, **kw):
221 value = list.pop(self, *args, **kw)
230 value = list.pop(self, *args, **kw)
222 self.changed()
231 self.changed()
223 return value
232 return value
224
233
225 def remove(self, value):
234 def remove(self, value):
226 list.remove(self, value)
235 list.remove(self, value)
227 self.changed()
236 self.changed()
228
237
229
238
230 def JsonType(impl=None, **kwargs):
239 def JsonType(impl=None, **kwargs):
231 """
240 """
232 Helper for using a mutation obj, it allows to use .with_variant easily.
241 Helper for using a mutation obj, it allows to use .with_variant easily.
233 example::
242 example::
234
243
235 settings = Column('settings_json',
244 settings = Column('settings_json',
236 MutationObj.as_mutable(
245 MutationObj.as_mutable(
237 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
246 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
238 """
247 """
239
248
240 if impl == 'list':
249 if impl == 'list':
241 return JSONEncodedObj(default=list, **kwargs)
250 return JSONEncodedObj(default=list, **kwargs)
242 elif impl == 'dict':
251 elif impl == 'dict':
243 return JSONEncodedObj(default=DictClass, **kwargs)
252 return JSONEncodedObj(default=DictClass, **kwargs)
244 else:
253 else:
245 return JSONEncodedObj(**kwargs)
254 return JSONEncodedObj(**kwargs)
246
255
247
256
248 JSON = MutationObj.as_mutable(JsonType())
257 JSON = MutationObj.as_mutable(JsonType())
249 """
258 """
250 A type to encode/decode JSON on the fly
259 A type to encode/decode JSON on the fly
251
260
252 sqltype is the string type for the underlying DB column::
261 sqltype is the string type for the underlying DB column::
253
262
254 Column(JSON) (defaults to UnicodeText)
263 Column(JSON) (defaults to UnicodeText)
255 """
264 """
256
265
257 JSONDict = MutationObj.as_mutable(JsonType('dict'))
266 JSONDict = MutationObj.as_mutable(JsonType('dict'))
258 """
267 """
259 A type to encode/decode JSON dictionaries on the fly
268 A type to encode/decode JSON dictionaries on the fly
260 """
269 """
261
270
262 JSONList = MutationObj.as_mutable(JsonType('list'))
271 JSONList = MutationObj.as_mutable(JsonType('list'))
263 """
272 """
264 A type to encode/decode JSON lists` on the fly
273 A type to encode/decode JSON lists` on the fly
265 """
274 """
General Comments 0
You need to be logged in to leave comments. Login now