blob: b0686aa713fa12c4cded0da81dfb0ce81a6b2b40 [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
tierno1e9a3292018-11-05 18:18:45 +010066 def __init__(self, logger_name='db', lock=False):
67 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)
82 version_data = self.get_one("admin", {"_id": "version"}, fail_on_empty=False, fail_on_more=True)
83 if version_data and version_data.get("serial"):
84 self.set_secret_key(b64decode(version_data["serial"]))
85 self.secret_obtained = True
tierno5c012612018-04-19 16:01:59 +020086
tierno136f2952018-10-19 13:01:03 +020087 def db_connect(self, config, target_version=None):
tierno87858ca2018-10-08 16:30:15 +020088 """
89 Connect to database
90 :param config: Configuration of database
tierno136f2952018-10-19 13:01:03 +020091 :param target_version: if provided it checks if database contains required version, raising exception otherwise.
tierno87858ca2018-10-08 16:30:15 +020092 :return: None or raises DbException on error
93 """
tierno5c012612018-04-19 16:01:59 +020094 try:
95 if "logger_name" in config:
96 self.logger = logging.getLogger(config["logger_name"])
tiernoeef7cb72018-11-12 11:51:49 +010097 master_key = config.get("commonkey") or config.get("masterpassword")
98 if master_key:
tiernoc5297e42019-12-11 12:32:41 +000099 self.database_key = master_key
tiernoeef7cb72018-11-12 11:51:49 +0100100 self.set_secret_key(master_key)
Juanc837a782018-11-16 10:47:46 -0300101 if config.get("uri"):
102 self.client = MongoClient(config["uri"])
Juan89e933d2018-11-12 16:17:08 -0300103 else:
104 self.client = MongoClient(config["host"], config["port"])
tiernocfc52722018-10-23 11:41:49 +0200105 # TODO add as parameters also username=config.get("user"), password=config.get("password"))
106 # when all modules are ready
tierno5c012612018-04-19 16:01:59 +0200107 self.db = self.client[config["name"]]
108 if "loglevel" in config:
109 self.logger.setLevel(getattr(logging, config['loglevel']))
110 # get data to try a connection
111 now = time()
112 while True:
113 try:
tierno136f2952018-10-19 13:01:03 +0200114 version_data = self.get_one("admin", {"_id": "version"}, fail_on_empty=False, fail_on_more=True)
115 # check database status is ok
116 if version_data and version_data.get("status") != 'ENABLED':
117 raise DbException("Wrong database status '{}'".format(version_data.get("status")),
118 http_code=HTTPStatus.INTERNAL_SERVER_ERROR)
119 # check version
120 db_version = None if not version_data else version_data.get("version")
121 if target_version and target_version != db_version:
122 raise DbException("Invalid database version {}. Expected {}".format(db_version, target_version))
123 # get serial
124 if version_data and version_data.get("serial"):
tiernoc5297e42019-12-11 12:32:41 +0000125 self.secret_obtained = True
tierno136f2952018-10-19 13:01:03 +0200126 self.set_secret_key(b64decode(version_data["serial"]))
127 self.logger.info("Connected to database {} version {}".format(config["name"], db_version))
tierno5c012612018-04-19 16:01:59 +0200128 return
129 except errors.ConnectionFailure as e:
130 if time() - now >= self.conn_initial_timout:
131 raise
132 self.logger.info("Waiting to database up {}".format(e))
133 sleep(2)
134 except errors.PyMongoError as e:
tierno87858ca2018-10-08 16:30:15 +0200135 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200136
137 @staticmethod
tierno6ec13b02018-05-14 11:24:57 +0200138 def _format_filter(q_filter):
139 """
tierno87858ca2018-10-08 16:30:15 +0200140 Translate query string q_filter into mongo database filter
tierno6ec13b02018-05-14 11:24:57 +0200141 :param q_filter: Query string content. Follows SOL005 section 4.3.2 guidelines, with the follow extensions and
tiernoaf241062018-08-31 14:53:15 +0200142 differences:
143 It accept ".nq" (not equal) in addition to ".neq".
144 For arrays you can specify index (concrete index must match), nothing (any index may match) or 'ANYINDEX'
145 (two or more matches applies for the same array element). Examples:
146 with database register: {A: [{B: 1, C: 2}, {B: 6, C: 9}]}
147 query 'A.B=6' matches because array A contains one element with B equal to 6
148 query 'A.0.B=6' does no match because index 0 of array A contains B with value 1, but not 6
149 query 'A.B=6&A.C=2' matches because one element of array matches B=6 and other matchesC=2
150 query 'A.ANYINDEX.B=6&A.ANYINDEX.C=2' does not match because it is needed the same element of the
151 array matching both
152
153 Examples of translations from SOL005 to >> mongo # comment
154 A=B; A.eq=B >> A: B # must contain key A and equal to B or be a list that contains B
155 A.cont=B >> A: B
156 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
157 # B or C
158 A.cont=B&A.cont=C; A.cont=B,C >> A: {$in: [B, C]}
159 A.ncont=B >> A: {$nin: B} # must not contain key A or if present not equal to B or if a list,
160 # it must not not contain B
161 A.ncont=B,C; A.ncont=B&A.ncont=C >> A: {$nin: [B,C]} # must not contain key A or if present not equal
162 # neither B nor C; or if a list, it must not contain neither B nor C
163 A.ne=B&A.ne=C; A.ne=B,C >> A: {$nin: [B, C]}
164 A.gt=B >> A: {$gt: B} # must contain key A and greater than B
165 A.ne=B; A.neq=B >> A: {$ne: B} # must not contain key A or if present not equal to B, or if
166 # an array not contain B
167 A.ANYINDEX.B=C >> A: {$elemMatch: {B=C}
tierno6ec13b02018-05-14 11:24:57 +0200168 :return: database mongo filter
169 """
tierno5c012612018-04-19 16:01:59 +0200170 try:
171 db_filter = {}
tierno87858ca2018-10-08 16:30:15 +0200172 if not q_filter:
173 return db_filter
tierno6ec13b02018-05-14 11:24:57 +0200174 for query_k, query_v in q_filter.items():
tierno5c012612018-04-19 16:01:59 +0200175 dot_index = query_k.rfind(".")
176 if dot_index > 1 and query_k[dot_index+1:] in ("eq", "ne", "gt", "gte", "lt", "lte", "cont",
177 "ncont", "neq"):
tiernob20a9022018-05-22 12:07:05 +0200178 operator = "$" + query_k[dot_index + 1:]
tierno5c012612018-04-19 16:01:59 +0200179 if operator == "$neq":
180 operator = "$ne"
181 k = query_k[:dot_index]
182 else:
183 operator = "$eq"
184 k = query_k
185
186 v = query_v
187 if isinstance(v, list):
188 if operator in ("$eq", "$cont"):
189 operator = "$in"
190 v = query_v
191 elif operator in ("$ne", "$ncont"):
192 operator = "$nin"
193 v = query_v
194 else:
195 v = query_v.join(",")
196
197 if operator in ("$eq", "$cont"):
198 # v cannot be a comma separated list, because operator would have been changed to $in
tierno6ec13b02018-05-14 11:24:57 +0200199 db_v = v
tierno5c012612018-04-19 16:01:59 +0200200 elif operator == "$ncount":
201 # v cannot be a comma separated list, because operator would have been changed to $nin
tierno6ec13b02018-05-14 11:24:57 +0200202 db_v = {"$ne": v}
tierno5c012612018-04-19 16:01:59 +0200203 else:
tierno6ec13b02018-05-14 11:24:57 +0200204 db_v = {operator: v}
205
tiernoaf241062018-08-31 14:53:15 +0200206 # process the ANYINDEX word at k.
tierno6ec13b02018-05-14 11:24:57 +0200207 kleft, _, kright = k.rpartition(".ANYINDEX.")
208 while kleft:
209 k = kleft
210 db_v = {"$elemMatch": {kright: db_v}}
211 kleft, _, kright = k.rpartition(".ANYINDEX.")
212
213 # insert in db_filter
214 # maybe db_filter[k] exist. e.g. in the query string for values between 5 and 8: "a.gt=5&a.lt=8"
215 deep_update(db_filter, {k: db_v})
tierno5c012612018-04-19 16:01:59 +0200216
217 return db_filter
218 except Exception as e:
219 raise DbException("Invalid query string filter at {}:{}. Error: {}".format(query_k, v, e),
220 http_code=HTTPStatus.BAD_REQUEST)
221
tierno87858ca2018-10-08 16:30:15 +0200222 def get_list(self, table, q_filter=None):
223 """
224 Obtain a list of entries matching q_filter
225 :param table: collection or table
226 :param q_filter: Filter
227 :return: a list (can be empty) with the found entries. Raises DbException on error
228 """
tierno5c012612018-04-19 16:01:59 +0200229 try:
tiernob20a9022018-05-22 12:07:05 +0200230 result = []
tierno1e9a3292018-11-05 18:18:45 +0100231 with self.lock:
232 collection = self.db[table]
233 db_filter = self._format_filter(q_filter)
234 rows = collection.find(db_filter)
tierno5c012612018-04-19 16:01:59 +0200235 for row in rows:
tiernob20a9022018-05-22 12:07:05 +0200236 result.append(row)
237 return result
tierno5c012612018-04-19 16:01:59 +0200238 except DbException:
239 raise
240 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200241 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200242
delacruzramoae049d82019-09-17 16:05:17 +0200243 def count(self, table, q_filter=None):
244 """
245 Count the number of entries matching q_filter
246 :param table: collection or table
247 :param q_filter: Filter
248 :return: number of entries found (can be zero)
249 :raise: DbException on error
250 """
251 try:
252 with self.lock:
253 collection = self.db[table]
254 db_filter = self._format_filter(q_filter)
255 count = collection.count(db_filter)
256 return count
257 except DbException:
258 raise
259 except Exception as e: # TODO refine
260 raise DbException(e)
261
tierno87858ca2018-10-08 16:30:15 +0200262 def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True):
263 """
264 Obtain one entry matching q_filter
265 :param table: collection or table
266 :param q_filter: Filter
267 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
268 it raises a DbException
269 :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so
270 that it raises a DbException
271 :return: The requested element, or None
272 """
tierno5c012612018-04-19 16:01:59 +0200273 try:
tierno87858ca2018-10-08 16:30:15 +0200274 db_filter = self._format_filter(q_filter)
tierno1e9a3292018-11-05 18:18:45 +0100275 with self.lock:
276 collection = self.db[table]
277 if not (fail_on_empty and fail_on_more):
278 return collection.find_one(db_filter)
279 rows = collection.find(db_filter)
tierno5c012612018-04-19 16:01:59 +0200280 if rows.count() == 0:
281 if fail_on_empty:
tierno87858ca2018-10-08 16:30:15 +0200282 raise DbException("Not found any {} with filter='{}'".format(table[:-1], q_filter),
tierno5c012612018-04-19 16:01:59 +0200283 HTTPStatus.NOT_FOUND)
284 return None
285 elif rows.count() > 1:
286 if fail_on_more:
tierno87858ca2018-10-08 16:30:15 +0200287 raise DbException("Found more than one {} with filter='{}'".format(table[:-1], q_filter),
tierno5c012612018-04-19 16:01:59 +0200288 HTTPStatus.CONFLICT)
289 return rows[0]
290 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200291 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200292
tierno87858ca2018-10-08 16:30:15 +0200293 def del_list(self, table, q_filter=None):
294 """
295 Deletes all entries that match q_filter
296 :param table: collection or table
297 :param q_filter: Filter
298 :return: Dict with the number of entries deleted
299 """
tierno5c012612018-04-19 16:01:59 +0200300 try:
tierno1e9a3292018-11-05 18:18:45 +0100301 with self.lock:
302 collection = self.db[table]
303 rows = collection.delete_many(self._format_filter(q_filter))
tierno5c012612018-04-19 16:01:59 +0200304 return {"deleted": rows.deleted_count}
305 except DbException:
306 raise
307 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200308 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200309
tierno87858ca2018-10-08 16:30:15 +0200310 def del_one(self, table, q_filter=None, fail_on_empty=True):
311 """
312 Deletes one entry that matches q_filter
313 :param table: collection or table
314 :param q_filter: Filter
315 :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in
316 which case it raises a DbException
317 :return: Dict with the number of entries deleted
318 """
tierno5c012612018-04-19 16:01:59 +0200319 try:
tierno1e9a3292018-11-05 18:18:45 +0100320 with self.lock:
321 collection = self.db[table]
322 rows = collection.delete_one(self._format_filter(q_filter))
tierno5c012612018-04-19 16:01:59 +0200323 if rows.deleted_count == 0:
324 if fail_on_empty:
tierno87858ca2018-10-08 16:30:15 +0200325 raise DbException("Not found any {} with filter='{}'".format(table[:-1], q_filter),
tierno5c012612018-04-19 16:01:59 +0200326 HTTPStatus.NOT_FOUND)
327 return None
328 return {"deleted": rows.deleted_count}
329 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200330 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200331
332 def create(self, table, indata):
tierno87858ca2018-10-08 16:30:15 +0200333 """
334 Add a new entry at database
335 :param table: collection or table
336 :param indata: content to be added
337 :return: database id of the inserted element. Raises a DbException on error
338 """
tierno5c012612018-04-19 16:01:59 +0200339 try:
tierno1e9a3292018-11-05 18:18:45 +0100340 with self.lock:
341 collection = self.db[table]
342 data = collection.insert_one(indata)
tierno5c012612018-04-19 16:01:59 +0200343 return data.inserted_id
344 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200345 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200346
tierno2c9794c2020-04-29 10:24:28 +0000347 def create_list(self, table, indata_list):
348 """
349 Add several entries at once
350 :param table: collection or table
351 :param indata_list: content list to be added.
352 :return: the list of inserted '_id's. Exception on error
353 """
354 try:
355 for item in indata_list:
356 if item.get("_id") is None:
357 item["_id"] = str(uuid4())
358 with self.lock:
359 collection = self.db[table]
360 data = collection.insert_many(indata_list)
361 return data.inserted_ids
362 except Exception as e: # TODO refine
363 raise DbException(e)
364
tierno399f6c32020-05-12 07:36:41 +0000365 def set_one(self, table, q_filter, update_dict, fail_on_empty=True, unset=None, pull=None, push=None,
tierno0d8e4bc2020-06-22 12:18:18 +0000366 push_list=None, pull_list=None):
tierno87858ca2018-10-08 16:30:15 +0200367 """
368 Modifies an entry at database
369 :param table: collection or table
370 :param q_filter: Filter
371 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
372 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
373 it raises a DbException
tiernod63ea272018-11-27 12:03:36 +0100374 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
375 ignored. If not exist, it is ignored
376 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
377 if exist in the array is removed. If not exist, it is ignored
tierno0d8e4bc2020-06-22 12:18:18 +0000378 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tiernod63ea272018-11-27 12:03:36 +0100379 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
380 is appended to the end of the array
tierno399f6c32020-05-12 07:36:41 +0000381 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
382 whole array
tierno87858ca2018-10-08 16:30:15 +0200383 :return: Dict with the number of entries modified. None if no matching is found.
384 """
tierno5c012612018-04-19 16:01:59 +0200385 try:
tiernod63ea272018-11-27 12:03:36 +0100386 db_oper = {}
387 if update_dict:
388 db_oper["$set"] = update_dict
389 if unset:
390 db_oper["$unset"] = unset
tierno0d8e4bc2020-06-22 12:18:18 +0000391 if pull or pull_list:
392 db_oper["$pull"] = pull or {}
393 if pull_list:
394 db_oper["$pull"].update({k: {"$in": v} for k, v in pull_list.items()})
tierno399f6c32020-05-12 07:36:41 +0000395 if push or push_list:
396 db_oper["$push"] = push or {}
397 if push_list:
398 db_oper["$push"].update({k: {"$each": v} for k, v in push_list.items()})
tiernod63ea272018-11-27 12:03:36 +0100399
tierno1e9a3292018-11-05 18:18:45 +0100400 with self.lock:
401 collection = self.db[table]
tiernod63ea272018-11-27 12:03:36 +0100402 rows = collection.update_one(self._format_filter(q_filter), db_oper)
tierno3054f782018-04-25 16:59:53 +0200403 if rows.matched_count == 0:
tierno5c012612018-04-19 16:01:59 +0200404 if fail_on_empty:
tierno87858ca2018-10-08 16:30:15 +0200405 raise DbException("Not found any {} with filter='{}'".format(table[:-1], q_filter),
tierno5c012612018-04-19 16:01:59 +0200406 HTTPStatus.NOT_FOUND)
407 return None
tierno3054f782018-04-25 16:59:53 +0200408 return {"modified": rows.modified_count}
tierno5c012612018-04-19 16:01:59 +0200409 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200410 raise DbException(e)
tierno5c012612018-04-19 16:01:59 +0200411
tierno0d8e4bc2020-06-22 12:18:18 +0000412 def set_list(self, table, q_filter, update_dict, unset=None, pull=None, push=None, push_list=None, pull_list=None):
tierno87858ca2018-10-08 16:30:15 +0200413 """
414 Modifies al matching entries at database
415 :param table: collection or table
416 :param q_filter: Filter
417 :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 +0000418 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
419 ignored. If not exist, it is ignored
420 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
421 if exist in the array is removed. If not exist, it is ignored
tierno399f6c32020-05-12 07:36:41 +0000422 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys, the
423 single value is appended to the end of the array
tierno0d8e4bc2020-06-22 12:18:18 +0000424 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno399f6c32020-05-12 07:36:41 +0000425 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
426 whole array
tierno87858ca2018-10-08 16:30:15 +0200427 :return: Dict with the number of entries modified
428 """
tierno5c012612018-04-19 16:01:59 +0200429 try:
delacruzramof71fcff2020-02-11 11:14:07 +0000430 db_oper = {}
431 if update_dict:
432 db_oper["$set"] = update_dict
433 if unset:
434 db_oper["$unset"] = unset
tierno0d8e4bc2020-06-22 12:18:18 +0000435 if pull or pull_list:
436 db_oper["$pull"] = pull or {}
437 if pull_list:
438 db_oper["$pull"].update({k: {"$in": v} for k, v in pull_list.items()})
tierno399f6c32020-05-12 07:36:41 +0000439 if push or push_list:
440 db_oper["$push"] = push or {}
441 if push_list:
442 db_oper["$push"].update({k: {"$each": v} for k, v in push_list.items()})
tierno1e9a3292018-11-05 18:18:45 +0100443 with self.lock:
444 collection = self.db[table]
delacruzramof71fcff2020-02-11 11:14:07 +0000445 rows = collection.update_many(self._format_filter(q_filter), db_oper)
tierno87858ca2018-10-08 16:30:15 +0200446 return {"modified": rows.modified_count}
447 except Exception as e: # TODO refine
448 raise DbException(e)
449
450 def replace(self, table, _id, indata, fail_on_empty=True):
451 """
452 Replace the content of an entry
453 :param table: collection or table
454 :param _id: internal database id
455 :param indata: content to replace
456 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
457 it raises a DbException
458 :return: Dict with the number of entries replaced
459 """
460 try:
461 db_filter = {"_id": _id}
tierno1e9a3292018-11-05 18:18:45 +0100462 with self.lock:
463 collection = self.db[table]
464 rows = collection.replace_one(db_filter, indata)
tierno5c012612018-04-19 16:01:59 +0200465 if rows.matched_count == 0:
466 if fail_on_empty:
tierno87858ca2018-10-08 16:30:15 +0200467 raise DbException("Not found any {} with _id='{}'".format(table[:-1], _id), HTTPStatus.NOT_FOUND)
tierno5c012612018-04-19 16:01:59 +0200468 return None
tierno3054f782018-04-25 16:59:53 +0200469 return {"replaced": rows.modified_count}
tierno5c012612018-04-19 16:01:59 +0200470 except Exception as e: # TODO refine
tierno87858ca2018-10-08 16:30:15 +0200471 raise DbException(e)