blob: 74378d022ce07fb7fb2a009513f657146a356d6e [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):
tierno5c012612018-04-19 16:01:59 +020032 def __init__(self, message, http_code=HTTPStatus.NOT_FOUND):
33 self.http_code = http_code
tierno87858ca2018-10-08 16:30:15 +020034 Exception.__init__(self, "database exception " + str(message))
tierno5c012612018-04-19 16:01:59 +020035
36
37class DbBase(object):
garciadeblas2644b762021-03-24 09:21:01 +010038 def __init__(self, logger_name="db", lock=False):
tierno87858ca2018-10-08 16:30:15 +020039 """
tierno1e9a3292018-11-05 18:18:45 +010040 Constructor of dbBase
tierno87858ca2018-10-08 16:30:15 +020041 :param logger_name: logging name
tierno1e9a3292018-11-05 18:18:45 +010042 :param lock: Used to protect simultaneous access to the same instance class by several threads:
43 False, None: Do not protect, this object will only be accessed by one thread
44 True: This object needs to be protected by several threads accessing.
45 Lock object. Use thi Lock for the threads access protection
tierno87858ca2018-10-08 16:30:15 +020046 """
47 self.logger = logging.getLogger(logger_name)
tiernoeef7cb72018-11-12 11:51:49 +010048 self.secret_key = None # 32 bytes length array used for encrypt/decrypt
tierno1e9a3292018-11-05 18:18:45 +010049 if not lock:
50 self.lock = FakeLock()
51 elif lock is True:
52 self.lock = Lock()
53 elif isinstance(lock, Lock):
54 self.lock = lock
55 else:
56 raise ValueError("lock parameter must be a Lock classclass or boolean")
tierno5c012612018-04-19 16:01:59 +020057
tierno136f2952018-10-19 13:01:03 +020058 def db_connect(self, config, target_version=None):
tierno87858ca2018-10-08 16:30:15 +020059 """
60 Connect to database
tiernocfc52722018-10-23 11:41:49 +020061 :param config: Configuration of database. Contains among others:
tierno81b47d52020-01-21 10:11:34 +000062 host: database host (mandatory)
tiernocfc52722018-10-23 11:41:49 +020063 port: database port (mandatory)
64 name: database name (mandatory)
65 user: database username
66 password: database password
tiernoeef7cb72018-11-12 11:51:49 +010067 commonkey: common OSM key used for sensible information encryption
68 materpassword: same as commonkey, for backward compatibility. Deprecated, to be removed in the future
tierno136f2952018-10-19 13:01:03 +020069 :param target_version: if provided it checks if database contains required version, raising exception otherwise.
tierno87858ca2018-10-08 16:30:15 +020070 :return: None or raises DbException on error
71 """
tierno136f2952018-10-19 13:01:03 +020072 raise DbException("Method 'db_connect' not implemented")
tierno5c012612018-04-19 16:01:59 +020073
74 def db_disconnect(self):
tierno87858ca2018-10-08 16:30:15 +020075 """
76 Disconnect from database
77 :return: None
78 """
tierno5c012612018-04-19 16:01:59 +020079 pass
80
tierno87858ca2018-10-08 16:30:15 +020081 def get_list(self, table, q_filter=None):
82 """
83 Obtain a list of entries matching q_filter
84 :param table: collection or table
85 :param q_filter: Filter
86 :return: a list (can be empty) with the found entries. Raises DbException on error
87 """
tiernoebbf3532018-05-03 17:49:37 +020088 raise DbException("Method 'get_list' not implemented")
tierno5c012612018-04-19 16:01:59 +020089
delacruzramoae049d82019-09-17 16:05:17 +020090 def count(self, table, q_filter=None):
91 """
92 Count the number of entries matching q_filter
93 :param table: collection or table
94 :param q_filter: Filter
95 :return: number of entries found (can be zero)
96 :raise: DbException on error
97 """
98 raise DbException("Method 'count' not implemented")
99
tierno87858ca2018-10-08 16:30:15 +0200100 def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True):
101 """
102 Obtain one entry matching q_filter
103 :param table: collection or table
104 :param q_filter: Filter
105 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
106 it raises a DbException
107 :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so
108 that it raises a DbException
109 :return: The requested element, or None
110 """
tiernoebbf3532018-05-03 17:49:37 +0200111 raise DbException("Method 'get_one' not implemented")
tierno5c012612018-04-19 16:01:59 +0200112
tierno87858ca2018-10-08 16:30:15 +0200113 def del_list(self, table, q_filter=None):
114 """
115 Deletes all entries that match q_filter
116 :param table: collection or table
117 :param q_filter: Filter
118 :return: Dict with the number of entries deleted
119 """
tiernoebbf3532018-05-03 17:49:37 +0200120 raise DbException("Method 'del_list' not implemented")
tierno5c012612018-04-19 16:01:59 +0200121
tierno87858ca2018-10-08 16:30:15 +0200122 def del_one(self, table, q_filter=None, fail_on_empty=True):
123 """
124 Deletes one entry that matches q_filter
125 :param table: collection or table
126 :param q_filter: Filter
127 :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in
128 which case it raises a DbException
129 :return: Dict with the number of entries deleted
130 """
tiernoebbf3532018-05-03 17:49:37 +0200131 raise DbException("Method 'del_one' not implemented")
tiernob3e750b2018-09-05 11:25:23 +0200132
tierno87858ca2018-10-08 16:30:15 +0200133 def create(self, table, indata):
134 """
135 Add a new entry at database
136 :param table: collection or table
137 :param indata: content to be added
tierno2c9794c2020-04-29 10:24:28 +0000138 :return: database '_id' of the inserted element. Raises a DbException on error
tierno87858ca2018-10-08 16:30:15 +0200139 """
140 raise DbException("Method 'create' not implemented")
tiernob3e750b2018-09-05 11:25:23 +0200141
tierno2c9794c2020-04-29 10:24:28 +0000142 def create_list(self, table, indata_list):
143 """
144 Add several entries at once
145 :param table: collection or table
146 :param indata_list: list of elements to insert. Each element must be a dictionary.
147 An '_id' key based on random uuid is added at each element if missing
148 :return: list of inserted '_id's. Exception on error
149 """
150 raise DbException("Method 'create_list' not implemented")
151
garciadeblas2644b762021-03-24 09:21:01 +0100152 def set_one(
153 self,
154 table,
155 q_filter,
156 update_dict,
157 fail_on_empty=True,
158 unset=None,
159 pull=None,
160 push=None,
161 push_list=None,
162 pull_list=None,
163 ):
tierno87858ca2018-10-08 16:30:15 +0200164 """
165 Modifies an entry at database
166 :param table: collection or table
167 :param q_filter: Filter
168 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
169 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
170 it raises a DbException
tiernod63ea272018-11-27 12:03:36 +0100171 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
172 ignored. If not exist, it is ignored
173 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
174 if exist in the array is removed. If not exist, it is ignored
175 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
176 is appended to the end of the array
tierno0d8e4bc2020-06-22 12:18:18 +0000177 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno399f6c32020-05-12 07:36:41 +0000178 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
179 whole array
tierno87858ca2018-10-08 16:30:15 +0200180 :return: Dict with the number of entries modified. None if no matching is found.
181 """
182 raise DbException("Method 'set_one' not implemented")
183
garciadeblas2644b762021-03-24 09:21:01 +0100184 def set_list(
185 self,
186 table,
187 q_filter,
188 update_dict,
189 unset=None,
190 pull=None,
191 push=None,
192 push_list=None,
193 pull_list=None,
194 ):
tierno87858ca2018-10-08 16:30:15 +0200195 """
196 Modifies al matching entries at database
197 :param table: collection or table
198 :param q_filter: Filter
199 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
delacruzramof71fcff2020-02-11 11:14:07 +0000200 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
201 ignored. If not exist, it is ignored
202 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
203 if exist in the array is removed. If not exist, it is ignored
204 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
205 is appended to the end of the array
tierno0d8e4bc2020-06-22 12:18:18 +0000206 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno399f6c32020-05-12 07:36:41 +0000207 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
208 whole array
tierno87858ca2018-10-08 16:30:15 +0200209 :return: Dict with the number of entries modified
210 """
211 raise DbException("Method 'set_list' not implemented")
212
213 def replace(self, table, _id, indata, fail_on_empty=True):
214 """
215 Replace the content of an entry
216 :param table: collection or table
217 :param _id: internal database id
218 :param indata: content to replace
219 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
220 it raises a DbException
221 :return: Dict with the number of entries replaced
222 """
223 raise DbException("Method 'replace' not implemented")
224
tiernoeef7cb72018-11-12 11:51:49 +0100225 def _join_secret_key(self, update_key):
tierno136f2952018-10-19 13:01:03 +0200226 """
tiernoeef7cb72018-11-12 11:51:49 +0100227 Returns a xor byte combination of the internal secret_key and the provided update_key.
228 It does not modify the internal secret_key. Used for adding salt, join keys, etc.
229 :param update_key: Can be a string, byte or None. Recommended a long one (e.g. 32 byte length)
230 :return: joined key in bytes with a 32 bytes length. Can be None if both internal secret_key and update_key
231 are None
tierno136f2952018-10-19 13:01:03 +0200232 """
tiernoeef7cb72018-11-12 11:51:49 +0100233 if not update_key:
234 return self.secret_key
235 elif isinstance(update_key, str):
236 update_key_bytes = update_key.encode()
237 else:
238 update_key_bytes = update_key
tierno136f2952018-10-19 13:01:03 +0200239
garciadeblas2644b762021-03-24 09:21:01 +0100240 new_secret_key = (
241 bytearray(self.secret_key) if self.secret_key else bytearray(32)
242 )
tiernoeef7cb72018-11-12 11:51:49 +0100243 for i, b in enumerate(update_key_bytes):
244 new_secret_key[i % 32] ^= b
245 return bytes(new_secret_key)
246
247 def set_secret_key(self, new_secret_key, replace=False):
tierno136f2952018-10-19 13:01:03 +0200248 """
tiernoeef7cb72018-11-12 11:51:49 +0100249 Updates internal secret_key used for encryption, with a byte xor
250 :param new_secret_key: string or byte array. It is recommended a 32 byte length
251 :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 +0200252 :return: None
253 """
tiernoeef7cb72018-11-12 11:51:49 +0100254 if replace:
255 self.secret_key = None
256 self.secret_key = self._join_secret_key(new_secret_key)
tierno136f2952018-10-19 13:01:03 +0200257
tiernoc5297e42019-12-11 12:32:41 +0000258 def get_secret_key(self):
259 """
260 Get the database secret key in case it is not done when "connect" is called. It can happens when database is
261 empty after an initial install. It should skip if secret is already obtained.
262 """
263 pass
264
tierno136f2952018-10-19 13:01:03 +0200265 def encrypt(self, value, schema_version=None, salt=None):
tierno87858ca2018-10-08 16:30:15 +0200266 """
267 Encrypt a value
tierno136f2952018-10-19 13:01:03 +0200268 :param value: value to be encrypted. It is string/unicode
269 :param schema_version: used for version control. If None or '1.0' no encryption is done.
270 If '1.1' symmetric AES encryption is done
271 :param salt: optional salt to be used. Must be str
tierno87858ca2018-10-08 16:30:15 +0200272 :return: Encrypted content of value
273 """
tiernoc5297e42019-12-11 12:32:41 +0000274 self.get_secret_key()
garciadeblas2644b762021-03-24 09:21:01 +0100275 if not self.secret_key or not schema_version or schema_version == "1.0":
tierno136f2952018-10-19 13:01:03 +0200276 return value
277 else:
tiernoeef7cb72018-11-12 11:51:49 +0100278 secret_key = self._join_secret_key(salt)
tierno136f2952018-10-19 13:01:03 +0200279 cipher = AES.new(secret_key)
garciadeblas2644b762021-03-24 09:21:01 +0100280 padded_private_msg = value + ("\0" * ((16 - len(value)) % 16))
tierno136f2952018-10-19 13:01:03 +0200281 encrypted_msg = cipher.encrypt(padded_private_msg)
282 encoded_encrypted_msg = b64encode(encrypted_msg)
283 return encoded_encrypted_msg.decode("ascii")
tierno87858ca2018-10-08 16:30:15 +0200284
tierno136f2952018-10-19 13:01:03 +0200285 def decrypt(self, value, schema_version=None, salt=None):
tierno87858ca2018-10-08 16:30:15 +0200286 """
287 Decrypt an encrypted value
tierno136f2952018-10-19 13:01:03 +0200288 :param value: value to be decrypted. It is a base64 string
289 :param schema_version: used for known encryption method used. If None or '1.0' no encryption has been done.
290 If '1.1' symmetric AES encryption has been done
tierno87858ca2018-10-08 16:30:15 +0200291 :param salt: optional salt to be used
292 :return: Plain content of value
293 """
tiernoc5297e42019-12-11 12:32:41 +0000294 self.get_secret_key()
garciadeblas2644b762021-03-24 09:21:01 +0100295 if not self.secret_key or not schema_version or schema_version == "1.0":
tierno136f2952018-10-19 13:01:03 +0200296 return value
297 else:
tiernoeef7cb72018-11-12 11:51:49 +0100298 secret_key = self._join_secret_key(salt)
tierno136f2952018-10-19 13:01:03 +0200299 encrypted_msg = b64decode(value)
300 cipher = AES.new(secret_key)
301 decrypted_msg = cipher.decrypt(encrypted_msg)
tiernobd5a4022019-01-30 09:48:38 +0000302 try:
garciadeblas2644b762021-03-24 09:21:01 +0100303 unpadded_private_msg = decrypted_msg.decode().rstrip("\0")
tiernobd5a4022019-01-30 09:48:38 +0000304 except UnicodeDecodeError:
garciadeblas2644b762021-03-24 09:21:01 +0100305 raise DbException(
306 "Cannot decrypt information. Are you using same COMMONKEY in all OSM components?",
307 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
308 )
tierno136f2952018-10-19 13:01:03 +0200309 return unpadded_private_msg
tierno87858ca2018-10-08 16:30:15 +0200310
garciadeblas2644b762021-03-24 09:21:01 +0100311 def encrypt_decrypt_fields(
312 self, item, action, fields=None, flags=None, schema_version=None, salt=None
313 ):
delacruzramo54a54642019-10-25 16:50:13 +0200314 if not fields:
315 return
tiernoc5297e42019-12-11 12:32:41 +0000316 self.get_secret_key()
garciadeblas2644b762021-03-24 09:21:01 +0100317 actions = ["encrypt", "decrypt"]
delacruzramo54a54642019-10-25 16:50:13 +0200318 if action.lower() not in actions:
garciadeblas2644b762021-03-24 09:21:01 +0100319 raise DbException(
320 "Unknown action ({}): Must be one of {}".format(action, actions),
321 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
322 )
323 method = self.encrypt if action.lower() == "encrypt" else self.decrypt
tiernoafc5cb62020-05-12 11:17:54 +0000324 if flags is None:
325 flags = re.I
delacruzramo54a54642019-10-25 16:50:13 +0200326
tiernoafc5cb62020-05-12 11:17:54 +0000327 def process(_item):
328 if isinstance(_item, list):
329 for elem in _item:
delacruzramo54a54642019-10-25 16:50:13 +0200330 process(elem)
tiernoafc5cb62020-05-12 11:17:54 +0000331 elif isinstance(_item, dict):
332 for key, val in _item.items():
333 if isinstance(val, str):
334 if any(re.search(f, key, flags) for f in fields):
335 _item[key] = method(val, schema_version, salt)
delacruzramo54a54642019-10-25 16:50:13 +0200336 else:
337 process(val)
garciadeblas2644b762021-03-24 09:21:01 +0100338
delacruzramo54a54642019-10-25 16:50:13 +0200339 process(item)
340
tierno87858ca2018-10-08 16:30:15 +0200341
342def deep_update_rfc7396(dict_to_change, dict_reference, key_list=None):
tiernob3e750b2018-09-05 11:25:23 +0200343 """
344 Modifies one dictionary with the information of the other following https://tools.ietf.org/html/rfc7396
345 Basically is a recursive python 'dict_to_change.update(dict_reference)', but a value of None is used to delete.
346 It implements an extra feature that allows modifying an array. RFC7396 only allows replacing the entire array.
347 For that, dict_reference should contains a dict with keys starting by "$" with the following meaning:
348 $[index] <index> is an integer for targeting a concrete index from dict_to_change array. If the value is None
349 the element of the array is deleted, otherwise it is edited.
350 $+[index] The value is inserted at this <index>. A value of None has not sense and an exception is raised.
351 $+ The value is appended at the end. A value of None has not sense and an exception is raised.
352 $val It looks for all the items in the array dict_to_change equal to <val>. <val> is evaluated as yaml,
353 that is, numbers are taken as type int, true/false as boolean, etc. Use quotes to force string.
354 Nothing happens if no match is found. If the value is None the matched elements are deleted.
355 $key: val In case a dictionary is passed in yaml format, if looks for all items in the array dict_to_change
356 that are dictionaries and contains this <key> equal to <val>. Several keys can be used by yaml
tierno3e759152019-08-28 16:08:25 +0000357 format '{key: val, key: val, ...}'; and all of them must match. Nothing happens if no match is
tiernob3e750b2018-09-05 11:25:23 +0200358 found. If value is None the matched items are deleted, otherwise they are edited.
359 $+val If no match if found (see '$val'), the value is appended to the array. If any match is found nothing
360 is changed. A value of None has not sense.
361 $+key: val If no match if found (see '$key: val'), the value is appended to the array. If any match is found
362 nothing is changed. A value of None has not sense.
363 If there are several editions, insertions and deletions; editions and deletions are done first in reverse index
364 order; then insertions also in reverse index order; and finally appends in any order. So indexes used at
365 insertions must take into account the deleted items.
366 :param dict_to_change: Target dictionary to be changed.
367 :param dict_reference: Dictionary that contains changes to be applied.
368 :param key_list: This is used internally for recursive calls. Do not fill this parameter.
369 :return: none or raises and exception only at array modification when there is a bad format or conflict.
370 """
garciadeblas2644b762021-03-24 09:21:01 +0100371
tiernob3e750b2018-09-05 11:25:23 +0200372 def _deep_update_array(array_to_change, _dict_reference, _key_list):
373 to_append = {}
374 to_insert_at_index = {}
375 values_to_edit_delete = {}
376 indexes_to_edit_delete = []
377 array_edition = None
378 _key_list.append("")
379 for k in _dict_reference:
380 _key_list[-1] = str(k)
381 if not isinstance(k, str) or not k.startswith("$"):
382 if array_edition is True:
garciadeblas2644b762021-03-24 09:21:01 +0100383 raise DbException(
384 "Found array edition (keys starting with '$') and pure dictionary edition in the"
385 " same dict at '{}'".format(":".join(_key_list[:-1]))
386 )
tiernob3e750b2018-09-05 11:25:23 +0200387 array_edition = False
388 continue
389 else:
390 if array_edition is False:
garciadeblas2644b762021-03-24 09:21:01 +0100391 raise DbException(
392 "Found array edition (keys starting with '$') and pure dictionary edition in the"
393 " same dict at '{}'".format(":".join(_key_list[:-1]))
394 )
tiernob3e750b2018-09-05 11:25:23 +0200395 array_edition = True
396 insert = False
397 indexes = [] # indexes to edit or insert
398 kitem = k[1:]
garciadeblas2644b762021-03-24 09:21:01 +0100399 if kitem.startswith("+"):
tiernob3e750b2018-09-05 11:25:23 +0200400 insert = True
401 kitem = kitem[1:]
402 if _dict_reference[k] is None:
garciadeblas2644b762021-03-24 09:21:01 +0100403 raise DbException(
404 "A value of None has not sense for insertions at '{}'".format(
405 ":".join(_key_list)
406 )
407 )
tiernob3e750b2018-09-05 11:25:23 +0200408
garciadeblas2644b762021-03-24 09:21:01 +0100409 if kitem.startswith("[") and kitem.endswith("]"):
tiernob3e750b2018-09-05 11:25:23 +0200410 try:
411 index = int(kitem[1:-1])
412 if index < 0:
413 index += len(array_to_change)
414 if index < 0:
415 index = 0 # skip outside index edition
416 indexes.append(index)
417 except Exception:
garciadeblas2644b762021-03-24 09:21:01 +0100418 raise DbException(
419 "Wrong format at '{}'. Expecting integer index inside quotes".format(
420 ":".join(_key_list)
421 )
422 )
tiernob3e750b2018-09-05 11:25:23 +0200423 elif kitem:
424 # match_found_skip = False
425 try:
426 filter_in = yaml.safe_load(kitem)
427 except Exception:
garciadeblas2644b762021-03-24 09:21:01 +0100428 raise DbException(
429 "Wrong format at '{}'. Expecting '$<yaml-format>'".format(
430 ":".join(_key_list)
431 )
432 )
tiernob3e750b2018-09-05 11:25:23 +0200433 if isinstance(filter_in, dict):
434 for index, item in enumerate(array_to_change):
435 for filter_k, filter_v in filter_in.items():
garciadeblas2644b762021-03-24 09:21:01 +0100436 if (
437 not isinstance(item, dict)
438 or filter_k not in item
439 or item[filter_k] != filter_v
440 ):
tiernob3e750b2018-09-05 11:25:23 +0200441 break
442 else: # match found
443 if insert:
444 # match_found_skip = True
445 insert = False
446 break
447 else:
448 indexes.append(index)
449 else:
450 index = 0
451 try:
452 while True: # if not match a ValueError exception will be raise
453 index = array_to_change.index(filter_in, index)
454 if insert:
455 # match_found_skip = True
456 insert = False
457 break
458 indexes.append(index)
459 index += 1
460 except ValueError:
461 pass
462
463 # if match_found_skip:
464 # continue
465 elif not insert:
garciadeblas2644b762021-03-24 09:21:01 +0100466 raise DbException(
467 "Wrong format at '{}'. Expecting '$+', '$[<index]' or '$[<filter>]'".format(
468 ":".join(_key_list)
469 )
470 )
tiernob3e750b2018-09-05 11:25:23 +0200471 for index in indexes:
472 if insert:
garciadeblas2644b762021-03-24 09:21:01 +0100473 if (
474 index in to_insert_at_index
475 and to_insert_at_index[index] != _dict_reference[k]
476 ):
tiernob3e750b2018-09-05 11:25:23 +0200477 # Several different insertions on the same item of the array
garciadeblas2644b762021-03-24 09:21:01 +0100478 raise DbException(
479 "Conflict at '{}'. Several insertions on same array index {}".format(
480 ":".join(_key_list), index
481 )
482 )
tiernob3e750b2018-09-05 11:25:23 +0200483 to_insert_at_index[index] = _dict_reference[k]
484 else:
garciadeblas2644b762021-03-24 09:21:01 +0100485 if (
486 index in indexes_to_edit_delete
487 and values_to_edit_delete[index] != _dict_reference[k]
488 ):
tiernob3e750b2018-09-05 11:25:23 +0200489 # Several different editions on the same item of the array
garciadeblas2644b762021-03-24 09:21:01 +0100490 raise DbException(
491 "Conflict at '{}'. Several editions on array index {}".format(
492 ":".join(_key_list), index
493 )
494 )
tiernob3e750b2018-09-05 11:25:23 +0200495 indexes_to_edit_delete.append(index)
496 values_to_edit_delete[index] = _dict_reference[k]
497 if not indexes:
498 if insert:
499 to_append[k] = _dict_reference[k]
500 # elif _dict_reference[k] is not None:
501 # raise DbException("Not found any match to edit in the array, or wrong format at '{}'".format(
502 # ":".join(_key_list)))
503
504 # edition/deletion is done before insertion
505 indexes_to_edit_delete.sort(reverse=True)
506 for index in indexes_to_edit_delete:
507 _key_list[-1] = str(index)
508 try:
509 if values_to_edit_delete[index] is None: # None->Anything
510 try:
garciadeblas2644b762021-03-24 09:21:01 +0100511 del array_to_change[index]
tiernob3e750b2018-09-05 11:25:23 +0200512 except IndexError:
513 pass # it is not consider an error if this index does not exist
garciadeblas2644b762021-03-24 09:21:01 +0100514 elif not isinstance(
515 values_to_edit_delete[index], dict
516 ): # NotDict->Anything
tiernob3e750b2018-09-05 11:25:23 +0200517 array_to_change[index] = deepcopy(values_to_edit_delete[index])
518 elif isinstance(array_to_change[index], dict): # Dict->Dict
garciadeblas2644b762021-03-24 09:21:01 +0100519 deep_update_rfc7396(
520 array_to_change[index], values_to_edit_delete[index], _key_list
521 )
tiernob3e750b2018-09-05 11:25:23 +0200522 else: # Dict->NotDict
garciadeblas2644b762021-03-24 09:21:01 +0100523 if isinstance(
524 array_to_change[index], list
525 ): # Dict->List. Check extra array edition
526 if _deep_update_array(
527 array_to_change[index],
528 values_to_edit_delete[index],
529 _key_list,
530 ):
tiernob3e750b2018-09-05 11:25:23 +0200531 continue
532 array_to_change[index] = deepcopy(values_to_edit_delete[index])
tierno87858ca2018-10-08 16:30:15 +0200533 # calling deep_update_rfc7396 to delete the None values
garciadeblas2644b762021-03-24 09:21:01 +0100534 deep_update_rfc7396(
535 array_to_change[index], values_to_edit_delete[index], _key_list
536 )
tiernob3e750b2018-09-05 11:25:23 +0200537 except IndexError:
garciadeblas2644b762021-03-24 09:21:01 +0100538 raise DbException(
539 "Array edition index out of range at '{}'".format(
540 ":".join(_key_list)
541 )
542 )
tiernob3e750b2018-09-05 11:25:23 +0200543
544 # insertion with indexes
545 to_insert_indexes = list(to_insert_at_index.keys())
546 to_insert_indexes.sort(reverse=True)
547 for index in to_insert_indexes:
548 array_to_change.insert(index, to_insert_at_index[index])
549
550 # append
551 for k, insert_value in to_append.items():
552 _key_list[-1] = str(k)
553 insert_value_copy = deepcopy(insert_value)
554 if isinstance(insert_value_copy, dict):
tierno87858ca2018-10-08 16:30:15 +0200555 # calling deep_update_rfc7396 to delete the None values
556 deep_update_rfc7396(insert_value_copy, insert_value, _key_list)
tiernob3e750b2018-09-05 11:25:23 +0200557 array_to_change.append(insert_value_copy)
558
559 _key_list.pop()
560 if array_edition:
561 return True
562 return False
563
564 if key_list is None:
565 key_list = []
566 key_list.append("")
567 for k in dict_reference:
568 key_list[-1] = str(k)
garciadeblas2644b762021-03-24 09:21:01 +0100569 if dict_reference[k] is None: # None->Anything
tiernob3e750b2018-09-05 11:25:23 +0200570 if k in dict_to_change:
571 del dict_to_change[k]
572 elif not isinstance(dict_reference[k], dict): # NotDict->Anything
573 dict_to_change[k] = deepcopy(dict_reference[k])
574 elif k not in dict_to_change: # Dict->Empty
575 dict_to_change[k] = deepcopy(dict_reference[k])
tierno87858ca2018-10-08 16:30:15 +0200576 # calling deep_update_rfc7396 to delete the None values
577 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
tiernob3e750b2018-09-05 11:25:23 +0200578 elif isinstance(dict_to_change[k], dict): # Dict->Dict
tierno87858ca2018-10-08 16:30:15 +0200579 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
garciadeblas2644b762021-03-24 09:21:01 +0100580 else: # Dict->NotDict
581 if isinstance(
582 dict_to_change[k], list
583 ): # Dict->List. Check extra array edition
tiernob3e750b2018-09-05 11:25:23 +0200584 if _deep_update_array(dict_to_change[k], dict_reference[k], key_list):
585 continue
586 dict_to_change[k] = deepcopy(dict_reference[k])
tierno87858ca2018-10-08 16:30:15 +0200587 # calling deep_update_rfc7396 to delete the None values
588 deep_update_rfc7396(dict_to_change[k], dict_reference[k], key_list)
tiernob3e750b2018-09-05 11:25:23 +0200589 key_list.pop()
tierno87858ca2018-10-08 16:30:15 +0200590
591
592def deep_update(dict_to_change, dict_reference):
garciadeblas2644b762021-03-24 09:21:01 +0100593 """Maintained for backward compatibility. Use deep_update_rfc7396 instead"""
tierno87858ca2018-10-08 16:30:15 +0200594 return deep_update_rfc7396(dict_to_change, dict_reference)