1 |
|
# -*- coding: utf-8 -*- |
2 |
|
|
3 |
|
# Copyright 2018 Telefonica S.A. |
4 |
|
# |
5 |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
6 |
|
# you may not use this file except in compliance with the License. |
7 |
|
# You may obtain a copy of the License at |
8 |
|
# |
9 |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
10 |
|
# |
11 |
|
# Unless required by applicable law or agreed to in writing, software |
12 |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
13 |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
14 |
|
# implied. |
15 |
|
# See the License for the specific language governing permissions and |
16 |
|
# limitations under the License. |
17 |
|
|
18 |
1 |
import yaml |
19 |
1 |
import logging |
20 |
1 |
import re |
21 |
1 |
from http import HTTPStatus |
22 |
1 |
from copy import deepcopy |
23 |
1 |
from Crypto.Cipher import AES |
24 |
1 |
from base64 import b64decode, b64encode |
25 |
1 |
from osm_common.common_utils import FakeLock |
26 |
1 |
from threading import Lock |
27 |
|
|
28 |
1 |
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>" |
29 |
|
|
30 |
|
|
31 |
1 |
class DbException(Exception): |
32 |
|
|
33 |
1 |
def __init__(self, message, http_code=HTTPStatus.NOT_FOUND): |
34 |
1 |
self.http_code = http_code |
35 |
1 |
Exception.__init__(self, "database exception " + str(message)) |
36 |
|
|
37 |
|
|
38 |
1 |
class DbBase(object): |
39 |
|
|
40 |
1 |
def __init__(self, logger_name='db', lock=False): |
41 |
|
""" |
42 |
|
Constructor of dbBase |
43 |
|
:param logger_name: logging name |
44 |
|
:param lock: Used to protect simultaneous access to the same instance class by several threads: |
45 |
|
False, None: Do not protect, this object will only be accessed by one thread |
46 |
|
True: This object needs to be protected by several threads accessing. |
47 |
|
Lock object. Use thi Lock for the threads access protection |
48 |
|
""" |
49 |
1 |
self.logger = logging.getLogger(logger_name) |
50 |
1 |
self.secret_key = None # 32 bytes length array used for encrypt/decrypt |
51 |
1 |
if not lock: |
52 |
1 |
self.lock = FakeLock() |
53 |
0 |
elif lock is True: |
54 |
0 |
self.lock = Lock() |
55 |
0 |
elif isinstance(lock, Lock): |
56 |
0 |
self.lock = lock |
57 |
|
else: |
58 |
0 |
raise ValueError("lock parameter must be a Lock classclass or boolean") |
59 |
|
|
60 |
1 |
def db_connect(self, config, target_version=None): |
61 |
|
""" |
62 |
|
Connect to database |
63 |
|
:param config: Configuration of database. Contains among others: |
64 |
|
host: database host (mandatory) |
65 |
|
port: database port (mandatory) |
66 |
|
name: database name (mandatory) |
67 |
|
user: database username |
68 |
|
password: database password |
69 |
|
commonkey: common OSM key used for sensible information encryption |
70 |
|
materpassword: same as commonkey, for backward compatibility. Deprecated, to be removed in the future |
71 |
|
:param target_version: if provided it checks if database contains required version, raising exception otherwise. |
72 |
|
:return: None or raises DbException on error |
73 |
|
""" |
74 |
0 |
raise DbException("Method 'db_connect' not implemented") |
75 |
|
|
76 |
1 |
def db_disconnect(self): |
77 |
|
""" |
78 |
|
Disconnect from database |
79 |
|
:return: None |
80 |
|
""" |
81 |
0 |
pass |
82 |
|
|
83 |
1 |
def get_list(self, table, q_filter=None): |
84 |
|
""" |
85 |
|
Obtain a list of entries matching q_filter |
86 |
|
:param table: collection or table |
87 |
|
:param q_filter: Filter |
88 |
|
:return: a list (can be empty) with the found entries. Raises DbException on error |
89 |
|
""" |
90 |
0 |
raise DbException("Method 'get_list' not implemented") |
91 |
|
|
92 |
1 |
def count(self, table, q_filter=None): |
93 |
|
""" |
94 |
|
Count the number of entries matching q_filter |
95 |
|
:param table: collection or table |
96 |
|
:param q_filter: Filter |
97 |
|
:return: number of entries found (can be zero) |
98 |
|
:raise: DbException on error |
99 |
|
""" |
100 |
0 |
raise DbException("Method 'count' not implemented") |
101 |
|
|
102 |
1 |
def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True): |
103 |
|
""" |
104 |
|
Obtain one entry matching q_filter |
105 |
|
:param table: collection or table |
106 |
|
:param q_filter: Filter |
107 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
108 |
|
it raises a DbException |
109 |
|
:param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so |
110 |
|
that it raises a DbException |
111 |
|
:return: The requested element, or None |
112 |
|
""" |
113 |
0 |
raise DbException("Method 'get_one' not implemented") |
114 |
|
|
115 |
1 |
def del_list(self, table, q_filter=None): |
116 |
|
""" |
117 |
|
Deletes all entries that match q_filter |
118 |
|
:param table: collection or table |
119 |
|
:param q_filter: Filter |
120 |
|
:return: Dict with the number of entries deleted |
121 |
|
""" |
122 |
0 |
raise DbException("Method 'del_list' not implemented") |
123 |
|
|
124 |
1 |
def del_one(self, table, q_filter=None, fail_on_empty=True): |
125 |
|
""" |
126 |
|
Deletes one entry that matches q_filter |
127 |
|
:param table: collection or table |
128 |
|
:param q_filter: Filter |
129 |
|
:param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in |
130 |
|
which case it raises a DbException |
131 |
|
:return: Dict with the number of entries deleted |
132 |
|
""" |
133 |
0 |
raise DbException("Method 'del_one' not implemented") |
134 |
|
|
135 |
1 |
def create(self, table, indata): |
136 |
|
""" |
137 |
|
Add a new entry at database |
138 |
|
:param table: collection or table |
139 |
|
:param indata: content to be added |
140 |
|
:return: database '_id' of the inserted element. Raises a DbException on error |
141 |
|
""" |
142 |
0 |
raise DbException("Method 'create' not implemented") |
143 |
|
|
144 |
1 |
def create_list(self, table, indata_list): |
145 |
|
""" |
146 |
|
Add several entries at once |
147 |
|
:param table: collection or table |
148 |
|
:param indata_list: list of elements to insert. Each element must be a dictionary. |
149 |
|
An '_id' key based on random uuid is added at each element if missing |
150 |
|
:return: list of inserted '_id's. Exception on error |
151 |
|
""" |
152 |
0 |
raise DbException("Method 'create_list' not implemented") |
153 |
|
|
154 |
1 |
def set_one(self, table, q_filter, update_dict, fail_on_empty=True, unset=None, pull=None, push=None, |
155 |
|
push_list=None, pull_list=None): |
156 |
|
""" |
157 |
|
Modifies an entry at database |
158 |
|
:param table: collection or table |
159 |
|
:param q_filter: Filter |
160 |
|
:param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value |
161 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
162 |
|
it raises a DbException |
163 |
|
:param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is |
164 |
|
ignored. If not exist, it is ignored |
165 |
|
:param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value |
166 |
|
if exist in the array is removed. If not exist, it is ignored |
167 |
|
:param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value |
168 |
|
is appended to the end of the array |
169 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
170 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
171 |
|
whole array |
172 |
|
:return: Dict with the number of entries modified. None if no matching is found. |
173 |
|
""" |
174 |
0 |
raise DbException("Method 'set_one' not implemented") |
175 |
|
|
176 |
1 |
def set_list(self, table, q_filter, update_dict, unset=None, pull=None, push=None, push_list=None, pull_list=None): |
177 |
|
""" |
178 |
|
Modifies al matching entries at database |
179 |
|
:param table: collection or table |
180 |
|
:param q_filter: Filter |
181 |
|
:param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value |
182 |
|
:param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is |
183 |
|
ignored. If not exist, it is ignored |
184 |
|
:param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value |
185 |
|
if exist in the array is removed. If not exist, it is ignored |
186 |
|
:param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value |
187 |
|
is appended to the end of the array |
188 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
189 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
190 |
|
whole array |
191 |
|
:return: Dict with the number of entries modified |
192 |
|
""" |
193 |
0 |
raise DbException("Method 'set_list' not implemented") |
194 |
|
|
195 |
1 |
def replace(self, table, _id, indata, fail_on_empty=True): |
196 |
|
""" |
197 |
|
Replace the content of an entry |
198 |
|
:param table: collection or table |
199 |
|
:param _id: internal database id |
200 |
|
:param indata: content to replace |
201 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
202 |
|
it raises a DbException |
203 |
|
:return: Dict with the number of entries replaced |
204 |
|
""" |
205 |
0 |
raise DbException("Method 'replace' not implemented") |
206 |
|
|
207 |
1 |
def _join_secret_key(self, update_key): |
208 |
|
""" |
209 |
|
Returns a xor byte combination of the internal secret_key and the provided update_key. |
210 |
|
It does not modify the internal secret_key. Used for adding salt, join keys, etc. |
211 |
|
:param update_key: Can be a string, byte or None. Recommended a long one (e.g. 32 byte length) |
212 |
|
:return: joined key in bytes with a 32 bytes length. Can be None if both internal secret_key and update_key |
213 |
|
are None |
214 |
|
""" |
215 |
1 |
if not update_key: |
216 |
1 |
return self.secret_key |
217 |
1 |
elif isinstance(update_key, str): |
218 |
1 |
update_key_bytes = update_key.encode() |
219 |
|
else: |
220 |
1 |
update_key_bytes = update_key |
221 |
|
|
222 |
1 |
new_secret_key = bytearray(self.secret_key) if self.secret_key else bytearray(32) |
223 |
1 |
for i, b in enumerate(update_key_bytes): |
224 |
1 |
new_secret_key[i % 32] ^= b |
225 |
1 |
return bytes(new_secret_key) |
226 |
|
|
227 |
1 |
def set_secret_key(self, new_secret_key, replace=False): |
228 |
|
""" |
229 |
|
Updates internal secret_key used for encryption, with a byte xor |
230 |
|
:param new_secret_key: string or byte array. It is recommended a 32 byte length |
231 |
|
:param replace: if True, old value of internal secret_key is ignored and replaced. If false, a byte xor is used |
232 |
|
:return: None |
233 |
|
""" |
234 |
1 |
if replace: |
235 |
1 |
self.secret_key = None |
236 |
1 |
self.secret_key = self._join_secret_key(new_secret_key) |
237 |
|
|
238 |
1 |
def get_secret_key(self): |
239 |
|
""" |
240 |
|
Get the database secret key in case it is not done when "connect" is called. It can happens when database is |
241 |
|
empty after an initial install. It should skip if secret is already obtained. |
242 |
|
""" |
243 |
1 |
pass |
244 |
|
|
245 |
1 |
def encrypt(self, value, schema_version=None, salt=None): |
246 |
|
""" |
247 |
|
Encrypt a value |
248 |
|
:param value: value to be encrypted. It is string/unicode |
249 |
|
:param schema_version: used for version control. If None or '1.0' no encryption is done. |
250 |
|
If '1.1' symmetric AES encryption is done |
251 |
|
:param salt: optional salt to be used. Must be str |
252 |
|
:return: Encrypted content of value |
253 |
|
""" |
254 |
1 |
self.get_secret_key() |
255 |
1 |
if not self.secret_key or not schema_version or schema_version == '1.0': |
256 |
1 |
return value |
257 |
|
else: |
258 |
1 |
secret_key = self._join_secret_key(salt) |
259 |
1 |
cipher = AES.new(secret_key) |
260 |
1 |
padded_private_msg = value + ('\0' * ((16-len(value)) % 16)) |
261 |
1 |
encrypted_msg = cipher.encrypt(padded_private_msg) |
262 |
1 |
encoded_encrypted_msg = b64encode(encrypted_msg) |
263 |
1 |
return encoded_encrypted_msg.decode("ascii") |
264 |
|
|
265 |
1 |
def decrypt(self, value, schema_version=None, salt=None): |
266 |
|
""" |
267 |
|
Decrypt an encrypted value |
268 |
|
:param value: value to be decrypted. It is a base64 string |
269 |
|
:param schema_version: used for known encryption method used. If None or '1.0' no encryption has been done. |
270 |
|
If '1.1' symmetric AES encryption has been done |
271 |
|
:param salt: optional salt to be used |
272 |
|
:return: Plain content of value |
273 |
|
""" |
274 |
1 |
self.get_secret_key() |
275 |
1 |
if not self.secret_key or not schema_version or schema_version == '1.0': |
276 |
1 |
return value |
277 |
|
else: |
278 |
1 |
secret_key = self._join_secret_key(salt) |
279 |
1 |
encrypted_msg = b64decode(value) |
280 |
1 |
cipher = AES.new(secret_key) |
281 |
1 |
decrypted_msg = cipher.decrypt(encrypted_msg) |
282 |
1 |
try: |
283 |
1 |
unpadded_private_msg = decrypted_msg.decode().rstrip('\0') |
284 |
1 |
except UnicodeDecodeError: |
285 |
1 |
raise DbException("Cannot decrypt information. Are you using same COMMONKEY in all OSM components?", |
286 |
|
http_code=HTTPStatus.INTERNAL_SERVER_ERROR) |
287 |
1 |
return unpadded_private_msg |
288 |
|
|
289 |
1 |
def encrypt_decrypt_fields(self, item, action, fields=None, flags=None, schema_version=None, salt=None): |
290 |
0 |
if not fields: |
291 |
0 |
return |
292 |
0 |
self.get_secret_key() |
293 |
0 |
actions = ['encrypt', 'decrypt'] |
294 |
0 |
if action.lower() not in actions: |
295 |
0 |
raise DbException("Unknown action ({}): Must be one of {}".format(action, actions), |
296 |
|
http_code=HTTPStatus.INTERNAL_SERVER_ERROR) |
297 |
0 |
method = self.encrypt if action.lower() == 'encrypt' else self.decrypt |
298 |
0 |
if flags is None: |
299 |
0 |
flags = re.I |
300 |
|
|
301 |
0 |
def process(_item): |
302 |
0 |
if isinstance(_item, list): |
303 |
0 |
for elem in _item: |
304 |
0 |
process(elem) |
305 |
0 |
elif isinstance(_item, dict): |
306 |
0 |
for key, val in _item.items(): |
307 |
0 |
if isinstance(val, str): |
308 |
0 |
if any(re.search(f, key, flags) for f in fields): |
309 |
0 |
_item[key] = method(val, schema_version, salt) |
310 |
|
else: |
311 |
0 |
process(val) |
312 |
0 |
process(item) |
313 |
|
|
314 |
|
|
315 |
1 |
def deep_update_rfc7396(dict_to_change, dict_reference, key_list=None): |
316 |
|
""" |
317 |
|
Modifies one dictionary with the information of the other following https://tools.ietf.org/html/rfc7396 |
318 |
|
Basically is a recursive python 'dict_to_change.update(dict_reference)', but a value of None is used to delete. |
319 |
|
It implements an extra feature that allows modifying an array. RFC7396 only allows replacing the entire array. |
320 |
|
For that, dict_reference should contains a dict with keys starting by "$" with the following meaning: |
321 |
|
$[index] <index> is an integer for targeting a concrete index from dict_to_change array. If the value is None |
322 |
|
the element of the array is deleted, otherwise it is edited. |
323 |
|
$+[index] The value is inserted at this <index>. A value of None has not sense and an exception is raised. |
324 |
|
$+ The value is appended at the end. A value of None has not sense and an exception is raised. |
325 |
|
$val It looks for all the items in the array dict_to_change equal to <val>. <val> is evaluated as yaml, |
326 |
|
that is, numbers are taken as type int, true/false as boolean, etc. Use quotes to force string. |
327 |
|
Nothing happens if no match is found. If the value is None the matched elements are deleted. |
328 |
|
$key: val In case a dictionary is passed in yaml format, if looks for all items in the array dict_to_change |
329 |
|
that are dictionaries and contains this <key> equal to <val>. Several keys can be used by yaml |
330 |
|
format '{key: val, key: val, ...}'; and all of them must match. Nothing happens if no match is |
331 |
|
found. If value is None the matched items are deleted, otherwise they are edited. |
332 |
|
$+val If no match if found (see '$val'), the value is appended to the array. If any match is found nothing |
333 |
|
is changed. A value of None has not sense. |
334 |
|
$+key: val If no match if found (see '$key: val'), the value is appended to the array. If any match is found |
335 |
|
nothing is changed. A value of None has not sense. |
336 |
|
If there are several editions, insertions and deletions; editions and deletions are done first in reverse index |
337 |
|
order; then insertions also in reverse index order; and finally appends in any order. So indexes used at |
338 |
|
insertions must take into account the deleted items. |
339 |
|
:param dict_to_change: Target dictionary to be changed. |
340 |
|
:param dict_reference: Dictionary that contains changes to be applied. |
341 |
|
:param key_list: This is used internally for recursive calls. Do not fill this parameter. |
342 |
|
:return: none or raises and exception only at array modification when there is a bad format or conflict. |
343 |
|
""" |
344 |
1 |
def _deep_update_array(array_to_change, _dict_reference, _key_list): |
345 |
1 |
to_append = {} |
346 |
1 |
to_insert_at_index = {} |
347 |
1 |
values_to_edit_delete = {} |
348 |
1 |
indexes_to_edit_delete = [] |
349 |
1 |
array_edition = None |
350 |
1 |
_key_list.append("") |
351 |
1 |
for k in _dict_reference: |
352 |
1 |
_key_list[-1] = str(k) |
353 |
1 |
if not isinstance(k, str) or not k.startswith("$"): |
354 |
1 |
if array_edition is True: |
355 |
1 |
raise DbException("Found array edition (keys starting with '$') and pure dictionary edition in the" |
356 |
|
" same dict at '{}'".format(":".join(_key_list[:-1]))) |
357 |
1 |
array_edition = False |
358 |
1 |
continue |
359 |
|
else: |
360 |
1 |
if array_edition is False: |
361 |
0 |
raise DbException("Found array edition (keys starting with '$') and pure dictionary edition in the" |
362 |
|
" same dict at '{}'".format(":".join(_key_list[:-1]))) |
363 |
1 |
array_edition = True |
364 |
1 |
insert = False |
365 |
1 |
indexes = [] # indexes to edit or insert |
366 |
1 |
kitem = k[1:] |
367 |
1 |
if kitem.startswith('+'): |
368 |
1 |
insert = True |
369 |
1 |
kitem = kitem[1:] |
370 |
1 |
if _dict_reference[k] is None: |
371 |
1 |
raise DbException("A value of None has not sense for insertions at '{}'".format( |
372 |
|
":".join(_key_list))) |
373 |
|
|
374 |
1 |
if kitem.startswith('[') and kitem.endswith(']'): |
375 |
1 |
try: |
376 |
1 |
index = int(kitem[1:-1]) |
377 |
1 |
if index < 0: |
378 |
1 |
index += len(array_to_change) |
379 |
1 |
if index < 0: |
380 |
0 |
index = 0 # skip outside index edition |
381 |
1 |
indexes.append(index) |
382 |
0 |
except Exception: |
383 |
0 |
raise DbException("Wrong format at '{}'. Expecting integer index inside quotes".format( |
384 |
|
":".join(_key_list))) |
385 |
1 |
elif kitem: |
386 |
|
# match_found_skip = False |
387 |
1 |
try: |
388 |
1 |
filter_in = yaml.safe_load(kitem) |
389 |
1 |
except Exception: |
390 |
1 |
raise DbException("Wrong format at '{}'. Expecting '$<yaml-format>'".format(":".join(_key_list))) |
391 |
1 |
if isinstance(filter_in, dict): |
392 |
1 |
for index, item in enumerate(array_to_change): |
393 |
1 |
for filter_k, filter_v in filter_in.items(): |
394 |
1 |
if not isinstance(item, dict) or filter_k not in item or item[filter_k] != filter_v: |
395 |
1 |
break |
396 |
|
else: # match found |
397 |
1 |
if insert: |
398 |
|
# match_found_skip = True |
399 |
0 |
insert = False |
400 |
0 |
break |
401 |
|
else: |
402 |
1 |
indexes.append(index) |
403 |
|
else: |
404 |
1 |
index = 0 |
405 |
1 |
try: |
406 |
1 |
while True: # if not match a ValueError exception will be raise |
407 |
1 |
index = array_to_change.index(filter_in, index) |
408 |
1 |
if insert: |
409 |
|
# match_found_skip = True |
410 |
1 |
insert = False |
411 |
1 |
break |
412 |
1 |
indexes.append(index) |
413 |
1 |
index += 1 |
414 |
1 |
except ValueError: |
415 |
1 |
pass |
416 |
|
|
417 |
|
# if match_found_skip: |
418 |
|
# continue |
419 |
1 |
elif not insert: |
420 |
1 |
raise DbException("Wrong format at '{}'. Expecting '$+', '$[<index]' or '$[<filter>]'".format( |
421 |
|
":".join(_key_list))) |
422 |
1 |
for index in indexes: |
423 |
1 |
if insert: |
424 |
1 |
if index in to_insert_at_index and to_insert_at_index[index] != _dict_reference[k]: |
425 |
|
# Several different insertions on the same item of the array |
426 |
0 |
raise DbException("Conflict at '{}'. Several insertions on same array index {}".format( |
427 |
|
":".join(_key_list), index)) |
428 |
1 |
to_insert_at_index[index] = _dict_reference[k] |
429 |
|
else: |
430 |
1 |
if index in indexes_to_edit_delete and values_to_edit_delete[index] != _dict_reference[k]: |
431 |
|
# Several different editions on the same item of the array |
432 |
1 |
raise DbException("Conflict at '{}'. Several editions on array index {}".format( |
433 |
|
":".join(_key_list), index)) |
434 |
1 |
indexes_to_edit_delete.append(index) |
435 |
1 |
values_to_edit_delete[index] = _dict_reference[k] |
436 |
1 |
if not indexes: |
437 |
1 |
if insert: |
438 |
1 |
to_append[k] = _dict_reference[k] |
439 |
|
# elif _dict_reference[k] is not None: |
440 |
|
# raise DbException("Not found any match to edit in the array, or wrong format at '{}'".format( |
441 |
|
# ":".join(_key_list))) |
442 |
|
|
443 |
|
# edition/deletion is done before insertion |
444 |
1 |
indexes_to_edit_delete.sort(reverse=True) |
445 |
1 |
for index in indexes_to_edit_delete: |
446 |
1 |
_key_list[-1] = str(index) |
447 |
1 |
try: |
448 |
1 |
if values_to_edit_delete[index] is None: # None->Anything |
449 |
1 |
try: |
450 |
1 |
del (array_to_change[index]) |
451 |
1 |
except IndexError: |
452 |
1 |
pass # it is not consider an error if this index does not exist |
453 |
1 |
elif not isinstance(values_to_edit_delete[index], dict): # NotDict->Anything |
454 |
1 |
array_to_change[index] = deepcopy(values_to_edit_delete[index]) |
455 |
1 |
elif isinstance(array_to_change[index], dict): # Dict->Dict |
456 |
1 |
deep_update_rfc7396(array_to_change[index], values_to_edit_delete[index], _key_list) |
457 |
|
else: # Dict->NotDict |
458 |
1 |
if isinstance(array_to_change[index], list): # Dict->List. Check extra array edition |
459 |
1 |
if _deep_update_array(array_to_change[index], values_to_edit_delete[index], _key_list): |
460 |
1 |
continue |
461 |
1 |
array_to_change[index] = deepcopy(values_to_edit_delete[index]) |
462 |
|
# calling deep_update_rfc7396 to delete the None values |
463 |
1 |
deep_update_rfc7396(array_to_change[index], values_to_edit_delete[index], _key_list) |
464 |
1 |
except IndexError: |
465 |
1 |
raise DbException("Array edition index out of range at '{}'".format(":".join(_key_list))) |
466 |
|
|
467 |
|
# insertion with indexes |
468 |
1 |
to_insert_indexes = list(to_insert_at_index.keys()) |
469 |
1 |
to_insert_indexes.sort(reverse=True) |
470 |
1 |
for index in to_insert_indexes: |
471 |
1 |
array_to_change.insert(index, to_insert_at_index[index]) |
472 |
|
|
473 |
|
# append |
474 |
1 |
for k, insert_value in to_append.items(): |
475 |
1 |
_key_list[-1] = str(k) |
476 |
1 |
insert_value_copy = deepcopy(insert_value) |
477 |
1 |
if isinstance(insert_value_copy, dict): |
478 |
|
# calling deep_update_rfc7396 to delete the None values |
479 |
0 |
deep_update_rfc7396(insert_value_copy, insert_value, _key_list) |
480 |
1 |
array_to_change.append(insert_value_copy) |
481 |
|
|
482 |
1 |
_key_list.pop() |
483 |
1 |
if array_edition: |
484 |
1 |
return True |
485 |
1 |
return False |
486 |
|
|
487 |
1 |
if key_list is None: |
488 |
1 |
key_list = [] |
489 |
1 |
key_list.append("") |
490 |
1 |
for k in dict_reference: |
491 |
1 |
key_list[-1] = str(k) |
492 |
1 |
if dict_reference[k] is None: # None->Anything |
493 |
1 |
if k in dict_to_change: |
494 |
1 |
del dict_to_change[k] |
495 |
1 |
elif not isinstance(dict_reference[k], dict): # NotDict->Anything |
496 |
1 |
dict_to_change[k] = deepcopy(dict_reference[k]) |
497 |
1 |
elif k not in dict_to_change: # Dict->Empty |
498 |
1 |
dict_to_change[k] = deepcopy(dict_reference[k]) |
499 |
|
# calling deep_update_rfc7396 to delete the None values |
500 |
1 |
deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list) |
501 |
1 |
elif isinstance(dict_to_change[k], dict): # Dict->Dict |
502 |
1 |
deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list) |
503 |
|
else: # Dict->NotDict |
504 |
1 |
if isinstance(dict_to_change[k], list): # Dict->List. Check extra array edition |
505 |
1 |
if _deep_update_array(dict_to_change[k], dict_reference[k], key_list): |
506 |
1 |
continue |
507 |
1 |
dict_to_change[k] = deepcopy(dict_reference[k]) |
508 |
|
# calling deep_update_rfc7396 to delete the None values |
509 |
1 |
deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list) |
510 |
1 |
key_list.pop() |
511 |
|
|
512 |
|
|
513 |
1 |
def deep_update(dict_to_change, dict_reference): |
514 |
|
""" Maintained for backward compatibility. Use deep_update_rfc7396 instead""" |
515 |
1 |
return deep_update_rfc7396(dict_to_change, dict_reference) |