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