blob: d8b373a47783d9b853549ca1d1c9009aac680f04 [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
tierno5c012612018-04-19 16:01:59 +020018
19import logging
20from pymongo import MongoClient, errors
tierno3054f782018-04-25 16:59:53 +020021from osm_common.dbbase import DbException, DbBase
tierno5c012612018-04-19 16:01:59 +020022from http import HTTPStatus
23from time import time, sleep
tierno6ec13b02018-05-14 11:24:57 +020024from copy import deepcopy
tierno136f2952018-10-19 13:01:03 +020025from base64 import b64decode
tierno2c9794c2020-04-29 10:24:28 +000026from uuid import uuid4
tierno5c012612018-04-19 16:01:59 +020027
28__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
29
30# TODO consider use this decorator for database access retries
31# @retry_mongocall
32# def retry_mongocall(call):
33# def _retry_mongocall(*args, **kwargs):
34# retry = 1
35# while True:
36# try:
37# return call(*args, **kwargs)
38# except pymongo.AutoReconnect as e:
39# if retry == 4:
tierno87858ca2018-10-08 16:30:15 +020040# raise DbException(e)
tierno5c012612018-04-19 16:01:59 +020041# sleep(retry)
42# return _retry_mongocall
43
44
tierno6ec13b02018-05-14 11:24:57 +020045def deep_update(to_update, update_with):
46 """
tierno87858ca2018-10-08 16:30:15 +020047 Similar to deepcopy but recursively with nested dictionaries. 'to_update' dict is updated with a content copy of
48 'update_with' dict recursively
tierno6ec13b02018-05-14 11:24:57 +020049 :param to_update: must be a dictionary to be modified
50 :param update_with: must be a dictionary. It is not changed
51 :return: to_update
52 """
53 for key in update_with:
54 if key in to_update:
55 if isinstance(to_update[key], dict) and isinstance(update_with[key], dict):
56 deep_update(to_update[key], update_with[key])
57 continue
58 to_update[key] = deepcopy(update_with[key])
59 return to_update
60
61
tierno5c012612018-04-19 16:01:59 +020062class DbMongo(DbBase):
63 conn_initial_timout = 120
64 conn_timout = 10
65
garciadeblas2644b762021-03-24 09:21:01 +010066 def __init__(self, logger_name="db", lock=False):
tierno1e9a3292018-11-05 18:18:45 +010067 super().__init__(logger_name, lock)
tierno87858ca2018-10-08 16:30:15 +020068 self.client = None
69 self.db = None
tiernoc5297e42019-12-11 12:32:41 +000070 self.database_key = None
71 self.secret_obtained = False
72 # ^ This is used to know if database serial has been got. Database is inited by NBI, who generates the serial
73 # In case it is not ready when connected, it should be got later on before any decrypt operation
74
75 def get_secret_key(self):
76 if self.secret_obtained:
77 return
78
79 self.secret_key = None
80 if self.database_key:
81 self.set_secret_key(self.database_key)
garciadeblas2644b762021-03-24 09:21:01 +010082 version_data = self.get_one(
83 "admin", {"_id": "version"}, fail_on_empty=False, fail_on_more=True
84 )
tiernoc5297e42019-12-11 12:32:41 +000085 if version_data and version_data.get("serial"):
86 self.set_secret_key(b64decode(version_data["serial"]))
87 self.secret_obtained = True
tierno5c012612018-04-19 16:01:59 +020088
tierno136f2952018-10-19 13:01:03 +020089 def db_connect(self, config, target_version=None):
tierno87858ca2018-10-08 16:30:15 +020090 """
91 Connect to database
92 :param config: Configuration of database
tierno136f2952018-10-19 13:01:03 +020093 :param target_version: if provided it checks if database contains required version, raising exception otherwise.
tierno87858ca2018-10-08 16:30:15 +020094 :return: None or raises DbException on error
95 """
tierno5c012612018-04-19 16:01:59 +020096 try:
97 if "logger_name" in config:
98 self.logger = logging.getLogger(config["logger_name"])
tiernoeef7cb72018-11-12 11:51:49 +010099 master_key = config.get("commonkey") or config.get("masterpassword")
100 if master_key:
tiernoc5297e42019-12-11 12:32:41 +0000101 self.database_key = master_key
tiernoeef7cb72018-11-12 11:51:49 +0100102 self.set_secret_key(master_key)
Juanc837a782018-11-16 10:47:46 -0300103 if config.get("uri"):
garciadeblas2644b762021-03-24 09:21:01 +0100104 self.client = MongoClient(
105 config["uri"], replicaSet=config.get("replicaset", None)
106 )
Juan89e933d2018-11-12 16:17:08 -0300107 else:
garciadeblas2644b762021-03-24 09:21:01 +0100108 self.client = MongoClient(
109 config["host"],
110 config["port"],
111 replicaSet=config.get("replicaset", None),
112 )
tiernocfc52722018-10-23 11:41:49 +0200113 # TODO add as parameters also username=config.get("user"), password=config.get("password"))
114 # when all modules are ready
tierno5c012612018-04-19 16:01:59 +0200115 self.db = self.client[config["name"]]
116 if "loglevel" in config:
garciadeblas2644b762021-03-24 09:21:01 +0100117 self.logger.setLevel(getattr(logging, config["loglevel"]))
tierno5c012612018-04-19 16:01:59 +0200118 # get data to try a connection
119 now = time()
120 while True:
121 try:
garciadeblas2644b762021-03-24 09:21:01 +0100122 version_data = self.get_one(
123 "admin",
124 {"_id": "version"},
125 fail_on_empty=False,
126 fail_on_more=True,
127 )
tierno136f2952018-10-19 13:01:03 +0200128 # check database status is ok
garciadeblas2644b762021-03-24 09:21:01 +0100129 if version_data and version_data.get("status") != "ENABLED":
130 raise DbException(
131 "Wrong database status '{}'".format(
132 version_data.get("status")
133 ),
134 http_code=HTTPStatus.INTERNAL_SERVER_ERROR,
135 )
tierno136f2952018-10-19 13:01:03 +0200136 # check version
garciadeblas2644b762021-03-24 09:21:01 +0100137 db_version = (
138 None if not version_data else version_data.get("version")
139 )
tierno136f2952018-10-19 13:01:03 +0200140 if target_version and target_version != db_version:
garciadeblas2644b762021-03-24 09:21:01 +0100141 raise DbException(
142 "Invalid database version {}. Expected {}".format(
143 db_version, target_version
144 )
145 )
tierno136f2952018-10-19 13:01:03 +0200146 # get serial
147 if version_data and version_data.get("serial"):
tiernoc5297e42019-12-11 12:32:41 +0000148 self.secret_obtained = True
tierno136f2952018-10-19 13:01:03 +0200149 self.set_secret_key(b64decode(version_data["serial"]))
garciadeblas2644b762021-03-24 09:21:01 +0100150 self.logger.info(
151 "Connected to database {} version {}".format(
152 config["name"], db_version
153 )
154 )
tierno5c012612018-04-19 16:01:59 +0200155 return
156 except errors.ConnectionFailure as e:
157 if time() - now >= self.conn_initial_timout:
158 raise
159 self.logger.info("Waiting to database up {}".format(e))
160 sleep(2)
161 except errors.PyMongoError as e:
tierno87858ca2018-10-08 16:30:15 +0200162 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200163
164 @staticmethod
tierno6ec13b02018-05-14 11:24:57 +0200165 def _format_filter(q_filter):
166 """
tierno87858ca2018-10-08 16:30:15 +0200167 Translate query string q_filter into mongo database filter
tierno6ec13b02018-05-14 11:24:57 +0200168 :param q_filter: Query string content. Follows SOL005 section 4.3.2 guidelines, with the follow extensions and
tiernoaf241062018-08-31 14:53:15 +0200169 differences:
170 It accept ".nq" (not equal) in addition to ".neq".
171 For arrays you can specify index (concrete index must match), nothing (any index may match) or 'ANYINDEX'
172 (two or more matches applies for the same array element). Examples:
173 with database register: {A: [{B: 1, C: 2}, {B: 6, C: 9}]}
174 query 'A.B=6' matches because array A contains one element with B equal to 6
175 query 'A.0.B=6' does no match because index 0 of array A contains B with value 1, but not 6
176 query 'A.B=6&A.C=2' matches because one element of array matches B=6 and other matchesC=2
177 query 'A.ANYINDEX.B=6&A.ANYINDEX.C=2' does not match because it is needed the same element of the
178 array matching both
179
180 Examples of translations from SOL005 to >> mongo # comment
181 A=B; A.eq=B >> A: B # must contain key A and equal to B or be a list that contains B
182 A.cont=B >> A: B
183 A=B&A=C; A=B,C >> A: {$in: [B, C]} # must contain key A and equal to B or C or be a list that contains
184 # B or C
185 A.cont=B&A.cont=C; A.cont=B,C >> A: {$in: [B, C]}
186 A.ncont=B >> A: {$nin: B} # must not contain key A or if present not equal to B or if a list,
187 # it must not not contain B
188 A.ncont=B,C; A.ncont=B&A.ncont=C >> A: {$nin: [B,C]} # must not contain key A or if present not equal
189 # neither B nor C; or if a list, it must not contain neither B nor C
190 A.ne=B&A.ne=C; A.ne=B,C >> A: {$nin: [B, C]}
191 A.gt=B >> A: {$gt: B} # must contain key A and greater than B
192 A.ne=B; A.neq=B >> A: {$ne: B} # must not contain key A or if present not equal to B, or if
193 # an array not contain B
194 A.ANYINDEX.B=C >> A: {$elemMatch: {B=C}
tierno6ec13b02018-05-14 11:24:57 +0200195 :return: database mongo filter
196 """
tierno5c012612018-04-19 16:01:59 +0200197 try:
198 db_filter = {}
tierno87858ca2018-10-08 16:30:15 +0200199 if not q_filter:
200 return db_filter
tierno6ec13b02018-05-14 11:24:57 +0200201 for query_k, query_v in q_filter.items():
tierno5c012612018-04-19 16:01:59 +0200202 dot_index = query_k.rfind(".")
garciadeblas2644b762021-03-24 09:21:01 +0100203 if dot_index > 1 and query_k[dot_index + 1 :] in (
204 "eq",
205 "ne",
206 "gt",
207 "gte",
208 "lt",
209 "lte",
210 "cont",
211 "ncont",
212 "neq",
213 ):
214 operator = "$" + query_k[dot_index + 1 :]
tierno5c012612018-04-19 16:01:59 +0200215 if operator == "$neq":
216 operator = "$ne"
217 k = query_k[:dot_index]
218 else:
219 operator = "$eq"
220 k = query_k
221
222 v = query_v
223 if isinstance(v, list):
224 if operator in ("$eq", "$cont"):
225 operator = "$in"
226 v = query_v
227 elif operator in ("$ne", "$ncont"):
228 operator = "$nin"
229 v = query_v
230 else:
231 v = query_v.join(",")
232
233 if operator in ("$eq", "$cont"):
234 # v cannot be a comma separated list, because operator would have been changed to $in
tierno6ec13b02018-05-14 11:24:57 +0200235 db_v = v
tierno5c012612018-04-19 16:01:59 +0200236 elif operator == "$ncount":
237 # v cannot be a comma separated list, because operator would have been changed to $nin
tierno6ec13b02018-05-14 11:24:57 +0200238 db_v = {"$ne": v}
tierno5c012612018-04-19 16:01:59 +0200239 else:
tierno6ec13b02018-05-14 11:24:57 +0200240 db_v = {operator: v}
241
tiernoaf241062018-08-31 14:53:15 +0200242 # process the ANYINDEX word at k.
tierno6ec13b02018-05-14 11:24:57 +0200243 kleft, _, kright = k.rpartition(".ANYINDEX.")
244 while kleft:
245 k = kleft
246 db_v = {"$elemMatch": {kright: db_v}}
247 kleft, _, kright = k.rpartition(".ANYINDEX.")
248
249 # insert in db_filter
250 # maybe db_filter[k] exist. e.g. in the query string for values between 5 and 8: "a.gt=5&a.lt=8"
251 deep_update(db_filter, {k: db_v})
tierno5c012612018-04-19 16:01:59 +0200252
253 return db_filter
254 except Exception as e:
garciadeblas2644b762021-03-24 09:21:01 +0100255 raise DbException(
256 "Invalid query string filter at {}:{}. Error: {}".format(query_k, v, e),
257 http_code=HTTPStatus.BAD_REQUEST,
258 )
tierno5c012612018-04-19 16:01:59 +0200259
tierno87858ca2018-10-08 16:30:15 +0200260 def get_list(self, table, q_filter=None):
261 """
262 Obtain a list of entries matching q_filter
263 :param table: collection or table
264 :param q_filter: Filter
265 :return: a list (can be empty) with the found entries. Raises DbException on error
266 """
tierno5c012612018-04-19 16:01:59 +0200267 try:
tiernob20a9022018-05-22 12:07:05 +0200268 result = []
tierno1e9a3292018-11-05 18:18:45 +0100269 with self.lock:
270 collection = self.db[table]
271 db_filter = self._format_filter(q_filter)
272 rows = collection.find(db_filter)
tierno5c012612018-04-19 16:01:59 +0200273 for row in rows:
tiernob20a9022018-05-22 12:07:05 +0200274 result.append(row)
275 return result
tierno5c012612018-04-19 16:01:59 +0200276 except DbException:
277 raise
278 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200279 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200280
delacruzramoae049d82019-09-17 16:05:17 +0200281 def count(self, table, q_filter=None):
282 """
283 Count the number of entries matching q_filter
284 :param table: collection or table
285 :param q_filter: Filter
286 :return: number of entries found (can be zero)
287 :raise: DbException on error
288 """
289 try:
290 with self.lock:
291 collection = self.db[table]
292 db_filter = self._format_filter(q_filter)
293 count = collection.count(db_filter)
294 return count
295 except DbException:
296 raise
297 except Exception as e: # TODO refine
298 raise DbException(e)
299
tierno87858ca2018-10-08 16:30:15 +0200300 def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True):
301 """
302 Obtain one entry matching q_filter
303 :param table: collection or table
304 :param q_filter: Filter
305 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
306 it raises a DbException
307 :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so
308 that it raises a DbException
309 :return: The requested element, or None
310 """
tierno5c012612018-04-19 16:01:59 +0200311 try:
tierno87858ca2018-10-08 16:30:15 +0200312 db_filter = self._format_filter(q_filter)
tierno1e9a3292018-11-05 18:18:45 +0100313 with self.lock:
314 collection = self.db[table]
315 if not (fail_on_empty and fail_on_more):
316 return collection.find_one(db_filter)
317 rows = collection.find(db_filter)
tierno5c012612018-04-19 16:01:59 +0200318 if rows.count() == 0:
319 if fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100320 raise DbException(
321 "Not found any {} with filter='{}'".format(
322 table[:-1], q_filter
323 ),
324 HTTPStatus.NOT_FOUND,
325 )
tierno5c012612018-04-19 16:01:59 +0200326 return None
327 elif rows.count() > 1:
328 if fail_on_more:
garciadeblas2644b762021-03-24 09:21:01 +0100329 raise DbException(
330 "Found more than one {} with filter='{}'".format(
331 table[:-1], q_filter
332 ),
333 HTTPStatus.CONFLICT,
334 )
tierno5c012612018-04-19 16:01:59 +0200335 return rows[0]
336 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200337 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200338
tierno87858ca2018-10-08 16:30:15 +0200339 def del_list(self, table, q_filter=None):
340 """
341 Deletes all entries that match q_filter
342 :param table: collection or table
343 :param q_filter: Filter
344 :return: Dict with the number of entries deleted
345 """
tierno5c012612018-04-19 16:01:59 +0200346 try:
tierno1e9a3292018-11-05 18:18:45 +0100347 with self.lock:
348 collection = self.db[table]
349 rows = collection.delete_many(self._format_filter(q_filter))
tierno5c012612018-04-19 16:01:59 +0200350 return {"deleted": rows.deleted_count}
351 except DbException:
352 raise
353 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200354 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200355
tierno87858ca2018-10-08 16:30:15 +0200356 def del_one(self, table, q_filter=None, fail_on_empty=True):
357 """
358 Deletes one entry that matches q_filter
359 :param table: collection or table
360 :param q_filter: Filter
361 :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in
362 which case it raises a DbException
363 :return: Dict with the number of entries deleted
364 """
tierno5c012612018-04-19 16:01:59 +0200365 try:
tierno1e9a3292018-11-05 18:18:45 +0100366 with self.lock:
367 collection = self.db[table]
368 rows = collection.delete_one(self._format_filter(q_filter))
tierno5c012612018-04-19 16:01:59 +0200369 if rows.deleted_count == 0:
370 if fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100371 raise DbException(
372 "Not found any {} with filter='{}'".format(
373 table[:-1], q_filter
374 ),
375 HTTPStatus.NOT_FOUND,
376 )
tierno5c012612018-04-19 16:01:59 +0200377 return None
378 return {"deleted": rows.deleted_count}
379 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200380 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200381
382 def create(self, table, indata):
tierno87858ca2018-10-08 16:30:15 +0200383 """
384 Add a new entry at database
385 :param table: collection or table
386 :param indata: content to be added
387 :return: database id of the inserted element. Raises a DbException on error
388 """
tierno5c012612018-04-19 16:01:59 +0200389 try:
tierno1e9a3292018-11-05 18:18:45 +0100390 with self.lock:
391 collection = self.db[table]
392 data = collection.insert_one(indata)
tierno5c012612018-04-19 16:01:59 +0200393 return data.inserted_id
394 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200395 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200396
tierno2c9794c2020-04-29 10:24:28 +0000397 def create_list(self, table, indata_list):
398 """
399 Add several entries at once
400 :param table: collection or table
401 :param indata_list: content list to be added.
402 :return: the list of inserted '_id's. Exception on error
403 """
404 try:
405 for item in indata_list:
406 if item.get("_id") is None:
407 item["_id"] = str(uuid4())
408 with self.lock:
409 collection = self.db[table]
410 data = collection.insert_many(indata_list)
411 return data.inserted_ids
412 except Exception as e: # TODO refine
413 raise DbException(e)
414
garciadeblas2644b762021-03-24 09:21:01 +0100415 def set_one(
416 self,
417 table,
418 q_filter,
419 update_dict,
420 fail_on_empty=True,
421 unset=None,
422 pull=None,
423 push=None,
424 push_list=None,
425 pull_list=None,
426 upsert=False,
427 ):
tierno87858ca2018-10-08 16:30:15 +0200428 """
429 Modifies an entry at database
430 :param table: collection or table
431 :param q_filter: Filter
432 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
bravof722a3202021-01-15 11:54:45 -0300433 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set to True, in which case
tierno87858ca2018-10-08 16:30:15 +0200434 it raises a DbException
tiernod63ea272018-11-27 12:03:36 +0100435 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
436 ignored. If not exist, it is ignored
437 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
438 if exist in the array is removed. If not exist, it is ignored
tierno0d8e4bc2020-06-22 12:18:18 +0000439 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tiernod63ea272018-11-27 12:03:36 +0100440 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
441 is appended to the end of the array
tierno399f6c32020-05-12 07:36:41 +0000442 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
443 whole array
bravof722a3202021-01-15 11:54:45 -0300444 :param upsert: If this parameter is set to True and no document is found using 'q_filter' it will be created.
445 By default this is false.
tierno87858ca2018-10-08 16:30:15 +0200446 :return: Dict with the number of entries modified. None if no matching is found.
447 """
tierno5c012612018-04-19 16:01:59 +0200448 try:
tiernod63ea272018-11-27 12:03:36 +0100449 db_oper = {}
450 if update_dict:
451 db_oper["$set"] = update_dict
452 if unset:
453 db_oper["$unset"] = unset
tierno0d8e4bc2020-06-22 12:18:18 +0000454 if pull or pull_list:
455 db_oper["$pull"] = pull or {}
456 if pull_list:
garciadeblas2644b762021-03-24 09:21:01 +0100457 db_oper["$pull"].update(
458 {k: {"$in": v} for k, v in pull_list.items()}
459 )
tierno399f6c32020-05-12 07:36:41 +0000460 if push or push_list:
461 db_oper["$push"] = push or {}
462 if push_list:
garciadeblas2644b762021-03-24 09:21:01 +0100463 db_oper["$push"].update(
464 {k: {"$each": v} for k, v in push_list.items()}
465 )
tiernod63ea272018-11-27 12:03:36 +0100466
tierno1e9a3292018-11-05 18:18:45 +0100467 with self.lock:
468 collection = self.db[table]
garciadeblas2644b762021-03-24 09:21:01 +0100469 rows = collection.update_one(
470 self._format_filter(q_filter), db_oper, upsert=upsert
471 )
tierno3054f782018-04-25 16:59:53 +0200472 if rows.matched_count == 0:
tierno5c012612018-04-19 16:01:59 +0200473 if fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100474 raise DbException(
475 "Not found any {} with filter='{}'".format(
476 table[:-1], q_filter
477 ),
478 HTTPStatus.NOT_FOUND,
479 )
tierno5c012612018-04-19 16:01:59 +0200480 return None
tierno3054f782018-04-25 16:59:53 +0200481 return {"modified": rows.modified_count}
tierno5c012612018-04-19 16:01:59 +0200482 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200483 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200484
garciadeblas2644b762021-03-24 09:21:01 +0100485 def set_list(
486 self,
487 table,
488 q_filter,
489 update_dict,
490 unset=None,
491 pull=None,
492 push=None,
493 push_list=None,
494 pull_list=None,
495 ):
tierno87858ca2018-10-08 16:30:15 +0200496 """
497 Modifies al matching entries at database
498 :param table: collection or table
499 :param q_filter: Filter
500 :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 +0000501 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
502 ignored. If not exist, it is ignored
503 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
504 if exist in the array is removed. If not exist, it is ignored
tierno399f6c32020-05-12 07:36:41 +0000505 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys, the
506 single value is appended to the end of the array
tierno0d8e4bc2020-06-22 12:18:18 +0000507 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno399f6c32020-05-12 07:36:41 +0000508 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
509 whole array
tierno87858ca2018-10-08 16:30:15 +0200510 :return: Dict with the number of entries modified
511 """
tierno5c012612018-04-19 16:01:59 +0200512 try:
delacruzramof71fcff2020-02-11 11:14:07 +0000513 db_oper = {}
514 if update_dict:
515 db_oper["$set"] = update_dict
516 if unset:
517 db_oper["$unset"] = unset
tierno0d8e4bc2020-06-22 12:18:18 +0000518 if pull or pull_list:
519 db_oper["$pull"] = pull or {}
520 if pull_list:
garciadeblas2644b762021-03-24 09:21:01 +0100521 db_oper["$pull"].update(
522 {k: {"$in": v} for k, v in pull_list.items()}
523 )
tierno399f6c32020-05-12 07:36:41 +0000524 if push or push_list:
525 db_oper["$push"] = push or {}
526 if push_list:
garciadeblas2644b762021-03-24 09:21:01 +0100527 db_oper["$push"].update(
528 {k: {"$each": v} for k, v in push_list.items()}
529 )
tierno1e9a3292018-11-05 18:18:45 +0100530 with self.lock:
531 collection = self.db[table]
delacruzramof71fcff2020-02-11 11:14:07 +0000532 rows = collection.update_many(self._format_filter(q_filter), db_oper)
tierno87858ca2018-10-08 16:30:15 +0200533 return {"modified": rows.modified_count}
534 except Exception as e: # TODO refine
535 raise DbException(e)
536
537 def replace(self, table, _id, indata, fail_on_empty=True):
538 """
539 Replace the content of an entry
540 :param table: collection or table
541 :param _id: internal database id
542 :param indata: content to replace
543 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
544 it raises a DbException
545 :return: Dict with the number of entries replaced
546 """
547 try:
548 db_filter = {"_id": _id}
tierno1e9a3292018-11-05 18:18:45 +0100549 with self.lock:
550 collection = self.db[table]
551 rows = collection.replace_one(db_filter, indata)
tierno5c012612018-04-19 16:01:59 +0200552 if rows.matched_count == 0:
553 if fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100554 raise DbException(
555 "Not found any {} with _id='{}'".format(table[:-1], _id),
556 HTTPStatus.NOT_FOUND,
557 )
tierno5c012612018-04-19 16:01:59 +0200558 return None
tierno3054f782018-04-25 16:59:53 +0200559 return {"replaced": rows.modified_count}
tierno5c012612018-04-19 16:01:59 +0200560 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200561 raise DbException(e)