blob: 7428ed969509860ef2cf4af999fa5f9024a28afb [file] [log] [blame]
tierno87858ca2018-10-08 16:30:15 +02001# -*- 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
tiernob3e750b2018-09-05 11:25:23 +020018import yaml
tierno87858ca2018-10-08 16:30:15 +020019import logging
delacruzramo54a54642019-10-25 16:50:13 +020020import re
tierno5c012612018-04-19 16:01:59 +020021from http import HTTPStatus
tiernob3e750b2018-09-05 11:25:23 +020022from copy import deepcopy
tierno136f2952018-10-19 13:01:03 +020023from Crypto.Cipher import AES
24from base64 import b64decode, b64encode
tierno1e9a3292018-11-05 18:18:45 +010025from osm_common.common_utils import FakeLock
26from threading import Lock
tierno5c012612018-04-19 16:01:59 +020027
28__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
29
30
31class DbException(Exception):
32
33 def __init__(self, message, http_code=HTTPStatus.NOT_FOUND):
34 self.http_code = http_code
tierno87858ca2018-10-08 16:30:15 +020035 Exception.__init__(self, "database exception " + str(message))
tierno5c012612018-04-19 16:01:59 +020036
37
38class DbBase(object):
39
tierno1e9a3292018-11-05 18:18:45 +010040 def __init__(self, logger_name='db', lock=False):
tierno87858ca2018-10-08 16:30:15 +020041 """
tierno1e9a3292018-11-05 18:18:45 +010042 Constructor of dbBase
tierno87858ca2018-10-08 16:30:15 +020043 :param logger_name: logging name
tierno1e9a3292018-11-05 18:18:45 +010044 :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
tierno87858ca2018-10-08 16:30:15 +020048 """
49 self.logger = logging.getLogger(logger_name)
tiernoeef7cb72018-11-12 11:51:49 +010050 self.secret_key = None # 32 bytes length array used for encrypt/decrypt
tierno1e9a3292018-11-05 18:18:45 +010051 if not lock:
52 self.lock = FakeLock()
53 elif lock is True:
54 self.lock = Lock()
55 elif isinstance(lock, Lock):
56 self.lock = lock
57 else:
58 raise ValueError("lock parameter must be a Lock classclass or boolean")
tierno5c012612018-04-19 16:01:59 +020059
tierno136f2952018-10-19 13:01:03 +020060 def db_connect(self, config, target_version=None):
tierno87858ca2018-10-08 16:30:15 +020061 """
62 Connect to database
tiernocfc52722018-10-23 11:41:49 +020063 :param config: Configuration of database. Contains among others:
64 host: database hosst (mandatory)
65 port: database port (mandatory)
66 name: database name (mandatory)
67 user: database username
68 password: database password
tiernoeef7cb72018-11-12 11:51:49 +010069 commonkey: common OSM key used for sensible information encryption
70 materpassword: same as commonkey, for backward compatibility. Deprecated, to be removed in the future
tierno136f2952018-10-19 13:01:03 +020071 :param target_version: if provided it checks if database contains required version, raising exception otherwise.
tierno87858ca2018-10-08 16:30:15 +020072 :return: None or raises DbException on error
73 """
tierno136f2952018-10-19 13:01:03 +020074 raise DbException("Method 'db_connect' not implemented")
tierno5c012612018-04-19 16:01:59 +020075
76 def db_disconnect(self):
tierno87858ca2018-10-08 16:30:15 +020077 """
78 Disconnect from database
79 :return: None
80 """
tierno5c012612018-04-19 16:01:59 +020081 pass
82
tierno87858ca2018-10-08 16:30:15 +020083 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 """
tiernoebbf3532018-05-03 17:49:37 +020090 raise DbException("Method 'get_list' not implemented")
tierno5c012612018-04-19 16:01:59 +020091
delacruzramoae049d82019-09-17 16:05:17 +020092 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 raise DbException("Method 'count' not implemented")
101
tierno87858ca2018-10-08 16:30:15 +0200102 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 """
tiernoebbf3532018-05-03 17:49:37 +0200113 raise DbException("Method 'get_one' not implemented")
tierno5c012612018-04-19 16:01:59 +0200114
tierno87858ca2018-10-08 16:30:15 +0200115 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 """
tiernoebbf3532018-05-03 17:49:37 +0200122 raise DbException("Method 'del_list' not implemented")
tierno5c012612018-04-19 16:01:59 +0200123
tierno87858ca2018-10-08 16:30:15 +0200124 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 """
tiernoebbf3532018-05-03 17:49:37 +0200133 raise DbException("Method 'del_one' not implemented")
tiernob3e750b2018-09-05 11:25:23 +0200134
tierno87858ca2018-10-08 16:30:15 +0200135 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 raise DbException("Method 'create' not implemented")
tiernob3e750b2018-09-05 11:25:23 +0200143
tiernod63ea272018-11-27 12:03:36 +0100144 def set_one(self, table, q_filter, update_dict, fail_on_empty=True, unset=None, pull=None, push=None):
tierno87858ca2018-10-08 16:30:15 +0200145 """
146 Modifies an entry at database
147 :param table: collection or table
148 :param q_filter: Filter
149 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
150 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
151 it raises a DbException
tiernod63ea272018-11-27 12:03:36 +0100152 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
153 ignored. If not exist, it is ignored
154 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
155 if exist in the array is removed. If not exist, it is ignored
156 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
157 is appended to the end of the array
tierno87858ca2018-10-08 16:30:15 +0200158 :return: Dict with the number of entries modified. None if no matching is found.
159 """
160 raise DbException("Method 'set_one' not implemented")
161
162 def set_list(self, table, q_filter, update_dict):
163 """
164 Modifies al matching entries at database
165 :param table: collection or table
166 :param q_filter: Filter
167 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
168 :return: Dict with the number of entries modified
169 """
170 raise DbException("Method 'set_list' not implemented")
171
172 def replace(self, table, _id, indata, fail_on_empty=True):
173 """
174 Replace the content of an entry
175 :param table: collection or table
176 :param _id: internal database id
177 :param indata: content to replace
178 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
179 it raises a DbException
180 :return: Dict with the number of entries replaced
181 """
182 raise DbException("Method 'replace' not implemented")
183
tiernoeef7cb72018-11-12 11:51:49 +0100184 def _join_secret_key(self, update_key):
tierno136f2952018-10-19 13:01:03 +0200185 """
tiernoeef7cb72018-11-12 11:51:49 +0100186 Returns a xor byte combination of the internal secret_key and the provided update_key.
187 It does not modify the internal secret_key. Used for adding salt, join keys, etc.
188 :param update_key: Can be a string, byte or None. Recommended a long one (e.g. 32 byte length)
189 :return: joined key in bytes with a 32 bytes length. Can be None if both internal secret_key and update_key
190 are None
tierno136f2952018-10-19 13:01:03 +0200191 """
tiernoeef7cb72018-11-12 11:51:49 +0100192 if not update_key:
193 return self.secret_key
194 elif isinstance(update_key, str):
195 update_key_bytes = update_key.encode()
196 else:
197 update_key_bytes = update_key
tierno136f2952018-10-19 13:01:03 +0200198
tiernoeef7cb72018-11-12 11:51:49 +0100199 new_secret_key = bytearray(self.secret_key) if self.secret_key else bytearray(32)
200 for i, b in enumerate(update_key_bytes):
201 new_secret_key[i % 32] ^= b
202 return bytes(new_secret_key)
203
204 def set_secret_key(self, new_secret_key, replace=False):
tierno136f2952018-10-19 13:01:03 +0200205 """
tiernoeef7cb72018-11-12 11:51:49 +0100206 Updates internal secret_key used for encryption, with a byte xor
207 :param new_secret_key: string or byte array. It is recommended a 32 byte length
208 :param replace: if True, old value of internal secret_key is ignored and replaced. If false, a byte xor is used
tierno136f2952018-10-19 13:01:03 +0200209 :return: None
210 """
tiernoeef7cb72018-11-12 11:51:49 +0100211 if replace:
212 self.secret_key = None
213 self.secret_key = self._join_secret_key(new_secret_key)
tierno136f2952018-10-19 13:01:03 +0200214
tiernoc5297e42019-12-11 12:32:41 +0000215 def get_secret_key(self):
216 """
217 Get the database secret key in case it is not done when "connect" is called. It can happens when database is
218 empty after an initial install. It should skip if secret is already obtained.
219 """
220 pass
221
tierno136f2952018-10-19 13:01:03 +0200222 def encrypt(self, value, schema_version=None, salt=None):
tierno87858ca2018-10-08 16:30:15 +0200223 """
224 Encrypt a value
tierno136f2952018-10-19 13:01:03 +0200225 :param value: value to be encrypted. It is string/unicode
226 :param schema_version: used for version control. If None or '1.0' no encryption is done.
227 If '1.1' symmetric AES encryption is done
228 :param salt: optional salt to be used. Must be str
tierno87858ca2018-10-08 16:30:15 +0200229 :return: Encrypted content of value
230 """
tiernoc5297e42019-12-11 12:32:41 +0000231 self.get_secret_key()
tiernoeef7cb72018-11-12 11:51:49 +0100232 if not self.secret_key or not schema_version or schema_version == '1.0':
tierno136f2952018-10-19 13:01:03 +0200233 return value
234 else:
tiernoeef7cb72018-11-12 11:51:49 +0100235 secret_key = self._join_secret_key(salt)
tierno136f2952018-10-19 13:01:03 +0200236 cipher = AES.new(secret_key)
237 padded_private_msg = value + ('\0' * ((16-len(value)) % 16))
238 encrypted_msg = cipher.encrypt(padded_private_msg)
239 encoded_encrypted_msg = b64encode(encrypted_msg)
240 return encoded_encrypted_msg.decode("ascii")
tierno87858ca2018-10-08 16:30:15 +0200241
tierno136f2952018-10-19 13:01:03 +0200242 def decrypt(self, value, schema_version=None, salt=None):
tierno87858ca2018-10-08 16:30:15 +0200243 """
244 Decrypt an encrypted value
tierno136f2952018-10-19 13:01:03 +0200245 :param value: value to be decrypted. It is a base64 string
246 :param schema_version: used for known encryption method used. If None or '1.0' no encryption has been done.
247 If '1.1' symmetric AES encryption has been done
tierno87858ca2018-10-08 16:30:15 +0200248 :param salt: optional salt to be used
249 :return: Plain content of value
250 """
tiernoc5297e42019-12-11 12:32:41 +0000251 self.get_secret_key()
tiernoeef7cb72018-11-12 11:51:49 +0100252 if not self.secret_key or not schema_version or schema_version == '1.0':
tierno136f2952018-10-19 13:01:03 +0200253 return value
254 else:
tiernoeef7cb72018-11-12 11:51:49 +0100255 secret_key = self._join_secret_key(salt)
tierno136f2952018-10-19 13:01:03 +0200256 encrypted_msg = b64decode(value)
257 cipher = AES.new(secret_key)
258 decrypted_msg = cipher.decrypt(encrypted_msg)
tiernobd5a4022019-01-30 09:48:38 +0000259 try:
260 unpadded_private_msg = decrypted_msg.decode().rstrip('\0')
261 except UnicodeDecodeError:
262 raise DbException("Cannot decrypt information. Are you using same COMMONKEY in all OSM components?",
263 http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
tierno136f2952018-10-19 13:01:03 +0200264 return unpadded_private_msg
tierno87858ca2018-10-08 16:30:15 +0200265
delacruzramo54a54642019-10-25 16:50:13 +0200266 def encrypt_decrypt_fields(self, item, action, fields=None, flags=re.I, schema_version=None, salt=None):
267 if not fields:
268 return
tiernoc5297e42019-12-11 12:32:41 +0000269 self.get_secret_key()
delacruzramo54a54642019-10-25 16:50:13 +0200270 actions = ['encrypt', 'decrypt']
271 if action.lower() not in actions:
272 raise DbException("Unknown action ({}): Must be one of {}".format(action, actions),
273 http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
274 method = self.encrypt if action.lower() == 'encrypt' else self.decrypt
275
276 def process(item):
277 if isinstance(item, list):
278 for elem in item:
279 process(elem)
280 elif isinstance(item, dict):
281 for key, val in item.items():
282 if any(re.search(f, key, flags) for f in fields) and isinstance(val, str):
283 item[key] = method(val, schema_version, salt)
284 else:
285 process(val)
286
287 process(item)
288
tierno87858ca2018-10-08 16:30:15 +0200289
290def deep_update_rfc7396(dict_to_change, dict_reference, key_list=None):
tiernob3e750b2018-09-05 11:25:23 +0200291 """
292 Modifies one dictionary with the information of the other following https://tools.ietf.org/html/rfc7396
293 Basically is a recursive python 'dict_to_change.update(dict_reference)', but a value of None is used to delete.
294 It implements an extra feature that allows modifying an array. RFC7396 only allows replacing the entire array.
295 For that, dict_reference should contains a dict with keys starting by "$" with the following meaning:
296 $[index] <index> is an integer for targeting a concrete index from dict_to_change array. If the value is None
297 the element of the array is deleted, otherwise it is edited.
298 $+[index] The value is inserted at this <index>. A value of None has not sense and an exception is raised.
299 $+ The value is appended at the end. A value of None has not sense and an exception is raised.
300 $val It looks for all the items in the array dict_to_change equal to <val>. <val> is evaluated as yaml,
301 that is, numbers are taken as type int, true/false as boolean, etc. Use quotes to force string.
302 Nothing happens if no match is found. If the value is None the matched elements are deleted.
303 $key: val In case a dictionary is passed in yaml format, if looks for all items in the array dict_to_change
304 that are dictionaries and contains this <key> equal to <val>. Several keys can be used by yaml
tierno3e759152019-08-28 16:08:25 +0000305 format '{key: val, key: val, ...}'; and all of them must match. Nothing happens if no match is
tiernob3e750b2018-09-05 11:25:23 +0200306 found. If value is None the matched items are deleted, otherwise they are edited.
307 $+val If no match if found (see '$val'), the value is appended to the array. If any match is found nothing
308 is changed. A value of None has not sense.
309 $+key: val If no match if found (see '$key: val'), the value is appended to the array. If any match is found
310 nothing is changed. A value of None has not sense.
311 If there are several editions, insertions and deletions; editions and deletions are done first in reverse index
312 order; then insertions also in reverse index order; and finally appends in any order. So indexes used at
313 insertions must take into account the deleted items.
314 :param dict_to_change: Target dictionary to be changed.
315 :param dict_reference: Dictionary that contains changes to be applied.
316 :param key_list: This is used internally for recursive calls. Do not fill this parameter.
317 :return: none or raises and exception only at array modification when there is a bad format or conflict.
318 """
319 def _deep_update_array(array_to_change, _dict_reference, _key_list):
320 to_append = {}
321 to_insert_at_index = {}
322 values_to_edit_delete = {}
323 indexes_to_edit_delete = []
324 array_edition = None
325 _key_list.append("")
326 for k in _dict_reference:
327 _key_list[-1] = str(k)
328 if not isinstance(k, str) or not k.startswith("$"):
329 if array_edition is True:
330 raise DbException("Found array edition (keys starting with '$') and pure dictionary edition in the"
331 " same dict at '{}'".format(":".join(_key_list[:-1])))
332 array_edition = False
333 continue
334 else:
335 if array_edition is False:
336 raise DbException("Found array edition (keys starting with '$') and pure dictionary edition in the"
337 " same dict at '{}'".format(":".join(_key_list[:-1])))
338 array_edition = True
339 insert = False
340 indexes = [] # indexes to edit or insert
341 kitem = k[1:]
342 if kitem.startswith('+'):
343 insert = True
344 kitem = kitem[1:]
345 if _dict_reference[k] is None:
346 raise DbException("A value of None has not sense for insertions at '{}'".format(
347 ":".join(_key_list)))
348
349 if kitem.startswith('[') and kitem.endswith(']'):
350 try:
351 index = int(kitem[1:-1])
352 if index < 0:
353 index += len(array_to_change)
354 if index < 0:
355 index = 0 # skip outside index edition
356 indexes.append(index)
357 except Exception:
358 raise DbException("Wrong format at '{}'. Expecting integer index inside quotes".format(
359 ":".join(_key_list)))
360 elif kitem:
361 # match_found_skip = False
362 try:
363 filter_in = yaml.safe_load(kitem)
364 except Exception:
365 raise DbException("Wrong format at '{}'. Expecting '$<yaml-format>'".format(":".join(_key_list)))
366 if isinstance(filter_in, dict):
367 for index, item in enumerate(array_to_change):
368 for filter_k, filter_v in filter_in.items():
369 if not isinstance(item, dict) or filter_k not in item or item[filter_k] != filter_v:
370 break
371 else: # match found
372 if insert:
373 # match_found_skip = True
374 insert = False
375 break
376 else:
377 indexes.append(index)
378 else:
379 index = 0
380 try:
381 while True: # if not match a ValueError exception will be raise
382 index = array_to_change.index(filter_in, index)
383 if insert:
384 # match_found_skip = True
385 insert = False
386 break
387 indexes.append(index)
388 index += 1
389 except ValueError:
390 pass
391
392 # if match_found_skip:
393 # continue
394 elif not insert:
395 raise DbException("Wrong format at '{}'. Expecting '$+', '$[<index]' or '$[<filter>]'".format(
396 ":".join(_key_list)))
397 for index in indexes:
398 if insert:
399 if index in to_insert_at_index and to_insert_at_index[index] != _dict_reference[k]:
400 # Several different insertions on the same item of the array
401 raise DbException("Conflict at '{}'. Several insertions on same array index {}".format(
402 ":".join(_key_list), index))
403 to_insert_at_index[index] = _dict_reference[k]
404 else:
405 if index in indexes_to_edit_delete and values_to_edit_delete[index] != _dict_reference[k]:
406 # Several different editions on the same item of the array
407 raise DbException("Conflict at '{}'. Several editions on array index {}".format(
408 ":".join(_key_list), index))
409 indexes_to_edit_delete.append(index)
410 values_to_edit_delete[index] = _dict_reference[k]
411 if not indexes:
412 if insert:
413 to_append[k] = _dict_reference[k]
414 # elif _dict_reference[k] is not None:
415 # raise DbException("Not found any match to edit in the array, or wrong format at '{}'".format(
416 # ":".join(_key_list)))
417
418 # edition/deletion is done before insertion
419 indexes_to_edit_delete.sort(reverse=True)
420 for index in indexes_to_edit_delete:
421 _key_list[-1] = str(index)
422 try:
423 if values_to_edit_delete[index] is None: # None->Anything
424 try:
425 del (array_to_change[index])
426 except IndexError:
427 pass # it is not consider an error if this index does not exist
428 elif not isinstance(values_to_edit_delete[index], dict): # NotDict->Anything
429 array_to_change[index] = deepcopy(values_to_edit_delete[index])
430 elif isinstance(array_to_change[index], dict): # Dict->Dict
tierno87858ca2018-10-08 16:30:15 +0200431 deep_update_rfc7396(array_to_change[index], values_to_edit_delete[index], _key_list)
tiernob3e750b2018-09-05 11:25:23 +0200432 else: # Dict->NotDict
433 if isinstance(array_to_change[index], list): # Dict->List. Check extra array edition
434 if _deep_update_array(array_to_change[index], values_to_edit_delete[index], _key_list):
435 continue
436 array_to_change[index] = deepcopy(values_to_edit_delete[index])
tierno87858ca2018-10-08 16:30:15 +0200437 # calling deep_update_rfc7396 to delete the None values
438 deep_update_rfc7396(array_to_change[index], values_to_edit_delete[index], _key_list)
tiernob3e750b2018-09-05 11:25:23 +0200439 except IndexError:
440 raise DbException("Array edition index out of range at '{}'".format(":".join(_key_list)))
441
442 # insertion with indexes
443 to_insert_indexes = list(to_insert_at_index.keys())
444 to_insert_indexes.sort(reverse=True)
445 for index in to_insert_indexes:
446 array_to_change.insert(index, to_insert_at_index[index])
447
448 # append
449 for k, insert_value in to_append.items():
450 _key_list[-1] = str(k)
451 insert_value_copy = deepcopy(insert_value)
452 if isinstance(insert_value_copy, dict):
tierno87858ca2018-10-08 16:30:15 +0200453 # calling deep_update_rfc7396 to delete the None values
454 deep_update_rfc7396(insert_value_copy, insert_value, _key_list)
tiernob3e750b2018-09-05 11:25:23 +0200455 array_to_change.append(insert_value_copy)
456
457 _key_list.pop()
458 if array_edition:
459 return True
460 return False
461
462 if key_list is None:
463 key_list = []
464 key_list.append("")
465 for k in dict_reference:
466 key_list[-1] = str(k)
467 if dict_reference[k] is None: # None->Anything
468 if k in dict_to_change:
469 del dict_to_change[k]
470 elif not isinstance(dict_reference[k], dict): # NotDict->Anything
471 dict_to_change[k] = deepcopy(dict_reference[k])
472 elif k not in dict_to_change: # Dict->Empty
473 dict_to_change[k] = deepcopy(dict_reference[k])
tierno87858ca2018-10-08 16:30:15 +0200474 # calling deep_update_rfc7396 to delete the None values
475 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
tiernob3e750b2018-09-05 11:25:23 +0200476 elif isinstance(dict_to_change[k], dict): # Dict->Dict
tierno87858ca2018-10-08 16:30:15 +0200477 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
tiernob3e750b2018-09-05 11:25:23 +0200478 else: # Dict->NotDict
479 if isinstance(dict_to_change[k], list): # Dict->List. Check extra array edition
480 if _deep_update_array(dict_to_change[k], dict_reference[k], key_list):
481 continue
482 dict_to_change[k] = deepcopy(dict_reference[k])
tierno87858ca2018-10-08 16:30:15 +0200483 # calling deep_update_rfc7396 to delete the None values
484 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
tiernob3e750b2018-09-05 11:25:23 +0200485 key_list.pop()
tierno87858ca2018-10-08 16:30:15 +0200486
487
488def deep_update(dict_to_change, dict_reference):
489 """ Maintained for backward compatibility. Use deep_update_rfc7396 instead"""
490 return deep_update_rfc7396(dict_to_change, dict_reference)