##// END OF EJS Templates
integrations: use mutable json settings using json alchemy MutableObject.
dan -
r447:d0466a7c default
parent child Browse files
Show More
@@ -0,0 +1,257 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
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
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import collections
22
23 import sqlalchemy
24 from sqlalchemy import UnicodeText
25 from sqlalchemy.ext.mutable import Mutable
26
27 from rhodecode.lib.ext_json import json
28
29
30 class JsonRaw(unicode):
31 """
32 Allows interacting with a JSON types field using a raw string.
33
34 For example::
35 db_instance = JsonTable()
36 db_instance.enabled = True
37 db_instance.json_data = JsonRaw('{"a": 4}')
38
39 This will bypass serialization/checks, and allow storing
40 raw values
41 """
42 pass
43
44
45 # Set this to the standard dict if Order is not required
46 DictClass = collections.OrderedDict
47
48
49 class JSONEncodedObj(sqlalchemy.types.TypeDecorator):
50 """
51 Represents an immutable structure as a json-encoded string.
52
53 If default is, for example, a dict, then a NULL value in the
54 database will be exposed as an empty dict.
55 """
56
57 impl = UnicodeText
58 safe = True
59
60 def __init__(self, *args, **kwargs):
61 self.default = kwargs.pop('default', None)
62 self.safe = kwargs.pop('safe_json', self.safe)
63 self.dialect_map = kwargs.pop('dialect_map', {})
64 super(JSONEncodedObj, self).__init__(*args, **kwargs)
65
66 def load_dialect_impl(self, dialect):
67 if dialect.name in self.dialect_map:
68 return dialect.type_descriptor(self.dialect_map[dialect.name])
69 return dialect.type_descriptor(self.impl)
70
71 def process_bind_param(self, value, dialect):
72 if isinstance(value, JsonRaw):
73 value = value
74 elif value is not None:
75 value = json.dumps(value)
76 return value
77
78 def process_result_value(self, value, dialect):
79 if self.default is not None and (not value or value == '""'):
80 return self.default()
81
82 if value is not None:
83 try:
84 value = json.loads(value, object_pairs_hook=DictClass)
85 except Exception as e:
86 if self.safe:
87 return self.default()
88 else:
89 raise
90 return value
91
92
93 class MutationObj(Mutable):
94 @classmethod
95 def coerce(cls, key, value):
96 if isinstance(value, dict) and not isinstance(value, MutationDict):
97 return MutationDict.coerce(key, value)
98 if isinstance(value, list) and not isinstance(value, MutationList):
99 return MutationList.coerce(key, value)
100 return value
101
102 @classmethod
103 def _listen_on_attribute(cls, attribute, coerce, parent_cls):
104 key = attribute.key
105 if parent_cls is not attribute.class_:
106 return
107
108 # rely on "propagate" here
109 parent_cls = attribute.class_
110
111 def load(state, *args):
112 val = state.dict.get(key, None)
113 if coerce:
114 val = cls.coerce(key, val)
115 state.dict[key] = val
116 if isinstance(val, cls):
117 val._parents[state.obj()] = key
118
119 def set(target, value, oldvalue, initiator):
120 if not isinstance(value, cls):
121 value = cls.coerce(key, value)
122 if isinstance(value, cls):
123 value._parents[target.obj()] = key
124 if isinstance(oldvalue, cls):
125 oldvalue._parents.pop(target.obj(), None)
126 return value
127
128 def pickle(state, state_dict):
129 val = state.dict.get(key, None)
130 if isinstance(val, cls):
131 if 'ext.mutable.values' not in state_dict:
132 state_dict['ext.mutable.values'] = []
133 state_dict['ext.mutable.values'].append(val)
134
135 def unpickle(state, state_dict):
136 if 'ext.mutable.values' in state_dict:
137 for val in state_dict['ext.mutable.values']:
138 val._parents[state.obj()] = key
139
140 sqlalchemy.event.listen(parent_cls, 'load', load, raw=True,
141 propagate=True)
142 sqlalchemy.event.listen(parent_cls, 'refresh', load, raw=True,
143 propagate=True)
144 sqlalchemy.event.listen(parent_cls, 'pickle', pickle, raw=True,
145 propagate=True)
146 sqlalchemy.event.listen(attribute, 'set', set, raw=True, retval=True,
147 propagate=True)
148 sqlalchemy.event.listen(parent_cls, 'unpickle', unpickle, raw=True,
149 propagate=True)
150
151
152 class MutationDict(MutationObj, DictClass):
153 @classmethod
154 def coerce(cls, key, value):
155 """Convert plain dictionary to MutationDict"""
156 self = MutationDict(
157 (k, MutationObj.coerce(key, v)) for (k, v) in value.items())
158 self._key = key
159 return self
160
161 def __setitem__(self, key, value):
162 # 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
164 # being set has already been coerced. So special case this and skip.
165 if hasattr(self, '_key'):
166 value = MutationObj.coerce(self._key, value)
167 DictClass.__setitem__(self, key, value)
168 self.changed()
169
170 def __delitem__(self, key):
171 DictClass.__delitem__(self, key)
172 self.changed()
173
174
175 class MutationList(MutationObj, list):
176 @classmethod
177 def coerce(cls, key, value):
178 """Convert plain list to MutationList"""
179 self = MutationList((MutationObj.coerce(key, v) for v in value))
180 self._key = key
181 return self
182
183 def __setitem__(self, idx, value):
184 list.__setitem__(self, idx, MutationObj.coerce(self._key, value))
185 self.changed()
186
187 def __setslice__(self, start, stop, values):
188 list.__setslice__(self, start, stop,
189 (MutationObj.coerce(self._key, v) for v in values))
190 self.changed()
191
192 def __delitem__(self, idx):
193 list.__delitem__(self, idx)
194 self.changed()
195
196 def __delslice__(self, start, stop):
197 list.__delslice__(self, start, stop)
198 self.changed()
199
200 def append(self, value):
201 list.append(self, MutationObj.coerce(self._key, value))
202 self.changed()
203
204 def insert(self, idx, value):
205 list.insert(self, idx, MutationObj.coerce(self._key, value))
206 self.changed()
207
208 def extend(self, values):
209 list.extend(self, (MutationObj.coerce(self._key, v) for v in values))
210 self.changed()
211
212 def pop(self, *args, **kw):
213 value = list.pop(self, *args, **kw)
214 self.changed()
215 return value
216
217 def remove(self, value):
218 list.remove(self, value)
219 self.changed()
220
221
222 def JsonType(impl=None, **kwargs):
223 """
224 Helper for using a mutation obj, it allows to use .with_variant easily.
225 example::
226
227 settings = Column('settings_json',
228 MutationObj.as_mutable(
229 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
230 """
231
232 if impl == 'list':
233 return JSONEncodedObj(default=list, **kwargs)
234 elif impl == 'dict':
235 return JSONEncodedObj(default=DictClass, **kwargs)
236 else:
237 return JSONEncodedObj(**kwargs)
238
239
240 JSON = MutationObj.as_mutable(JsonType())
241 """
242 A type to encode/decode JSON on the fly
243
244 sqltype is the string type for the underlying DB column::
245
246 Column(JSON) (defaults to UnicodeText)
247 """
248
249 JSONDict = MutationObj.as_mutable(JsonType('dict'))
250 """
251 A type to encode/decode JSON dictionaries on the fly
252 """
253
254 JSONList = MutationObj.as_mutable(JsonType('list'))
255 """
256 A type to encode/decode JSON lists` on the fly
257 """
@@ -57,6 +57,7 b' from rhodecode.lib.vcs.backends.base imp'
57 from rhodecode.lib.utils2 import (
57 from rhodecode.lib.utils2 import (
58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
60 from rhodecode.lib.ext_json import json
61 from rhodecode.lib.ext_json import json
61 from rhodecode.lib.caching_query import FromCache
62 from rhodecode.lib.caching_query import FromCache
62 from rhodecode.lib.encrypt import AESCipher
63 from rhodecode.lib.encrypt import AESCipher
@@ -3488,12 +3489,14 b' class Integration(Base, BaseModel):'
3488
3489
3489 integration_id = Column('integration_id', Integer(), primary_key=True)
3490 integration_id = Column('integration_id', Integer(), primary_key=True)
3490 integration_type = Column('integration_type', String(255))
3491 integration_type = Column('integration_type', String(255))
3491 enabled = Column("enabled", Boolean(), nullable=False)
3492 enabled = Column('enabled', Boolean(), nullable=False)
3492 name = Column('name', String(255), nullable=False)
3493 name = Column('name', String(255), nullable=False)
3493 settings_json = Column('settings_json',
3494
3494 UnicodeText().with_variant(UnicodeText(16384), 'mysql'))
3495 settings = Column(
3496 'settings_json', MutationObj.as_mutable(
3497 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3495 repo_id = Column(
3498 repo_id = Column(
3496 "repo_id", Integer(), ForeignKey('repositories.repo_id'),
3499 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3497 nullable=True, unique=None, default=None)
3500 nullable=True, unique=None, default=None)
3498 repo = relationship('Repository', lazy='joined')
3501 repo = relationship('Repository', lazy='joined')
3499
3502
@@ -3502,15 +3505,6 b' class Integration(Base, BaseModel):'
3502 self.settings = settings
3505 self.settings = settings
3503 super(Integration, self).__init__(**kw)
3506 super(Integration, self).__init__(**kw)
3504
3507
3505 @hybrid_property
3506 def settings(self):
3507 data = json.loads(self.settings_json or '{}')
3508 return data
3509
3510 @settings.setter
3511 def settings(self, dct):
3512 self.settings_json = json.dumps(dct, indent=2)
3513
3514 def __repr__(self):
3508 def __repr__(self):
3515 if self.repo:
3509 if self.repo:
3516 scope = 'repo=%r' % self.repo
3510 scope = 'repo=%r' % self.repo
General Comments 0
You need to be logged in to leave comments. Login now