def db_connect(self, config, target_version=None): """ Connect to database :param config: Configuration of database :param target_version: if provided it checks if database contains required version, raising exception otherwise. :return: None or raises DbException on error """ try: if "logger_name" in config: self.logger = logging.getLogger(config["logger_name"]) master_key = config.get("commonkey") or config.get( "masterpassword") if master_key: self.database_key = master_key self.set_secret_key(master_key) if config.get("uri"): self.client = MongoClient(config["uri"]) else: self.client = MongoClient(config["host"], config["port"]) # TODO add as parameters also username=config.get("user"), password=config.get("password")) # when all modules are ready self.db = self.client[config["name"]] if "loglevel" in config: self.logger.setLevel(getattr(logging, config['loglevel'])) # get data to try a connection now = time() while True: try: version_data = self.get_one("admin", {"_id": "version"}, fail_on_empty=False, fail_on_more=True) # check database status is ok if version_data and version_data.get( "status") != 'ENABLED': raise DbException( "Wrong database status '{}'".format( version_data.get("status")), http_code=HTTPStatus.INTERNAL_SERVER_ERROR) # check version db_version = None if not version_data else version_data.get( "version") if target_version and target_version != db_version: raise DbException( "Invalid database version {}. Expected {}".format( db_version, target_version)) # get serial if version_data and version_data.get("serial"): self.secret_obtained = True self.set_secret_key(b64decode(version_data["serial"])) self.logger.info( "Connected to database {} version {}".format( config["name"], db_version)) return except errors.ConnectionFailure as e: if time() - now >= self.conn_initial_timout: raise self.logger.info("Waiting to database up {}".format(e)) sleep(2) except errors.PyMongoError as e: raise DbException(e)
def get_one_by_id(db, session, topic, id): # find owned by this project _filter = BaseTopic._get_project_filter(session, write=False, show_all=False) _filter["id"] = id desc_list = db.get_list(topic, _filter) if len(desc_list) == 1: return desc_list[0] elif len(desc_list) > 1: raise DbException( "Found more than one {} with id='{}' belonging to this project" .format(topic[:-1], id), HTTPStatus.CONFLICT) # not found any: try to find public _filter = BaseTopic._get_project_filter(session, write=False, show_all=True) _filter["id"] = id desc_list = db.get_list(topic, _filter) if not desc_list: raise DbException( "Not found any {} with id='{}'".format(topic[:-1], id), HTTPStatus.NOT_FOUND) elif len(desc_list) == 1: return desc_list[0] else: raise DbException( "Found more than one public {} with id='{}'; and no one belonging to this project" .format(topic[:-1], id), HTTPStatus.CONFLICT)
def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True): """ Obtain one entry matching q_filter :param table: collection or table :param q_filter: Filter :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case it raises a DbException :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so that it raises a DbException :return: The requested element, or None """ try: result = None with self.lock: for _, row in self._find(table, self._format_filter(q_filter)): if not fail_on_more: return deepcopy(row) if result: raise DbException( "Found more than one entry with filter='{}'". format(q_filter), HTTPStatus.CONFLICT.value) result = row if not result and fail_on_empty: raise DbException( "Not found entry with filter='{}'".format(q_filter), HTTPStatus.NOT_FOUND) return deepcopy(result) except Exception as e: # TODO refine raise DbException(str(e))
def replace(self, table, _id, indata, fail_on_empty=True): """ Replace the content of an entry :param table: collection or table :param _id: internal database id :param indata: content to replace :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case it raises a DbException :return: Dict with the number of entries replaced """ try: with self.lock: for i, _ in self._find(table, self._format_filter({"_id": _id})): break else: if fail_on_empty: raise DbException( "Not found entry with _id='{}'".format(_id), HTTPStatus.NOT_FOUND) return None self.db[table][i] = deepcopy(indata) return {"updated": 1} except DbException: raise except Exception as e: # TODO refine raise DbException(str(e))
def _iterate_keys(k, db_nested, populate=True): k_list = k.split(".") k_item_prev = k_list[0] populated = False if k_item_prev not in db_nested and populate: populated = True db_nested[k_item_prev] = None for k_item in k_list[1:]: if isinstance(db_nested[k_item_prev], dict): if k_item not in db_nested[k_item_prev]: if not populate: raise DbException( "Cannot set '{}', not existing '{}'".format( k, k_item)) populated = True db_nested[k_item_prev][k_item] = None elif isinstance(db_nested[k_item_prev], list) and k_item.isdigit(): # extend list with Nones if index greater than list k_item = int(k_item) if k_item >= len(db_nested[k_item_prev]): if not populate: raise DbException( "Cannot set '{}', index too large '{}'".format( k, k_item)) populated = True db_nested[k_item_prev] += [None] * ( k_item - len(db_nested[k_item_prev]) + 1) elif db_nested[k_item_prev] is None: if not populate: raise DbException( "Cannot set '{}', not existing '{}'".format( k, k_item)) populated = True db_nested[k_item_prev] = {k_item: None} else: # number, string, boolean, ... or list but with not integer key raise DbException( "Cannot set '{}' on existing '{}={}'".format( k, k_item_prev, db_nested[k_item_prev])) db_nested = db_nested[k_item_prev] k_item_prev = k_item return db_nested, k_item_prev, populated
def set_list(self, table, q_filter, update_dict, unset=None, pull=None, push=None, push_list=None, pull_list=None): """ Modifies al matching entries at database :param table: collection or table :param q_filter: Filter :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is ignored. If not exist, it is ignored :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value if exist in the array is removed. If not exist, it is ignored :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys, the single value is appended to the end of the array :param pull_list: Same as pull but values are arrays where each item is removed from the array :param push_list: Same as push but values are arrays where each item is and appended instead of appending the whole array :return: Dict with the number of entries modified """ try: db_oper = {} if update_dict: db_oper["$set"] = update_dict if unset: db_oper["$unset"] = unset if pull or pull_list: db_oper["$pull"] = pull or {} if pull_list: db_oper["$pull"].update( {k: { "$in": v } for k, v in pull_list.items()}) if push or push_list: db_oper["$push"] = push or {} if push_list: db_oper["$push"].update( {k: { "$each": v } for k, v in push_list.items()}) with self.lock: collection = self.db[table] rows = collection.update_many(self._format_filter(q_filter), db_oper) return {"modified": rows.modified_count} except Exception as e: # TODO refine raise DbException(e)
def del_one(self, table, q_filter=None, fail_on_empty=True): """ Deletes one entry that matches q_filter :param table: collection or table :param q_filter: Filter :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in which case it raises a DbException :return: Dict with the number of entries deleted """ try: with self.lock: collection = self.db[table] rows = collection.delete_one(self._format_filter(q_filter)) if rows.deleted_count == 0: if fail_on_empty: raise DbException( "Not found any {} with filter='{}'".format( table[:-1], q_filter), HTTPStatus.NOT_FOUND) return None return {"deleted": rows.deleted_count} except Exception as e: # TODO refine raise DbException(e)
def del_one(self, table, q_filter=None, fail_on_empty=True): """ Deletes one entry that matches q_filter :param table: collection or table :param q_filter: Filter :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in which case it raises a DbException :return: Dict with the number of entries deleted """ try: with self.lock: for i, _ in self._find(table, self._format_filter(q_filter)): break else: if fail_on_empty: raise DbException( "Not found entry with filter='{}'".format( q_filter), HTTPStatus.NOT_FOUND) return None del self.db[table][i] return {"deleted": 1} except Exception as e: # TODO refine raise DbException(str(e))
def create(self, table, indata): """ Add a new entry at database :param table: collection or table :param indata: content to be added :return: database id of the inserted element. Raises a DbException on error """ try: with self.lock: collection = self.db[table] data = collection.insert_one(indata) return data.inserted_id except Exception as e: # TODO refine raise DbException(e)
def replace(self, table, _id, indata, fail_on_empty=True): """ Replace the content of an entry :param table: collection or table :param _id: internal database id :param indata: content to replace :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case it raises a DbException :return: Dict with the number of entries replaced """ try: db_filter = {"_id": _id} with self.lock: collection = self.db[table] rows = collection.replace_one(db_filter, indata) if rows.matched_count == 0: if fail_on_empty: raise DbException( "Not found any {} with _id='{}'".format( table[:-1], _id), HTTPStatus.NOT_FOUND) return None return {"replaced": rows.modified_count} except Exception as e: # TODO refine raise DbException(e)
def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True): """ Obtain one entry matching q_filter :param table: collection or table :param q_filter: Filter :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case it raises a DbException :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so that it raises a DbException :return: The requested element, or None """ try: db_filter = self._format_filter(q_filter) with self.lock: collection = self.db[table] if not (fail_on_empty and fail_on_more): return collection.find_one(db_filter) rows = collection.find(db_filter) if rows.count() == 0: if fail_on_empty: raise DbException( "Not found any {} with filter='{}'".format( table[:-1], q_filter), HTTPStatus.NOT_FOUND) return None elif rows.count() > 1: if fail_on_more: raise DbException( "Found more than one {} with filter='{}'".format( table[:-1], q_filter), HTTPStatus.CONFLICT) return rows[0] except Exception as e: # TODO refine raise DbException(e)
def del_list(self, table, q_filter=None): """ Deletes all entries that match q_filter :param table: collection or table :param q_filter: Filter :return: Dict with the number of entries deleted """ try: with self.lock: collection = self.db[table] rows = collection.delete_many(self._format_filter(q_filter)) return {"deleted": rows.deleted_count} except DbException: raise except Exception as e: # TODO refine raise DbException(e)
def create_list(self, table, indata_list): """ Add several entries at once :param table: collection or table :param indata_list: content list to be added. :return: the list of inserted '_id's. Exception on error """ try: for item in indata_list: if item.get("_id") is None: item["_id"] = str(uuid4()) with self.lock: collection = self.db[table] data = collection.insert_many(indata_list) return data.inserted_ids except Exception as e: # TODO refine raise DbException(e)
def count(self, table, q_filter=None): """ Count the number of entries matching q_filter :param table: collection or table :param q_filter: Filter :return: number of entries found (can be zero) :raise: DbException on error """ try: with self.lock: return sum( 1 for x in self._find(table, self._format_filter(q_filter))) except DbException: raise except Exception as e: # TODO refine raise DbException(str(e))
def get_list(self, table, q_filter=None): """ Obtain a list of entries matching q_filter :param table: collection or table :param q_filter: Filter :return: a list (can be empty) with the found entries. Raises DbException on error """ try: result = [] with self.lock: for _, row in self._find(table, self._format_filter(q_filter)): result.append(deepcopy(row)) return result except DbException: raise except Exception as e: # TODO refine raise DbException(str(e))
def count(self, table, q_filter=None): """ Count the number of entries matching q_filter :param table: collection or table :param q_filter: Filter :return: number of entries found (can be zero) :raise: DbException on error """ try: with self.lock: collection = self.db[table] db_filter = self._format_filter(q_filter) count = collection.count(db_filter) return count except DbException: raise except Exception as e: # TODO refine raise DbException(e)
def set_one(self, table, q_filter, update_dict, fail_on_empty=True, unset=None, pull=None, push=None, push_list=None, pull_list=None): """ Modifies an entry at database :param table: collection or table :param q_filter: Filter :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case it raises a DbException :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is ignored. If not exist, it is ignored :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value if exist in the array is removed. If not exist, it is ignored :param pull_list: Same as pull but values are arrays where each item is removed from the array :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value is appended to the end of the array :param push_list: Same as push but values are arrays where each item is and appended instead of appending the whole array :return: Dict with the number of entries modified. None if no matching is found. """ with self.lock: for i, db_item in self._find(table, self._format_filter(q_filter)): updated = self._update(db_item, update_dict, unset=unset, pull=pull, push=push, push_list=push_list, pull_list=pull_list) return {"updated": 1 if updated else 0} else: if fail_on_empty: raise DbException( "Not found entry with _id='{}'".format(q_filter), HTTPStatus.NOT_FOUND) return None
def create(self, table, indata): """ Add a new entry at database :param table: collection or table :param indata: content to be added :return: database '_id' of the inserted element. Raises a DbException on error """ try: id = indata.get("_id") if not id: id = str(uuid4()) indata["_id"] = id with self.lock: if table not in self.db: self.db[table] = [] self.db[table].append(deepcopy(indata)) return id except Exception as e: # TODO refine raise DbException(str(e))
def del_list(self, table, q_filter=None): """ Deletes all entries that match q_filter :param table: collection or table :param q_filter: Filter :return: Dict with the number of entries deleted """ try: id_list = [] with self.lock: for i, _ in self._find(table, self._format_filter(q_filter)): id_list.append(i) deleted = len(id_list) for i in reversed(id_list): del self.db[table][i] return {"deleted": deleted} except DbException: raise except Exception as e: # TODO refine raise DbException(str(e))
def get_list(self, table, q_filter=None): """ Obtain a list of entries matching q_filter :param table: collection or table :param q_filter: Filter :return: a list (can be empty) with the found entries. Raises DbException on error """ try: result = [] with self.lock: collection = self.db[table] db_filter = self._format_filter(q_filter) rows = collection.find(db_filter) for row in rows: result.append(row) return result except DbException: raise except Exception as e: # TODO refine raise DbException(e)
def create_list(self, table, indata_list): """ Add a new entry at database :param table: collection or table :param indata_list: list content to be added :return: list of inserted 'id's. Raises a DbException on error """ try: _ids = [] with self.lock: for indata in indata_list: _id = indata.get("_id") if not _id: _id = str(uuid4()) indata["_id"] = _id with self.lock: if table not in self.db: self.db[table] = [] self.db[table].append(deepcopy(indata)) _ids.append(_id) return _ids except Exception as e: # TODO refine raise DbException(str(e))
def recursive_find(key_list, key_next_index, content, oper, target): if key_next_index == len(key_list) or content is None: try: if oper in ("eq", "cont"): if isinstance(target, list): if isinstance(content, list): return any(content_item in target for content_item in content) return content in target elif isinstance(content, list): return target in content else: return content == target elif oper in ("neq", "ne", "ncont"): if isinstance(target, list): if isinstance(content, list): return all(content_item not in target for content_item in content) return content not in target elif isinstance(content, list): return target not in content else: return content != target if oper == "gt": return content > target elif oper == "gte": return content >= target elif oper == "lt": return content < target elif oper == "lte": return content <= target else: raise DbException( "Unknown filter operator '{}' in key '{}'".format( oper, ".".join(key_list)), http_code=HTTPStatus.BAD_REQUEST) except TypeError: return False elif isinstance(content, dict): return recursive_find(key_list, key_next_index + 1, content.get(key_list[key_next_index]), oper, target) elif isinstance(content, list): look_for_match = True # when there is a match return immediately if (target is None) != (oper in ("neq", "ne", "ncont") ): # one True and other False (Xor) look_for_match = False # when there is not a match return immediately for content_item in content: if key_list[key_next_index] == "ANYINDEX" and isinstance( v, dict): matches = True for k2, v2 in target.items(): k_new_list = k2.split(".") new_operator = "eq" if k_new_list[-1] in ("eq", "ne", "gt", "gte", "lt", "lte", "cont", "ncont", "neq"): new_operator = k_new_list.pop() if not recursive_find(k_new_list, 0, content_item, new_operator, v2): matches = False break else: matches = recursive_find(key_list, key_next_index, content_item, oper, target) if matches == look_for_match: return matches if key_list[key_next_index].isdecimal() and int( key_list[key_next_index]) < len(content): matches = recursive_find( key_list, key_next_index + 1, content[int(key_list[key_next_index])], oper, target) if matches == look_for_match: return matches return not look_for_match else: # content is not dict, nor list neither None, so not found if oper in ("neq", "ne", "ncont"): return target is not None else: return target is None
def _format_filter(q_filter): """ Translate query string q_filter into mongo database filter :param q_filter: Query string content. Follows SOL005 section 4.3.2 guidelines, with the follow extensions and differences: It accept ".nq" (not equal) in addition to ".neq". For arrays you can specify index (concrete index must match), nothing (any index may match) or 'ANYINDEX' (two or more matches applies for the same array element). Examples: with database register: {A: [{B: 1, C: 2}, {B: 6, C: 9}]} query 'A.B=6' matches because array A contains one element with B equal to 6 query 'A.0.B=6' does no match because index 0 of array A contains B with value 1, but not 6 query 'A.B=6&A.C=2' matches because one element of array matches B=6 and other matchesC=2 query 'A.ANYINDEX.B=6&A.ANYINDEX.C=2' does not match because it is needed the same element of the array matching both Examples of translations from SOL005 to >> mongo # comment A=B; A.eq=B >> A: B # must contain key A and equal to B or be a list that contains B A.cont=B >> A: B 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 # B or C A.cont=B&A.cont=C; A.cont=B,C >> A: {$in: [B, C]} A.ncont=B >> A: {$nin: B} # must not contain key A or if present not equal to B or if a list, # it must not not contain B A.ncont=B,C; A.ncont=B&A.ncont=C >> A: {$nin: [B,C]} # must not contain key A or if present not equal # neither B nor C; or if a list, it must not contain neither B nor C A.ne=B&A.ne=C; A.ne=B,C >> A: {$nin: [B, C]} A.gt=B >> A: {$gt: B} # must contain key A and greater than B A.ne=B; A.neq=B >> A: {$ne: B} # must not contain key A or if present not equal to B, or if # an array not contain B A.ANYINDEX.B=C >> A: {$elemMatch: {B=C} :return: database mongo filter """ try: db_filter = {} if not q_filter: return db_filter for query_k, query_v in q_filter.items(): dot_index = query_k.rfind(".") if dot_index > 1 and query_k[dot_index + 1:] in ( "eq", "ne", "gt", "gte", "lt", "lte", "cont", "ncont", "neq"): operator = "$" + query_k[dot_index + 1:] if operator == "$neq": operator = "$ne" k = query_k[:dot_index] else: operator = "$eq" k = query_k v = query_v if isinstance(v, list): if operator in ("$eq", "$cont"): operator = "$in" v = query_v elif operator in ("$ne", "$ncont"): operator = "$nin" v = query_v else: v = query_v.join(",") if operator in ("$eq", "$cont"): # v cannot be a comma separated list, because operator would have been changed to $in db_v = v elif operator == "$ncount": # v cannot be a comma separated list, because operator would have been changed to $nin db_v = {"$ne": v} else: db_v = {operator: v} # process the ANYINDEX word at k. kleft, _, kright = k.rpartition(".ANYINDEX.") while kleft: k = kleft db_v = {"$elemMatch": {kright: db_v}} kleft, _, kright = k.rpartition(".ANYINDEX.") # insert in db_filter # maybe db_filter[k] exist. e.g. in the query string for values between 5 and 8: "a.gt=5&a.lt=8" deep_update(db_filter, {k: db_v}) return db_filter except Exception as e: raise DbException( "Invalid query string filter at {}:{}. Error: {}".format( query_k, v, e), http_code=HTTPStatus.BAD_REQUEST)
def set_one(self, table, q_filter, update_dict, fail_on_empty=True, unset=None, pull=None, push=None, push_list=None, pull_list=None): """ Modifies an entry at database :param table: collection or table :param q_filter: Filter :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case it raises a DbException :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is ignored. If not exist, it is ignored :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value if exist in the array is removed. If not exist, it is ignored :param pull_list: Same as pull but values are arrays where each item is removed from the array :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value is appended to the end of the array :param push_list: Same as push but values are arrays where each item is and appended instead of appending the whole array :return: Dict with the number of entries modified. None if no matching is found. """ try: db_oper = {} if update_dict: db_oper["$set"] = update_dict if unset: db_oper["$unset"] = unset if pull or pull_list: db_oper["$pull"] = pull or {} if pull_list: db_oper["$pull"].update( {k: { "$in": v } for k, v in pull_list.items()}) if push or push_list: db_oper["$push"] = push or {} if push_list: db_oper["$push"].update( {k: { "$each": v } for k, v in push_list.items()}) with self.lock: collection = self.db[table] rows = collection.update_one(self._format_filter(q_filter), db_oper) if rows.matched_count == 0: if fail_on_empty: raise DbException( "Not found any {} with filter='{}'".format( table[:-1], q_filter), HTTPStatus.NOT_FOUND) return None return {"modified": rows.modified_count} except Exception as e: # TODO refine raise DbException(e)
def _update(self, db_item, update_dict, unset=None, pull=None, push=None, push_list=None, pull_list=None): """ Modifies an entry at database :param db_item: entry of the table to update :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is ignored. If not exist, it is ignored :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value if exist in the array is removed. If not exist, it is ignored :param pull_list: Same as pull but values are arrays where each item is removed from the array :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value is appended to the end of the array :param push_list: Same as push but values are arrays where each item is and appended instead of appending the whole array :return: True if database has been changed, False if not; Exception on error """ def _iterate_keys(k, db_nested, populate=True): k_list = k.split(".") k_item_prev = k_list[0] populated = False if k_item_prev not in db_nested and populate: populated = True db_nested[k_item_prev] = None for k_item in k_list[1:]: if isinstance(db_nested[k_item_prev], dict): if k_item not in db_nested[k_item_prev]: if not populate: raise DbException( "Cannot set '{}', not existing '{}'".format( k, k_item)) populated = True db_nested[k_item_prev][k_item] = None elif isinstance(db_nested[k_item_prev], list) and k_item.isdigit(): # extend list with Nones if index greater than list k_item = int(k_item) if k_item >= len(db_nested[k_item_prev]): if not populate: raise DbException( "Cannot set '{}', index too large '{}'".format( k, k_item)) populated = True db_nested[k_item_prev] += [None] * ( k_item - len(db_nested[k_item_prev]) + 1) elif db_nested[k_item_prev] is None: if not populate: raise DbException( "Cannot set '{}', not existing '{}'".format( k, k_item)) populated = True db_nested[k_item_prev] = {k_item: None} else: # number, string, boolean, ... or list but with not integer key raise DbException( "Cannot set '{}' on existing '{}={}'".format( k, k_item_prev, db_nested[k_item_prev])) db_nested = db_nested[k_item_prev] k_item_prev = k_item return db_nested, k_item_prev, populated updated = False try: if update_dict: for dot_k, v in update_dict.items(): dict_to_update, key_to_update, _ = _iterate_keys( dot_k, db_item) dict_to_update[key_to_update] = v updated = True if unset: for dot_k in unset: try: dict_to_update, key_to_update, _ = _iterate_keys( dot_k, db_item, populate=False) del dict_to_update[key_to_update] updated = True except Exception: pass if pull: for dot_k, v in pull.items(): try: dict_to_update, key_to_update, _ = _iterate_keys( dot_k, db_item, populate=False) except Exception: continue if key_to_update not in dict_to_update: continue if not isinstance(dict_to_update[key_to_update], list): raise DbException( "Cannot pull '{}'. Target is not a list".format( dot_k)) while v in dict_to_update[key_to_update]: dict_to_update[key_to_update].remove(v) updated = True if pull_list: for dot_k, v in pull_list.items(): if not isinstance(v, list): raise DbException( "Invalid content at pull_list, '{}' must be an array" .format(dot_k), http_code=HTTPStatus.BAD_REQUEST) try: dict_to_update, key_to_update, _ = _iterate_keys( dot_k, db_item, populate=False) except Exception: continue if key_to_update not in dict_to_update: continue if not isinstance(dict_to_update[key_to_update], list): raise DbException( "Cannot pull_list '{}'. Target is not a list". format(dot_k)) for single_v in v: while single_v in dict_to_update[key_to_update]: dict_to_update[key_to_update].remove(single_v) updated = True if push: for dot_k, v in push.items(): dict_to_update, key_to_update, populated = _iterate_keys( dot_k, db_item) if isinstance( dict_to_update, dict) and key_to_update not in dict_to_update: dict_to_update[key_to_update] = [v] updated = True elif populated and dict_to_update[key_to_update] is None: dict_to_update[key_to_update] = [v] updated = True elif not isinstance(dict_to_update[key_to_update], list): raise DbException( "Cannot push '{}'. Target is not a list".format( dot_k)) else: dict_to_update[key_to_update].append(v) updated = True if push_list: for dot_k, v in push_list.items(): if not isinstance(v, list): raise DbException( "Invalid content at push_list, '{}' must be an array" .format(dot_k), http_code=HTTPStatus.BAD_REQUEST) dict_to_update, key_to_update, populated = _iterate_keys( dot_k, db_item) if isinstance( dict_to_update, dict) and key_to_update not in dict_to_update: dict_to_update[key_to_update] = v.copy() updated = True elif populated and dict_to_update[key_to_update] is None: dict_to_update[key_to_update] = v.copy() updated = True elif not isinstance(dict_to_update[key_to_update], list): raise DbException( "Cannot push '{}'. Target is not a list".format( dot_k), http_code=HTTPStatus.CONFLICT) else: dict_to_update[key_to_update] += v updated = True return updated except DbException: raise except Exception as e: # TODO refine raise DbException(str(e))