blob: ad5213573f3d9f32db8bc9ef3f7e1a7eedbcef2b [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 +020018from copy import deepcopy
aticig3dd0db62022-03-04 19:35:45 +030019from http import HTTPStatus
20import logging
21from uuid import uuid4
22
23from osm_common.dbbase import DbBase, DbException
24from osm_common.dbmongo import deep_update
25
tierno5c012612018-04-19 16:01:59 +020026
27__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
28
29
30class DbMemory(DbBase):
garciadeblas2644b762021-03-24 09:21:01 +010031 def __init__(self, logger_name="db", lock=False):
tierno1e9a3292018-11-05 18:18:45 +010032 super().__init__(logger_name, lock)
tierno5c012612018-04-19 16:01:59 +020033 self.db = {}
34
35 def db_connect(self, config):
tierno87858ca2018-10-08 16:30:15 +020036 """
37 Connect to database
38 :param config: Configuration of database
39 :return: None or raises DbException on error
40 """
tierno5c012612018-04-19 16:01:59 +020041 if "logger_name" in config:
42 self.logger = logging.getLogger(config["logger_name"])
tiernoeef7cb72018-11-12 11:51:49 +010043 master_key = config.get("commonkey") or config.get("masterpassword")
44 if master_key:
45 self.set_secret_key(master_key)
tierno5c012612018-04-19 16:01:59 +020046
47 @staticmethod
tierno87858ca2018-10-08 16:30:15 +020048 def _format_filter(q_filter):
tierno6472e2b2019-09-02 16:04:16 +000049 db_filter = {}
50 # split keys with ANYINDEX in this way:
51 # {"A.B.ANYINDEX.C.D.ANYINDEX.E": v } -> {"A.B.ANYINDEX": {"C.D.ANYINDEX": {"E": v}}}
52 if q_filter:
53 for k, v in q_filter.items():
54 db_v = v
55 kleft, _, kright = k.rpartition(".ANYINDEX.")
56 while kleft:
57 k = kleft + ".ANYINDEX"
58 db_v = {kright: db_v}
59 kleft, _, kright = k.rpartition(".ANYINDEX.")
60 deep_update(db_filter, {k: db_v})
61
62 return db_filter
tierno5c012612018-04-19 16:01:59 +020063
tierno87858ca2018-10-08 16:30:15 +020064 def _find(self, table, q_filter):
tierno40e326a2019-09-19 09:23:44 +000065 def recursive_find(key_list, key_next_index, content, oper, target):
tierno6472e2b2019-09-02 16:04:16 +000066 if key_next_index == len(key_list) or content is None:
67 try:
tierno40e326a2019-09-19 09:23:44 +000068 if oper in ("eq", "cont"):
69 if isinstance(target, list):
70 if isinstance(content, list):
garciadeblas2644b762021-03-24 09:21:01 +010071 return any(
72 content_item in target for content_item in content
73 )
tierno40e326a2019-09-19 09:23:44 +000074 return content in target
75 elif isinstance(content, list):
76 return target in content
77 else:
78 return content == target
79 elif oper in ("neq", "ne", "ncont"):
80 if isinstance(target, list):
81 if isinstance(content, list):
garciadeblas2644b762021-03-24 09:21:01 +010082 return all(
83 content_item not in target
84 for content_item in content
85 )
tierno40e326a2019-09-19 09:23:44 +000086 return content not in target
87 elif isinstance(content, list):
88 return target not in content
89 else:
90 return content != target
91 if oper == "gt":
tierno6472e2b2019-09-02 16:04:16 +000092 return content > target
tierno40e326a2019-09-19 09:23:44 +000093 elif oper == "gte":
tierno6472e2b2019-09-02 16:04:16 +000094 return content >= target
tierno40e326a2019-09-19 09:23:44 +000095 elif oper == "lt":
tierno6472e2b2019-09-02 16:04:16 +000096 return content < target
tierno40e326a2019-09-19 09:23:44 +000097 elif oper == "lte":
tierno6472e2b2019-09-02 16:04:16 +000098 return content <= target
tierno6472e2b2019-09-02 16:04:16 +000099 else:
garciadeblas2644b762021-03-24 09:21:01 +0100100 raise DbException(
101 "Unknown filter operator '{}' in key '{}'".format(
102 oper, ".".join(key_list)
103 ),
104 http_code=HTTPStatus.BAD_REQUEST,
105 )
tierno6472e2b2019-09-02 16:04:16 +0000106 except TypeError:
107 return False
108
109 elif isinstance(content, dict):
garciadeblas2644b762021-03-24 09:21:01 +0100110 return recursive_find(
111 key_list,
112 key_next_index + 1,
113 content.get(key_list[key_next_index]),
114 oper,
115 target,
116 )
tierno6472e2b2019-09-02 16:04:16 +0000117 elif isinstance(content, list):
118 look_for_match = True # when there is a match return immediately
garciadeblas2644b762021-03-24 09:21:01 +0100119 if (target is None) != (
120 oper in ("neq", "ne", "ncont")
121 ): # one True and other False (Xor)
122 look_for_match = (
123 False # when there is not a match return immediately
124 )
tierno6472e2b2019-09-02 16:04:16 +0000125
126 for content_item in content:
127 if key_list[key_next_index] == "ANYINDEX" and isinstance(v, dict):
tierno40e326a2019-09-19 09:23:44 +0000128 matches = True
tierno6472e2b2019-09-02 16:04:16 +0000129 for k2, v2 in target.items():
130 k_new_list = k2.split(".")
131 new_operator = "eq"
garciadeblas2644b762021-03-24 09:21:01 +0100132 if k_new_list[-1] in (
133 "eq",
134 "ne",
135 "gt",
136 "gte",
137 "lt",
138 "lte",
139 "cont",
140 "ncont",
141 "neq",
142 ):
tierno6472e2b2019-09-02 16:04:16 +0000143 new_operator = k_new_list.pop()
garciadeblas2644b762021-03-24 09:21:01 +0100144 if not recursive_find(
145 k_new_list, 0, content_item, new_operator, v2
146 ):
tierno40e326a2019-09-19 09:23:44 +0000147 matches = False
tierno6472e2b2019-09-02 16:04:16 +0000148 break
tierno6472e2b2019-09-02 16:04:16 +0000149
150 else:
garciadeblas2644b762021-03-24 09:21:01 +0100151 matches = recursive_find(
152 key_list, key_next_index, content_item, oper, target
153 )
tierno40e326a2019-09-19 09:23:44 +0000154 if matches == look_for_match:
155 return matches
garciadeblas2644b762021-03-24 09:21:01 +0100156 if key_list[key_next_index].isdecimal() and int(
157 key_list[key_next_index]
158 ) < len(content):
159 matches = recursive_find(
160 key_list,
161 key_next_index + 1,
162 content[int(key_list[key_next_index])],
163 oper,
164 target,
165 )
tierno40e326a2019-09-19 09:23:44 +0000166 if matches == look_for_match:
167 return matches
tierno6472e2b2019-09-02 16:04:16 +0000168 return not look_for_match
169 else: # content is not dict, nor list neither None, so not found
tierno40e326a2019-09-19 09:23:44 +0000170 if oper in ("neq", "ne", "ncont"):
171 return target is not None
tierno6472e2b2019-09-02 16:04:16 +0000172 else:
tierno40e326a2019-09-19 09:23:44 +0000173 return target is None
tierno6472e2b2019-09-02 16:04:16 +0000174
tierno5c012612018-04-19 16:01:59 +0200175 for i, row in enumerate(self.db.get(table, ())):
tierno6472e2b2019-09-02 16:04:16 +0000176 q_filter = q_filter or {}
177 for k, v in q_filter.items():
178 k_list = k.split(".")
179 operator = "eq"
garciadeblas2644b762021-03-24 09:21:01 +0100180 if k_list[-1] in (
181 "eq",
182 "ne",
183 "gt",
184 "gte",
185 "lt",
186 "lte",
187 "cont",
188 "ncont",
189 "neq",
190 ):
tierno6472e2b2019-09-02 16:04:16 +0000191 operator = k_list.pop()
tierno40e326a2019-09-19 09:23:44 +0000192 matches = recursive_find(k_list, 0, row, operator, v)
193 if not matches:
tierno6472e2b2019-09-02 16:04:16 +0000194 break
195 else:
196 # match
tierno5c012612018-04-19 16:01:59 +0200197 yield i, row
198
tierno87858ca2018-10-08 16:30:15 +0200199 def get_list(self, table, q_filter=None):
200 """
201 Obtain a list of entries matching q_filter
202 :param table: collection or table
203 :param q_filter: Filter
204 :return: a list (can be empty) with the found entries. Raises DbException on error
205 """
tierno5c012612018-04-19 16:01:59 +0200206 try:
tiernob20a9022018-05-22 12:07:05 +0200207 result = []
tierno1e9a3292018-11-05 18:18:45 +0100208 with self.lock:
209 for _, row in self._find(table, self._format_filter(q_filter)):
210 result.append(deepcopy(row))
tiernob20a9022018-05-22 12:07:05 +0200211 return result
tierno5c012612018-04-19 16:01:59 +0200212 except DbException:
213 raise
214 except Exception as e: # TODO refine
215 raise DbException(str(e))
216
delacruzramoae049d82019-09-17 16:05:17 +0200217 def count(self, table, q_filter=None):
218 """
219 Count the number of entries matching q_filter
220 :param table: collection or table
221 :param q_filter: Filter
222 :return: number of entries found (can be zero)
223 :raise: DbException on error
224 """
225 try:
226 with self.lock:
227 return sum(1 for x in self._find(table, self._format_filter(q_filter)))
228 except DbException:
229 raise
230 except Exception as e: # TODO refine
231 raise DbException(str(e))
232
tierno87858ca2018-10-08 16:30:15 +0200233 def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True):
234 """
235 Obtain one entry matching q_filter
236 :param table: collection or table
237 :param q_filter: Filter
238 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
239 it raises a DbException
240 :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so
241 that it raises a DbException
242 :return: The requested element, or None
243 """
tierno5c012612018-04-19 16:01:59 +0200244 try:
tiernob20a9022018-05-22 12:07:05 +0200245 result = None
tierno1e9a3292018-11-05 18:18:45 +0100246 with self.lock:
247 for _, row in self._find(table, self._format_filter(q_filter)):
248 if not fail_on_more:
249 return deepcopy(row)
250 if result:
garciadeblas2644b762021-03-24 09:21:01 +0100251 raise DbException(
252 "Found more than one entry with filter='{}'".format(
253 q_filter
254 ),
255 HTTPStatus.CONFLICT.value,
256 )
tierno1e9a3292018-11-05 18:18:45 +0100257 result = row
tiernob20a9022018-05-22 12:07:05 +0200258 if not result and fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100259 raise DbException(
260 "Not found entry with filter='{}'".format(q_filter),
261 HTTPStatus.NOT_FOUND,
262 )
tiernob20a9022018-05-22 12:07:05 +0200263 return deepcopy(result)
tierno5c012612018-04-19 16:01:59 +0200264 except Exception as e: # TODO refine
265 raise DbException(str(e))
266
tierno87858ca2018-10-08 16:30:15 +0200267 def del_list(self, table, q_filter=None):
268 """
269 Deletes all entries that match q_filter
270 :param table: collection or table
271 :param q_filter: Filter
272 :return: Dict with the number of entries deleted
273 """
tierno5c012612018-04-19 16:01:59 +0200274 try:
275 id_list = []
tierno1e9a3292018-11-05 18:18:45 +0100276 with self.lock:
277 for i, _ in self._find(table, self._format_filter(q_filter)):
278 id_list.append(i)
tierno5c012612018-04-19 16:01:59 +0200279 deleted = len(id_list)
Eduardo Sousa857731b2018-04-26 15:55:05 +0100280 for i in reversed(id_list):
tierno5c012612018-04-19 16:01:59 +0200281 del self.db[table][i]
282 return {"deleted": deleted}
283 except DbException:
284 raise
285 except Exception as e: # TODO refine
286 raise DbException(str(e))
287
tierno87858ca2018-10-08 16:30:15 +0200288 def del_one(self, table, q_filter=None, fail_on_empty=True):
289 """
290 Deletes one entry that matches q_filter
291 :param table: collection or table
292 :param q_filter: Filter
293 :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in
294 which case it raises a DbException
295 :return: Dict with the number of entries deleted
296 """
tierno5c012612018-04-19 16:01:59 +0200297 try:
tierno1e9a3292018-11-05 18:18:45 +0100298 with self.lock:
299 for i, _ in self._find(table, self._format_filter(q_filter)):
300 break
301 else:
302 if fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100303 raise DbException(
304 "Not found entry with filter='{}'".format(q_filter),
305 HTTPStatus.NOT_FOUND,
306 )
tierno1e9a3292018-11-05 18:18:45 +0100307 return None
308 del self.db[table][i]
tierno5c012612018-04-19 16:01:59 +0200309 return {"deleted": 1}
310 except Exception as e: # TODO refine
311 raise DbException(str(e))
312
garciadeblas2644b762021-03-24 09:21:01 +0100313 def _update(
314 self,
315 db_item,
316 update_dict,
317 unset=None,
318 pull=None,
319 push=None,
320 push_list=None,
321 pull_list=None,
322 ):
tierno7fc50dd2020-02-17 12:01:38 +0000323 """
324 Modifies an entry at database
325 :param db_item: entry of the table to update
326 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
327 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
328 ignored. If not exist, it is ignored
329 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
330 if exist in the array is removed. If not exist, it is ignored
tierno0d8e4bc2020-06-22 12:18:18 +0000331 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno7fc50dd2020-02-17 12:01:38 +0000332 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
333 is appended to the end of the array
tierno399f6c32020-05-12 07:36:41 +0000334 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
335 whole array
tierno7fc50dd2020-02-17 12:01:38 +0000336 :return: True if database has been changed, False if not; Exception on error
337 """
garciadeblas2644b762021-03-24 09:21:01 +0100338
tierno7fc50dd2020-02-17 12:01:38 +0000339 def _iterate_keys(k, db_nested, populate=True):
340 k_list = k.split(".")
341 k_item_prev = k_list[0]
342 populated = False
tiernobf6c5722020-03-12 09:54:35 +0000343 if k_item_prev not in db_nested and populate:
344 populated = True
345 db_nested[k_item_prev] = None
tierno7fc50dd2020-02-17 12:01:38 +0000346 for k_item in k_list[1:]:
347 if isinstance(db_nested[k_item_prev], dict):
348 if k_item not in db_nested[k_item_prev]:
349 if not populate:
garciadeblas2644b762021-03-24 09:21:01 +0100350 raise DbException(
351 "Cannot set '{}', not existing '{}'".format(k, k_item)
352 )
tierno7fc50dd2020-02-17 12:01:38 +0000353 populated = True
354 db_nested[k_item_prev][k_item] = None
355 elif isinstance(db_nested[k_item_prev], list) and k_item.isdigit():
356 # extend list with Nones if index greater than list
357 k_item = int(k_item)
358 if k_item >= len(db_nested[k_item_prev]):
359 if not populate:
garciadeblas2644b762021-03-24 09:21:01 +0100360 raise DbException(
361 "Cannot set '{}', index too large '{}'".format(
362 k, k_item
363 )
364 )
tierno7fc50dd2020-02-17 12:01:38 +0000365 populated = True
garciadeblas2644b762021-03-24 09:21:01 +0100366 db_nested[k_item_prev] += [None] * (
367 k_item - len(db_nested[k_item_prev]) + 1
368 )
tierno7fc50dd2020-02-17 12:01:38 +0000369 elif db_nested[k_item_prev] is None:
370 if not populate:
garciadeblas2644b762021-03-24 09:21:01 +0100371 raise DbException(
372 "Cannot set '{}', not existing '{}'".format(k, k_item)
373 )
tierno7fc50dd2020-02-17 12:01:38 +0000374 populated = True
375 db_nested[k_item_prev] = {k_item: None}
376 else: # number, string, boolean, ... or list but with not integer key
garciadeblas2644b762021-03-24 09:21:01 +0100377 raise DbException(
378 "Cannot set '{}' on existing '{}={}'".format(
379 k, k_item_prev, db_nested[k_item_prev]
380 )
381 )
tierno7fc50dd2020-02-17 12:01:38 +0000382 db_nested = db_nested[k_item_prev]
383 k_item_prev = k_item
384 return db_nested, k_item_prev, populated
385
386 updated = False
387 try:
388 if update_dict:
389 for dot_k, v in update_dict.items():
390 dict_to_update, key_to_update, _ = _iterate_keys(dot_k, db_item)
391 dict_to_update[key_to_update] = v
392 updated = True
393 if unset:
394 for dot_k in unset:
395 try:
garciadeblas2644b762021-03-24 09:21:01 +0100396 dict_to_update, key_to_update, _ = _iterate_keys(
397 dot_k, db_item, populate=False
398 )
tierno7fc50dd2020-02-17 12:01:38 +0000399 del dict_to_update[key_to_update]
400 updated = True
aticigbf0e13e2022-08-24 22:41:56 +0300401 except Exception as unset_error:
402 self.logger.error(f"{unset_error} occured while updating DB.")
tierno7fc50dd2020-02-17 12:01:38 +0000403 if pull:
404 for dot_k, v in pull.items():
405 try:
garciadeblas2644b762021-03-24 09:21:01 +0100406 dict_to_update, key_to_update, _ = _iterate_keys(
407 dot_k, db_item, populate=False
408 )
aticigbf0e13e2022-08-24 22:41:56 +0300409 except Exception as pull_error:
410 self.logger.error(f"{pull_error} occured while updating DB.")
tierno7fc50dd2020-02-17 12:01:38 +0000411 continue
aticigbf0e13e2022-08-24 22:41:56 +0300412
tierno7fc50dd2020-02-17 12:01:38 +0000413 if key_to_update not in dict_to_update:
414 continue
415 if not isinstance(dict_to_update[key_to_update], list):
garciadeblas2644b762021-03-24 09:21:01 +0100416 raise DbException(
417 "Cannot pull '{}'. Target is not a list".format(dot_k)
418 )
tierno7fc50dd2020-02-17 12:01:38 +0000419 while v in dict_to_update[key_to_update]:
420 dict_to_update[key_to_update].remove(v)
421 updated = True
tierno0d8e4bc2020-06-22 12:18:18 +0000422 if pull_list:
423 for dot_k, v in pull_list.items():
424 if not isinstance(v, list):
garciadeblas2644b762021-03-24 09:21:01 +0100425 raise DbException(
426 "Invalid content at pull_list, '{}' must be an array".format(
427 dot_k
428 ),
429 http_code=HTTPStatus.BAD_REQUEST,
430 )
tierno0d8e4bc2020-06-22 12:18:18 +0000431 try:
garciadeblas2644b762021-03-24 09:21:01 +0100432 dict_to_update, key_to_update, _ = _iterate_keys(
433 dot_k, db_item, populate=False
434 )
aticigbf0e13e2022-08-24 22:41:56 +0300435 except Exception as iterate_error:
436 self.logger.error(
437 f"{iterate_error} occured while iterating keys in db update."
438 )
tierno0d8e4bc2020-06-22 12:18:18 +0000439 continue
aticigbf0e13e2022-08-24 22:41:56 +0300440
tierno0d8e4bc2020-06-22 12:18:18 +0000441 if key_to_update not in dict_to_update:
442 continue
443 if not isinstance(dict_to_update[key_to_update], list):
garciadeblas2644b762021-03-24 09:21:01 +0100444 raise DbException(
445 "Cannot pull_list '{}'. Target is not a list".format(dot_k)
446 )
tierno0d8e4bc2020-06-22 12:18:18 +0000447 for single_v in v:
448 while single_v in dict_to_update[key_to_update]:
449 dict_to_update[key_to_update].remove(single_v)
450 updated = True
tierno7fc50dd2020-02-17 12:01:38 +0000451 if push:
452 for dot_k, v in push.items():
garciadeblas2644b762021-03-24 09:21:01 +0100453 dict_to_update, key_to_update, populated = _iterate_keys(
454 dot_k, db_item
455 )
456 if (
457 isinstance(dict_to_update, dict)
458 and key_to_update not in dict_to_update
459 ):
tierno7fc50dd2020-02-17 12:01:38 +0000460 dict_to_update[key_to_update] = [v]
461 updated = True
462 elif populated and dict_to_update[key_to_update] is None:
463 dict_to_update[key_to_update] = [v]
464 updated = True
465 elif not isinstance(dict_to_update[key_to_update], list):
garciadeblas2644b762021-03-24 09:21:01 +0100466 raise DbException(
467 "Cannot push '{}'. Target is not a list".format(dot_k)
468 )
tierno7fc50dd2020-02-17 12:01:38 +0000469 else:
470 dict_to_update[key_to_update].append(v)
471 updated = True
tierno399f6c32020-05-12 07:36:41 +0000472 if push_list:
473 for dot_k, v in push_list.items():
474 if not isinstance(v, list):
garciadeblas2644b762021-03-24 09:21:01 +0100475 raise DbException(
476 "Invalid content at push_list, '{}' must be an array".format(
477 dot_k
478 ),
479 http_code=HTTPStatus.BAD_REQUEST,
480 )
481 dict_to_update, key_to_update, populated = _iterate_keys(
482 dot_k, db_item
483 )
484 if (
485 isinstance(dict_to_update, dict)
486 and key_to_update not in dict_to_update
487 ):
tierno399f6c32020-05-12 07:36:41 +0000488 dict_to_update[key_to_update] = v.copy()
489 updated = True
490 elif populated and dict_to_update[key_to_update] is None:
491 dict_to_update[key_to_update] = v.copy()
492 updated = True
493 elif not isinstance(dict_to_update[key_to_update], list):
garciadeblas2644b762021-03-24 09:21:01 +0100494 raise DbException(
495 "Cannot push '{}'. Target is not a list".format(dot_k),
496 http_code=HTTPStatus.CONFLICT,
497 )
tierno399f6c32020-05-12 07:36:41 +0000498 else:
499 dict_to_update[key_to_update] += v
500 updated = True
tierno7fc50dd2020-02-17 12:01:38 +0000501
502 return updated
503 except DbException:
504 raise
505 except Exception as e: # TODO refine
506 raise DbException(str(e))
507
garciadeblas2644b762021-03-24 09:21:01 +0100508 def set_one(
509 self,
510 table,
511 q_filter,
512 update_dict,
513 fail_on_empty=True,
514 unset=None,
515 pull=None,
516 push=None,
517 push_list=None,
518 pull_list=None,
519 ):
tierno6472e2b2019-09-02 16:04:16 +0000520 """
521 Modifies an entry at database
522 :param table: collection or table
523 :param q_filter: Filter
524 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
525 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
526 it raises a DbException
527 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
528 ignored. If not exist, it is ignored
529 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
530 if exist in the array is removed. If not exist, it is ignored
tierno0d8e4bc2020-06-22 12:18:18 +0000531 :param pull_list: Same as pull but values are arrays where each item is removed from the array
tierno6472e2b2019-09-02 16:04:16 +0000532 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
533 is appended to the end of the array
tierno399f6c32020-05-12 07:36:41 +0000534 :param push_list: Same as push but values are arrays where each item is and appended instead of appending the
535 whole array
tierno6472e2b2019-09-02 16:04:16 +0000536 :return: Dict with the number of entries modified. None if no matching is found.
537 """
tierno7fc50dd2020-02-17 12:01:38 +0000538 with self.lock:
539 for i, db_item in self._find(table, self._format_filter(q_filter)):
garciadeblas2644b762021-03-24 09:21:01 +0100540 updated = self._update(
541 db_item,
542 update_dict,
543 unset=unset,
544 pull=pull,
545 push=push,
546 push_list=push_list,
547 pull_list=pull_list,
548 )
tierno7fc50dd2020-02-17 12:01:38 +0000549 return {"updated": 1 if updated else 0}
550 else:
551 if fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100552 raise DbException(
553 "Not found entry with _id='{}'".format(q_filter),
554 HTTPStatus.NOT_FOUND,
555 )
tierno7fc50dd2020-02-17 12:01:38 +0000556 return None
tierno6472e2b2019-09-02 16:04:16 +0000557
garciadeblas2644b762021-03-24 09:21:01 +0100558 def set_list(
559 self,
560 table,
561 q_filter,
562 update_dict,
563 unset=None,
564 pull=None,
565 push=None,
566 push_list=None,
567 pull_list=None,
568 ):
tierno399f6c32020-05-12 07:36:41 +0000569 """Modifies al matching entries at database. Same as push. Do not fail if nothing matches"""
tierno7fc50dd2020-02-17 12:01:38 +0000570 with self.lock:
571 updated = 0
tierno77e2d6a2020-03-18 07:31:54 +0000572 found = 0
573 for _, db_item in self._find(table, self._format_filter(q_filter)):
574 found += 1
garciadeblas2644b762021-03-24 09:21:01 +0100575 if self._update(
576 db_item,
577 update_dict,
578 unset=unset,
579 pull=pull,
580 push=push,
581 push_list=push_list,
582 pull_list=pull_list,
583 ):
tierno7fc50dd2020-02-17 12:01:38 +0000584 updated += 1
tierno70911f02020-03-30 08:56:15 +0000585 # if not found and fail_on_empty:
586 # raise DbException("Not found entry with '{}'".format(q_filter), HTTPStatus.NOT_FOUND)
tierno77e2d6a2020-03-18 07:31:54 +0000587 return {"updated": updated} if found else None
tierno6472e2b2019-09-02 16:04:16 +0000588
tierno87858ca2018-10-08 16:30:15 +0200589 def replace(self, table, _id, indata, fail_on_empty=True):
590 """
591 Replace the content of an entry
592 :param table: collection or table
593 :param _id: internal database id
594 :param indata: content to replace
595 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
596 it raises a DbException
597 :return: Dict with the number of entries replaced
598 """
tierno5c012612018-04-19 16:01:59 +0200599 try:
tierno1e9a3292018-11-05 18:18:45 +0100600 with self.lock:
601 for i, _ in self._find(table, self._format_filter({"_id": _id})):
602 break
603 else:
604 if fail_on_empty:
garciadeblas2644b762021-03-24 09:21:01 +0100605 raise DbException(
606 "Not found entry with _id='{}'".format(_id),
607 HTTPStatus.NOT_FOUND,
608 )
tierno1e9a3292018-11-05 18:18:45 +0100609 return None
610 self.db[table][i] = deepcopy(indata)
Eduardo Sousa22f0fcd2018-04-26 15:43:28 +0100611 return {"updated": 1}
tierno136f2952018-10-19 13:01:03 +0200612 except DbException:
613 raise
tierno5c012612018-04-19 16:01:59 +0200614 except Exception as e: # TODO refine
615 raise DbException(str(e))
616
617 def create(self, table, indata):
tierno87858ca2018-10-08 16:30:15 +0200618 """
619 Add a new entry at database
620 :param table: collection or table
621 :param indata: content to be added
tierno2c9794c2020-04-29 10:24:28 +0000622 :return: database '_id' of the inserted element. Raises a DbException on error
tierno87858ca2018-10-08 16:30:15 +0200623 """
tierno5c012612018-04-19 16:01:59 +0200624 try:
625 id = indata.get("_id")
626 if not id:
627 id = str(uuid4())
628 indata["_id"] = id
tierno1e9a3292018-11-05 18:18:45 +0100629 with self.lock:
630 if table not in self.db:
631 self.db[table] = []
632 self.db[table].append(deepcopy(indata))
tierno5c012612018-04-19 16:01:59 +0200633 return id
634 except Exception as e: # TODO refine
635 raise DbException(str(e))
636
tierno6472e2b2019-09-02 16:04:16 +0000637 def create_list(self, table, indata_list):
638 """
639 Add a new entry at database
640 :param table: collection or table
641 :param indata_list: list content to be added
tierno2c9794c2020-04-29 10:24:28 +0000642 :return: list of inserted 'id's. Raises a DbException on error
tierno6472e2b2019-09-02 16:04:16 +0000643 """
644 try:
645 _ids = []
tierno40e326a2019-09-19 09:23:44 +0000646 with self.lock:
647 for indata in indata_list:
648 _id = indata.get("_id")
649 if not _id:
650 _id = str(uuid4())
651 indata["_id"] = _id
652 with self.lock:
653 if table not in self.db:
654 self.db[table] = []
655 self.db[table].append(deepcopy(indata))
656 _ids.append(_id)
tierno6472e2b2019-09-02 16:04:16 +0000657 return _ids
658 except Exception as e: # TODO refine
659 raise DbException(str(e))
660
tierno5c012612018-04-19 16:01:59 +0200661
garciadeblas2644b762021-03-24 09:21:01 +0100662if __name__ == "__main__":
tierno5c012612018-04-19 16:01:59 +0200663 # some test code
tierno3054f782018-04-25 16:59:53 +0200664 db = DbMemory()
tierno5c012612018-04-19 16:01:59 +0200665 db.create("test", {"_id": 1, "data": 1})
666 db.create("test", {"_id": 2, "data": 2})
667 db.create("test", {"_id": 3, "data": 3})
668 print("must be 3 items:", db.get_list("test"))
669 print("must return item 2:", db.get_list("test", {"_id": 2}))
670 db.del_one("test", {"_id": 2})
671 print("must be emtpy:", db.get_list("test", {"_id": 2}))