1 |
|
# -*- coding: utf-8 -*- |
2 |
|
|
3 |
|
# Copyright 2018 Telefonica S.A. |
4 |
|
# |
5 |
|
# Licensed under the Apache License, Version 2.0 (the "License"); |
6 |
|
# you may not use this file except in compliance with the License. |
7 |
|
# You may obtain a copy of the License at |
8 |
|
# |
9 |
|
# http://www.apache.org/licenses/LICENSE-2.0 |
10 |
|
# |
11 |
|
# Unless required by applicable law or agreed to in writing, software |
12 |
|
# distributed under the License is distributed on an "AS IS" BASIS, |
13 |
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
14 |
|
# implied. |
15 |
|
# See the License for the specific language governing permissions and |
16 |
|
# limitations under the License. |
17 |
|
|
18 |
1 |
from copy import deepcopy |
19 |
1 |
from http import HTTPStatus |
20 |
1 |
import logging |
21 |
1 |
from uuid import uuid4 |
22 |
|
|
23 |
1 |
from osm_common.dbbase import DbBase, DbException |
24 |
1 |
from osm_common.dbmongo import deep_update |
25 |
|
|
26 |
|
|
27 |
1 |
__author__ = "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>" |
28 |
|
|
29 |
|
|
30 |
1 |
class DbMemory(DbBase): |
31 |
1 |
def __init__(self, logger_name="db", lock=False): |
32 |
1 |
super().__init__(logger_name=logger_name, lock=lock) |
33 |
1 |
self.db = {} |
34 |
|
|
35 |
1 |
def db_connect(self, config): |
36 |
|
""" |
37 |
|
Connect to database |
38 |
|
:param config: Configuration of database |
39 |
|
:return: None or raises DbException on error |
40 |
|
""" |
41 |
1 |
if "logger_name" in config: |
42 |
1 |
self.logger = logging.getLogger(config["logger_name"]) |
43 |
1 |
master_key = config.get("commonkey") or config.get("masterpassword") |
44 |
1 |
if master_key: |
45 |
0 |
self.set_secret_key(master_key) |
46 |
|
|
47 |
1 |
@staticmethod |
48 |
1 |
def _format_filter(q_filter): |
49 |
1 |
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 |
1 |
if q_filter: |
53 |
0 |
for k, v in q_filter.items(): |
54 |
0 |
db_v = v |
55 |
0 |
kleft, _, kright = k.rpartition(".ANYINDEX.") |
56 |
0 |
while kleft: |
57 |
0 |
k = kleft + ".ANYINDEX" |
58 |
0 |
db_v = {kright: db_v} |
59 |
0 |
kleft, _, kright = k.rpartition(".ANYINDEX.") |
60 |
0 |
deep_update(db_filter, {k: db_v}) |
61 |
|
|
62 |
1 |
return db_filter |
63 |
|
|
64 |
1 |
def _find(self, table, q_filter): |
65 |
0 |
def recursive_find(key_list, key_next_index, content, oper, target): |
66 |
0 |
if key_next_index == len(key_list) or content is None: |
67 |
0 |
try: |
68 |
0 |
if oper in ("eq", "cont"): |
69 |
0 |
if isinstance(target, list): |
70 |
0 |
if isinstance(content, list): |
71 |
0 |
return any( |
72 |
|
content_item in target for content_item in content |
73 |
|
) |
74 |
0 |
return content in target |
75 |
0 |
elif isinstance(content, list): |
76 |
0 |
return target in content |
77 |
|
else: |
78 |
0 |
return content == target |
79 |
0 |
elif oper in ("neq", "ne", "ncont"): |
80 |
0 |
if isinstance(target, list): |
81 |
0 |
if isinstance(content, list): |
82 |
0 |
return all( |
83 |
|
content_item not in target |
84 |
|
for content_item in content |
85 |
|
) |
86 |
0 |
return content not in target |
87 |
0 |
elif isinstance(content, list): |
88 |
0 |
return target not in content |
89 |
|
else: |
90 |
0 |
return content != target |
91 |
0 |
if oper == "gt": |
92 |
0 |
return content > target |
93 |
0 |
elif oper == "gte": |
94 |
0 |
return content >= target |
95 |
0 |
elif oper == "lt": |
96 |
0 |
return content < target |
97 |
0 |
elif oper == "lte": |
98 |
0 |
return content <= target |
99 |
|
else: |
100 |
0 |
raise DbException( |
101 |
|
"Unknown filter operator '{}' in key '{}'".format( |
102 |
|
oper, ".".join(key_list) |
103 |
|
), |
104 |
|
http_code=HTTPStatus.BAD_REQUEST, |
105 |
|
) |
106 |
0 |
except TypeError: |
107 |
0 |
return False |
108 |
|
|
109 |
0 |
elif isinstance(content, dict): |
110 |
0 |
return recursive_find( |
111 |
|
key_list, |
112 |
|
key_next_index + 1, |
113 |
|
content.get(key_list[key_next_index]), |
114 |
|
oper, |
115 |
|
target, |
116 |
|
) |
117 |
0 |
elif isinstance(content, list): |
118 |
0 |
look_for_match = True # when there is a match return immediately |
119 |
0 |
if (target is None) != ( |
120 |
|
oper in ("neq", "ne", "ncont") |
121 |
|
): # one True and other False (Xor) |
122 |
0 |
look_for_match = ( |
123 |
|
False # when there is not a match return immediately |
124 |
|
) |
125 |
|
|
126 |
0 |
for content_item in content: |
127 |
0 |
if key_list[key_next_index] == "ANYINDEX" and isinstance(v, dict): |
128 |
0 |
matches = True |
129 |
0 |
if target: |
130 |
0 |
for k2, v2 in target.items(): |
131 |
0 |
k_new_list = k2.split(".") |
132 |
0 |
new_operator = "eq" |
133 |
0 |
if k_new_list[-1] in ( |
134 |
|
"eq", |
135 |
|
"ne", |
136 |
|
"gt", |
137 |
|
"gte", |
138 |
|
"lt", |
139 |
|
"lte", |
140 |
|
"cont", |
141 |
|
"ncont", |
142 |
|
"neq", |
143 |
|
): |
144 |
0 |
new_operator = k_new_list.pop() |
145 |
0 |
if not recursive_find( |
146 |
|
k_new_list, 0, content_item, new_operator, v2 |
147 |
|
): |
148 |
0 |
matches = False |
149 |
0 |
break |
150 |
|
|
151 |
|
else: |
152 |
0 |
matches = recursive_find( |
153 |
|
key_list, key_next_index, content_item, oper, target |
154 |
|
) |
155 |
0 |
if matches == look_for_match: |
156 |
0 |
return matches |
157 |
0 |
if key_list[key_next_index].isdecimal() and int( |
158 |
|
key_list[key_next_index] |
159 |
|
) < len(content): |
160 |
0 |
matches = recursive_find( |
161 |
|
key_list, |
162 |
|
key_next_index + 1, |
163 |
|
content[int(key_list[key_next_index])], |
164 |
|
oper, |
165 |
|
target, |
166 |
|
) |
167 |
0 |
if matches == look_for_match: |
168 |
0 |
return matches |
169 |
0 |
return not look_for_match |
170 |
|
else: # content is not dict, nor list neither None, so not found |
171 |
0 |
if oper in ("neq", "ne", "ncont"): |
172 |
0 |
return target is not None |
173 |
|
else: |
174 |
0 |
return target is None |
175 |
|
|
176 |
0 |
for i, row in enumerate(self.db.get(table, ())): |
177 |
0 |
q_filter = q_filter or {} |
178 |
0 |
for k, v in q_filter.items(): |
179 |
0 |
k_list = k.split(".") |
180 |
0 |
operator = "eq" |
181 |
0 |
if k_list[-1] in ( |
182 |
|
"eq", |
183 |
|
"ne", |
184 |
|
"gt", |
185 |
|
"gte", |
186 |
|
"lt", |
187 |
|
"lte", |
188 |
|
"cont", |
189 |
|
"ncont", |
190 |
|
"neq", |
191 |
|
): |
192 |
0 |
operator = k_list.pop() |
193 |
0 |
matches = recursive_find(k_list, 0, row, operator, v) |
194 |
0 |
if not matches: |
195 |
0 |
break |
196 |
|
else: |
197 |
|
# match |
198 |
0 |
yield i, row |
199 |
|
|
200 |
1 |
def get_list(self, table, q_filter=None): |
201 |
|
""" |
202 |
|
Obtain a list of entries matching q_filter |
203 |
|
:param table: collection or table |
204 |
|
:param q_filter: Filter |
205 |
|
:return: a list (can be empty) with the found entries. Raises DbException on error |
206 |
|
""" |
207 |
0 |
try: |
208 |
0 |
result = [] |
209 |
0 |
with self.lock: |
210 |
0 |
for _, row in self._find(table, self._format_filter(q_filter)): |
211 |
0 |
result.append(deepcopy(row)) |
212 |
0 |
return result |
213 |
0 |
except DbException: |
214 |
0 |
raise |
215 |
0 |
except Exception as e: # TODO refine |
216 |
0 |
raise DbException(str(e)) |
217 |
|
|
218 |
1 |
def count(self, table, q_filter=None): |
219 |
|
""" |
220 |
|
Count the number of entries matching q_filter |
221 |
|
:param table: collection or table |
222 |
|
:param q_filter: Filter |
223 |
|
:return: number of entries found (can be zero) |
224 |
|
:raise: DbException on error |
225 |
|
""" |
226 |
0 |
try: |
227 |
0 |
with self.lock: |
228 |
0 |
return sum(1 for x in self._find(table, self._format_filter(q_filter))) |
229 |
0 |
except DbException: |
230 |
0 |
raise |
231 |
0 |
except Exception as e: # TODO refine |
232 |
0 |
raise DbException(str(e)) |
233 |
|
|
234 |
1 |
def get_one(self, table, q_filter=None, fail_on_empty=True, fail_on_more=True): |
235 |
|
""" |
236 |
|
Obtain one entry matching q_filter |
237 |
|
:param table: collection or table |
238 |
|
:param q_filter: Filter |
239 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
240 |
|
it raises a DbException |
241 |
|
:param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so |
242 |
|
that it raises a DbException |
243 |
|
:return: The requested element, or None |
244 |
|
""" |
245 |
0 |
try: |
246 |
0 |
result = None |
247 |
0 |
with self.lock: |
248 |
0 |
for _, row in self._find(table, self._format_filter(q_filter)): |
249 |
0 |
if not fail_on_more: |
250 |
0 |
return deepcopy(row) |
251 |
0 |
if result: |
252 |
0 |
raise DbException( |
253 |
|
"Found more than one entry with filter='{}'".format( |
254 |
|
q_filter |
255 |
|
), |
256 |
|
HTTPStatus.CONFLICT.value, |
257 |
|
) |
258 |
0 |
result = row |
259 |
0 |
if not result and fail_on_empty: |
260 |
0 |
raise DbException( |
261 |
|
"Not found entry with filter='{}'".format(q_filter), |
262 |
|
HTTPStatus.NOT_FOUND, |
263 |
|
) |
264 |
0 |
return deepcopy(result) |
265 |
0 |
except Exception as e: # TODO refine |
266 |
0 |
raise DbException(str(e)) |
267 |
|
|
268 |
1 |
def del_list(self, table, q_filter=None): |
269 |
|
""" |
270 |
|
Deletes all entries that match q_filter |
271 |
|
:param table: collection or table |
272 |
|
:param q_filter: Filter |
273 |
|
:return: Dict with the number of entries deleted |
274 |
|
""" |
275 |
0 |
try: |
276 |
0 |
id_list = [] |
277 |
0 |
with self.lock: |
278 |
0 |
for i, _ in self._find(table, self._format_filter(q_filter)): |
279 |
0 |
id_list.append(i) |
280 |
0 |
deleted = len(id_list) |
281 |
0 |
for i in reversed(id_list): |
282 |
0 |
del self.db[table][i] |
283 |
0 |
return {"deleted": deleted} |
284 |
0 |
except DbException: |
285 |
0 |
raise |
286 |
0 |
except Exception as e: # TODO refine |
287 |
0 |
raise DbException(str(e)) |
288 |
|
|
289 |
1 |
def del_one(self, table, q_filter=None, fail_on_empty=True): |
290 |
|
""" |
291 |
|
Deletes one entry that matches q_filter |
292 |
|
:param table: collection or table |
293 |
|
:param q_filter: Filter |
294 |
|
:param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in |
295 |
|
which case it raises a DbException |
296 |
|
:return: Dict with the number of entries deleted |
297 |
|
""" |
298 |
0 |
try: |
299 |
0 |
with self.lock: |
300 |
0 |
for i, _ in self._find(table, self._format_filter(q_filter)): |
301 |
0 |
break |
302 |
|
else: |
303 |
0 |
if fail_on_empty: |
304 |
0 |
raise DbException( |
305 |
|
"Not found entry with filter='{}'".format(q_filter), |
306 |
|
HTTPStatus.NOT_FOUND, |
307 |
|
) |
308 |
0 |
return None |
309 |
0 |
del self.db[table][i] |
310 |
0 |
return {"deleted": 1} |
311 |
0 |
except Exception as e: # TODO refine |
312 |
0 |
raise DbException(str(e)) |
313 |
|
|
314 |
1 |
def _update( |
315 |
|
self, |
316 |
|
db_item, |
317 |
|
update_dict, |
318 |
|
unset=None, |
319 |
|
pull=None, |
320 |
|
push=None, |
321 |
|
push_list=None, |
322 |
|
pull_list=None, |
323 |
|
): |
324 |
|
""" |
325 |
|
Modifies an entry at database |
326 |
|
:param db_item: entry of the table to update |
327 |
|
:param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value |
328 |
|
:param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is |
329 |
|
ignored. If not exist, it is ignored |
330 |
|
:param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value |
331 |
|
if exist in the array is removed. If not exist, it is ignored |
332 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
333 |
|
:param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value |
334 |
|
is appended to the end of the array |
335 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
336 |
|
whole array |
337 |
|
:return: True if database has been changed, False if not; Exception on error |
338 |
|
""" |
339 |
|
|
340 |
1 |
def _iterate_keys(k, db_nested, populate=True): |
341 |
1 |
k_list = k.split(".") |
342 |
1 |
k_item_prev = k_list[0] |
343 |
1 |
populated = False |
344 |
1 |
if k_item_prev not in db_nested and populate: |
345 |
1 |
populated = True |
346 |
1 |
db_nested[k_item_prev] = None |
347 |
1 |
for k_item in k_list[1:]: |
348 |
1 |
if isinstance(db_nested[k_item_prev], dict): |
349 |
1 |
if k_item not in db_nested[k_item_prev]: |
350 |
1 |
if not populate: |
351 |
1 |
raise DbException( |
352 |
|
"Cannot set '{}', not existing '{}'".format(k, k_item) |
353 |
|
) |
354 |
1 |
populated = True |
355 |
1 |
db_nested[k_item_prev][k_item] = None |
356 |
1 |
elif isinstance(db_nested[k_item_prev], list) and k_item.isdigit(): |
357 |
|
# extend list with Nones if index greater than list |
358 |
1 |
k_item = int(k_item) |
359 |
1 |
if k_item >= len(db_nested[k_item_prev]): |
360 |
1 |
if not populate: |
361 |
1 |
raise DbException( |
362 |
|
"Cannot set '{}', index too large '{}'".format( |
363 |
|
k, k_item |
364 |
|
) |
365 |
|
) |
366 |
1 |
populated = True |
367 |
1 |
db_nested[k_item_prev] += [None] * ( |
368 |
|
k_item - len(db_nested[k_item_prev]) + 1 |
369 |
|
) |
370 |
1 |
elif db_nested[k_item_prev] is None: |
371 |
1 |
if not populate: |
372 |
0 |
raise DbException( |
373 |
|
"Cannot set '{}', not existing '{}'".format(k, k_item) |
374 |
|
) |
375 |
1 |
populated = True |
376 |
1 |
db_nested[k_item_prev] = {k_item: None} |
377 |
|
else: # number, string, boolean, ... or list but with not integer key |
378 |
1 |
raise DbException( |
379 |
|
"Cannot set '{}' on existing '{}={}'".format( |
380 |
|
k, k_item_prev, db_nested[k_item_prev] |
381 |
|
) |
382 |
|
) |
383 |
1 |
db_nested = db_nested[k_item_prev] |
384 |
1 |
k_item_prev = k_item |
385 |
1 |
return db_nested, k_item_prev, populated |
386 |
|
|
387 |
1 |
updated = False |
388 |
1 |
try: |
389 |
1 |
if update_dict: |
390 |
1 |
for dot_k, v in update_dict.items(): |
391 |
1 |
dict_to_update, key_to_update, _ = _iterate_keys(dot_k, db_item) |
392 |
1 |
dict_to_update[key_to_update] = v |
393 |
1 |
updated = True |
394 |
1 |
if unset: |
395 |
1 |
for dot_k in unset: |
396 |
1 |
try: |
397 |
1 |
dict_to_update, key_to_update, _ = _iterate_keys( |
398 |
|
dot_k, db_item, populate=False |
399 |
|
) |
400 |
1 |
del dict_to_update[key_to_update] |
401 |
1 |
updated = True |
402 |
1 |
except Exception as unset_error: |
403 |
1 |
self.logger.error(f"{unset_error} occured while updating DB.") |
404 |
1 |
if pull: |
405 |
1 |
for dot_k, v in pull.items(): |
406 |
1 |
try: |
407 |
1 |
dict_to_update, key_to_update, _ = _iterate_keys( |
408 |
|
dot_k, db_item, populate=False |
409 |
|
) |
410 |
1 |
except Exception as pull_error: |
411 |
1 |
self.logger.error(f"{pull_error} occured while updating DB.") |
412 |
1 |
continue |
413 |
|
|
414 |
1 |
if key_to_update not in dict_to_update: |
415 |
1 |
continue |
416 |
1 |
if not isinstance(dict_to_update[key_to_update], list): |
417 |
1 |
raise DbException( |
418 |
|
"Cannot pull '{}'. Target is not a list".format(dot_k) |
419 |
|
) |
420 |
1 |
while v in dict_to_update[key_to_update]: |
421 |
1 |
dict_to_update[key_to_update].remove(v) |
422 |
1 |
updated = True |
423 |
1 |
if pull_list: |
424 |
0 |
for dot_k, v in pull_list.items(): |
425 |
0 |
if not isinstance(v, list): |
426 |
0 |
raise DbException( |
427 |
|
"Invalid content at pull_list, '{}' must be an array".format( |
428 |
|
dot_k |
429 |
|
), |
430 |
|
http_code=HTTPStatus.BAD_REQUEST, |
431 |
|
) |
432 |
0 |
try: |
433 |
0 |
dict_to_update, key_to_update, _ = _iterate_keys( |
434 |
|
dot_k, db_item, populate=False |
435 |
|
) |
436 |
0 |
except Exception as iterate_error: |
437 |
0 |
self.logger.error( |
438 |
|
f"{iterate_error} occured while iterating keys in db update." |
439 |
|
) |
440 |
0 |
continue |
441 |
|
|
442 |
0 |
if key_to_update not in dict_to_update: |
443 |
0 |
continue |
444 |
0 |
if not isinstance(dict_to_update[key_to_update], list): |
445 |
0 |
raise DbException( |
446 |
|
"Cannot pull_list '{}'. Target is not a list".format(dot_k) |
447 |
|
) |
448 |
0 |
for single_v in v: |
449 |
0 |
while single_v in dict_to_update[key_to_update]: |
450 |
0 |
dict_to_update[key_to_update].remove(single_v) |
451 |
0 |
updated = True |
452 |
1 |
if push: |
453 |
1 |
for dot_k, v in push.items(): |
454 |
1 |
dict_to_update, key_to_update, populated = _iterate_keys( |
455 |
|
dot_k, db_item |
456 |
|
) |
457 |
1 |
if ( |
458 |
|
isinstance(dict_to_update, dict) |
459 |
|
and key_to_update not in dict_to_update |
460 |
|
): |
461 |
0 |
dict_to_update[key_to_update] = [v] |
462 |
0 |
updated = True |
463 |
1 |
elif populated and dict_to_update[key_to_update] is None: |
464 |
1 |
dict_to_update[key_to_update] = [v] |
465 |
1 |
updated = True |
466 |
1 |
elif not isinstance(dict_to_update[key_to_update], list): |
467 |
1 |
raise DbException( |
468 |
|
"Cannot push '{}'. Target is not a list".format(dot_k) |
469 |
|
) |
470 |
|
else: |
471 |
1 |
dict_to_update[key_to_update].append(v) |
472 |
1 |
updated = True |
473 |
1 |
if push_list: |
474 |
1 |
for dot_k, v in push_list.items(): |
475 |
1 |
if not isinstance(v, list): |
476 |
1 |
raise DbException( |
477 |
|
"Invalid content at push_list, '{}' must be an array".format( |
478 |
|
dot_k |
479 |
|
), |
480 |
|
http_code=HTTPStatus.BAD_REQUEST, |
481 |
|
) |
482 |
1 |
dict_to_update, key_to_update, populated = _iterate_keys( |
483 |
|
dot_k, db_item |
484 |
|
) |
485 |
1 |
if ( |
486 |
|
isinstance(dict_to_update, dict) |
487 |
|
and key_to_update not in dict_to_update |
488 |
|
): |
489 |
0 |
dict_to_update[key_to_update] = v.copy() |
490 |
0 |
updated = True |
491 |
1 |
elif populated and dict_to_update[key_to_update] is None: |
492 |
1 |
dict_to_update[key_to_update] = v.copy() |
493 |
1 |
updated = True |
494 |
1 |
elif not isinstance(dict_to_update[key_to_update], list): |
495 |
1 |
raise DbException( |
496 |
|
"Cannot push '{}'. Target is not a list".format(dot_k), |
497 |
|
http_code=HTTPStatus.CONFLICT, |
498 |
|
) |
499 |
|
else: |
500 |
1 |
dict_to_update[key_to_update] += v |
501 |
1 |
updated = True |
502 |
|
|
503 |
1 |
return updated |
504 |
1 |
except DbException: |
505 |
1 |
raise |
506 |
0 |
except Exception as e: # TODO refine |
507 |
0 |
raise DbException(str(e)) |
508 |
|
|
509 |
1 |
def set_one( |
510 |
|
self, |
511 |
|
table, |
512 |
|
q_filter, |
513 |
|
update_dict, |
514 |
|
fail_on_empty=True, |
515 |
|
unset=None, |
516 |
|
pull=None, |
517 |
|
push=None, |
518 |
|
push_list=None, |
519 |
|
pull_list=None, |
520 |
|
): |
521 |
|
""" |
522 |
|
Modifies an entry at database |
523 |
|
:param table: collection or table |
524 |
|
:param q_filter: Filter |
525 |
|
:param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value |
526 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
527 |
|
it raises a DbException |
528 |
|
:param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is |
529 |
|
ignored. If not exist, it is ignored |
530 |
|
:param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value |
531 |
|
if exist in the array is removed. If not exist, it is ignored |
532 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
533 |
|
:param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value |
534 |
|
is appended to the end of the array |
535 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
536 |
|
whole array |
537 |
|
:return: Dict with the number of entries modified. None if no matching is found. |
538 |
|
""" |
539 |
1 |
with self.lock: |
540 |
1 |
for i, db_item in self._find(table, self._format_filter(q_filter)): |
541 |
1 |
updated = self._update( |
542 |
|
db_item, |
543 |
|
update_dict, |
544 |
|
unset=unset, |
545 |
|
pull=pull, |
546 |
|
push=push, |
547 |
|
push_list=push_list, |
548 |
|
pull_list=pull_list, |
549 |
|
) |
550 |
1 |
return {"updated": 1 if updated else 0} |
551 |
|
else: |
552 |
0 |
if fail_on_empty: |
553 |
0 |
raise DbException( |
554 |
|
"Not found entry with _id='{}'".format(q_filter), |
555 |
|
HTTPStatus.NOT_FOUND, |
556 |
|
) |
557 |
0 |
return None |
558 |
|
|
559 |
1 |
def set_list( |
560 |
|
self, |
561 |
|
table, |
562 |
|
q_filter, |
563 |
|
update_dict, |
564 |
|
unset=None, |
565 |
|
pull=None, |
566 |
|
push=None, |
567 |
|
push_list=None, |
568 |
|
pull_list=None, |
569 |
|
): |
570 |
|
"""Modifies al matching entries at database. Same as push. Do not fail if nothing matches""" |
571 |
0 |
with self.lock: |
572 |
0 |
updated = 0 |
573 |
0 |
found = 0 |
574 |
0 |
for _, db_item in self._find(table, self._format_filter(q_filter)): |
575 |
0 |
found += 1 |
576 |
0 |
if self._update( |
577 |
|
db_item, |
578 |
|
update_dict, |
579 |
|
unset=unset, |
580 |
|
pull=pull, |
581 |
|
push=push, |
582 |
|
push_list=push_list, |
583 |
|
pull_list=pull_list, |
584 |
|
): |
585 |
0 |
updated += 1 |
586 |
|
# if not found and fail_on_empty: |
587 |
|
# raise DbException("Not found entry with '{}'".format(q_filter), HTTPStatus.NOT_FOUND) |
588 |
0 |
return {"updated": updated} if found else None |
589 |
|
|
590 |
1 |
def replace(self, table, _id, indata, fail_on_empty=True): |
591 |
|
""" |
592 |
|
Replace the content of an entry |
593 |
|
:param table: collection or table |
594 |
|
:param _id: internal database id |
595 |
|
:param indata: content to replace |
596 |
|
:param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case |
597 |
|
it raises a DbException |
598 |
|
:return: Dict with the number of entries replaced |
599 |
|
""" |
600 |
0 |
try: |
601 |
0 |
with self.lock: |
602 |
0 |
for i, _ in self._find(table, self._format_filter({"_id": _id})): |
603 |
0 |
break |
604 |
|
else: |
605 |
0 |
if fail_on_empty: |
606 |
0 |
raise DbException( |
607 |
|
"Not found entry with _id='{}'".format(_id), |
608 |
|
HTTPStatus.NOT_FOUND, |
609 |
|
) |
610 |
0 |
return None |
611 |
0 |
self.db[table][i] = deepcopy(indata) |
612 |
0 |
return {"updated": 1} |
613 |
0 |
except DbException: |
614 |
0 |
raise |
615 |
0 |
except Exception as e: # TODO refine |
616 |
0 |
raise DbException(str(e)) |
617 |
|
|
618 |
1 |
def create(self, table, indata): |
619 |
|
""" |
620 |
|
Add a new entry at database |
621 |
|
:param table: collection or table |
622 |
|
:param indata: content to be added |
623 |
|
:return: database '_id' of the inserted element. Raises a DbException on error |
624 |
|
""" |
625 |
0 |
try: |
626 |
0 |
id = indata.get("_id") |
627 |
0 |
if not id: |
628 |
0 |
id = str(uuid4()) |
629 |
0 |
indata["_id"] = id |
630 |
0 |
with self.lock: |
631 |
0 |
if table not in self.db: |
632 |
0 |
self.db[table] = [] |
633 |
0 |
self.db[table].append(deepcopy(indata)) |
634 |
0 |
return id |
635 |
0 |
except Exception as e: # TODO refine |
636 |
0 |
raise DbException(str(e)) |
637 |
|
|
638 |
1 |
def create_list(self, table, indata_list): |
639 |
|
""" |
640 |
|
Add a new entry at database |
641 |
|
:param table: collection or table |
642 |
|
:param indata_list: list content to be added |
643 |
|
:return: list of inserted 'id's. Raises a DbException on error |
644 |
|
""" |
645 |
0 |
try: |
646 |
0 |
_ids = [] |
647 |
0 |
with self.lock: |
648 |
0 |
for indata in indata_list: |
649 |
0 |
_id = indata.get("_id") |
650 |
0 |
if not _id: |
651 |
0 |
_id = str(uuid4()) |
652 |
0 |
indata["_id"] = _id |
653 |
0 |
with self.lock: |
654 |
0 |
if table not in self.db: |
655 |
0 |
self.db[table] = [] |
656 |
0 |
self.db[table].append(deepcopy(indata)) |
657 |
0 |
_ids.append(_id) |
658 |
0 |
return _ids |
659 |
0 |
except Exception as e: # TODO refine |
660 |
0 |
raise DbException(str(e)) |
661 |
|
|
662 |
|
|
663 |
1 |
if __name__ == "__main__": |
664 |
|
# some test code |
665 |
0 |
db = DbMemory() |
666 |
0 |
db.create("test", {"_id": 1, "data": 1}) |
667 |
0 |
db.create("test", {"_id": 2, "data": 2}) |
668 |
0 |
db.create("test", {"_id": 3, "data": 3}) |
669 |
0 |
print("must be 3 items:", db.get_list("test")) |
670 |
0 |
print("must return item 2:", db.get_list("test", {"_id": 2})) |
671 |
0 |
db.del_one("test", {"_id": 2}) |
672 |
0 |
print("must be emtpy:", db.get_list("test", {"_id": 2})) |