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, 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 |
for k2, v2 in target.items(): |
130 |
0 |
k_new_list = k2.split(".") |
131 |
0 |
new_operator = "eq" |
132 |
0 |
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 |
|
): |
143 |
0 |
new_operator = k_new_list.pop() |
144 |
0 |
if not recursive_find( |
145 |
|
k_new_list, 0, content_item, new_operator, v2 |
146 |
|
): |
147 |
0 |
matches = False |
148 |
0 |
break |
149 |
|
|
150 |
|
else: |
151 |
0 |
matches = recursive_find( |
152 |
|
key_list, key_next_index, content_item, oper, target |
153 |
|
) |
154 |
0 |
if matches == look_for_match: |
155 |
0 |
return matches |
156 |
0 |
if key_list[key_next_index].isdecimal() and int( |
157 |
|
key_list[key_next_index] |
158 |
|
) < len(content): |
159 |
0 |
matches = recursive_find( |
160 |
|
key_list, |
161 |
|
key_next_index + 1, |
162 |
|
content[int(key_list[key_next_index])], |
163 |
|
oper, |
164 |
|
target, |
165 |
|
) |
166 |
0 |
if matches == look_for_match: |
167 |
0 |
return matches |
168 |
0 |
return not look_for_match |
169 |
|
else: # content is not dict, nor list neither None, so not found |
170 |
0 |
if oper in ("neq", "ne", "ncont"): |
171 |
0 |
return target is not None |
172 |
|
else: |
173 |
0 |
return target is None |
174 |
|
|
175 |
0 |
for i, row in enumerate(self.db.get(table, ())): |
176 |
0 |
q_filter = q_filter or {} |
177 |
0 |
for k, v in q_filter.items(): |
178 |
0 |
k_list = k.split(".") |
179 |
0 |
operator = "eq" |
180 |
0 |
if k_list[-1] in ( |
181 |
|
"eq", |
182 |
|
"ne", |
183 |
|
"gt", |
184 |
|
"gte", |
185 |
|
"lt", |
186 |
|
"lte", |
187 |
|
"cont", |
188 |
|
"ncont", |
189 |
|
"neq", |
190 |
|
): |
191 |
0 |
operator = k_list.pop() |
192 |
0 |
matches = recursive_find(k_list, 0, row, operator, v) |
193 |
0 |
if not matches: |
194 |
0 |
break |
195 |
|
else: |
196 |
|
# match |
197 |
0 |
yield i, row |
198 |
|
|
199 |
1 |
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 |
|
""" |
206 |
0 |
try: |
207 |
0 |
result = [] |
208 |
0 |
with self.lock: |
209 |
0 |
for _, row in self._find(table, self._format_filter(q_filter)): |
210 |
0 |
result.append(deepcopy(row)) |
211 |
0 |
return result |
212 |
0 |
except DbException: |
213 |
0 |
raise |
214 |
0 |
except Exception as e: # TODO refine |
215 |
0 |
raise DbException(str(e)) |
216 |
|
|
217 |
1 |
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 |
0 |
try: |
226 |
0 |
with self.lock: |
227 |
0 |
return sum(1 for x in self._find(table, self._format_filter(q_filter))) |
228 |
0 |
except DbException: |
229 |
0 |
raise |
230 |
0 |
except Exception as e: # TODO refine |
231 |
0 |
raise DbException(str(e)) |
232 |
|
|
233 |
1 |
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 |
|
""" |
244 |
0 |
try: |
245 |
0 |
result = None |
246 |
0 |
with self.lock: |
247 |
0 |
for _, row in self._find(table, self._format_filter(q_filter)): |
248 |
0 |
if not fail_on_more: |
249 |
0 |
return deepcopy(row) |
250 |
0 |
if result: |
251 |
0 |
raise DbException( |
252 |
|
"Found more than one entry with filter='{}'".format( |
253 |
|
q_filter |
254 |
|
), |
255 |
|
HTTPStatus.CONFLICT.value, |
256 |
|
) |
257 |
0 |
result = row |
258 |
0 |
if not result and fail_on_empty: |
259 |
0 |
raise DbException( |
260 |
|
"Not found entry with filter='{}'".format(q_filter), |
261 |
|
HTTPStatus.NOT_FOUND, |
262 |
|
) |
263 |
0 |
return deepcopy(result) |
264 |
0 |
except Exception as e: # TODO refine |
265 |
0 |
raise DbException(str(e)) |
266 |
|
|
267 |
1 |
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 |
|
""" |
274 |
0 |
try: |
275 |
0 |
id_list = [] |
276 |
0 |
with self.lock: |
277 |
0 |
for i, _ in self._find(table, self._format_filter(q_filter)): |
278 |
0 |
id_list.append(i) |
279 |
0 |
deleted = len(id_list) |
280 |
0 |
for i in reversed(id_list): |
281 |
0 |
del self.db[table][i] |
282 |
0 |
return {"deleted": deleted} |
283 |
0 |
except DbException: |
284 |
0 |
raise |
285 |
0 |
except Exception as e: # TODO refine |
286 |
0 |
raise DbException(str(e)) |
287 |
|
|
288 |
1 |
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 |
|
""" |
297 |
0 |
try: |
298 |
0 |
with self.lock: |
299 |
0 |
for i, _ in self._find(table, self._format_filter(q_filter)): |
300 |
0 |
break |
301 |
|
else: |
302 |
0 |
if fail_on_empty: |
303 |
0 |
raise DbException( |
304 |
|
"Not found entry with filter='{}'".format(q_filter), |
305 |
|
HTTPStatus.NOT_FOUND, |
306 |
|
) |
307 |
0 |
return None |
308 |
0 |
del self.db[table][i] |
309 |
0 |
return {"deleted": 1} |
310 |
0 |
except Exception as e: # TODO refine |
311 |
0 |
raise DbException(str(e)) |
312 |
|
|
313 |
1 |
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 |
|
): |
323 |
|
""" |
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 |
331 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
332 |
|
: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 |
334 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
335 |
|
whole array |
336 |
|
:return: True if database has been changed, False if not; Exception on error |
337 |
|
""" |
338 |
|
|
339 |
1 |
def _iterate_keys(k, db_nested, populate=True): |
340 |
1 |
k_list = k.split(".") |
341 |
1 |
k_item_prev = k_list[0] |
342 |
1 |
populated = False |
343 |
1 |
if k_item_prev not in db_nested and populate: |
344 |
1 |
populated = True |
345 |
1 |
db_nested[k_item_prev] = None |
346 |
1 |
for k_item in k_list[1:]: |
347 |
1 |
if isinstance(db_nested[k_item_prev], dict): |
348 |
1 |
if k_item not in db_nested[k_item_prev]: |
349 |
1 |
if not populate: |
350 |
1 |
raise DbException( |
351 |
|
"Cannot set '{}', not existing '{}'".format(k, k_item) |
352 |
|
) |
353 |
1 |
populated = True |
354 |
1 |
db_nested[k_item_prev][k_item] = None |
355 |
1 |
elif isinstance(db_nested[k_item_prev], list) and k_item.isdigit(): |
356 |
|
# extend list with Nones if index greater than list |
357 |
1 |
k_item = int(k_item) |
358 |
1 |
if k_item >= len(db_nested[k_item_prev]): |
359 |
1 |
if not populate: |
360 |
1 |
raise DbException( |
361 |
|
"Cannot set '{}', index too large '{}'".format( |
362 |
|
k, k_item |
363 |
|
) |
364 |
|
) |
365 |
1 |
populated = True |
366 |
1 |
db_nested[k_item_prev] += [None] * ( |
367 |
|
k_item - len(db_nested[k_item_prev]) + 1 |
368 |
|
) |
369 |
1 |
elif db_nested[k_item_prev] is None: |
370 |
1 |
if not populate: |
371 |
0 |
raise DbException( |
372 |
|
"Cannot set '{}', not existing '{}'".format(k, k_item) |
373 |
|
) |
374 |
1 |
populated = True |
375 |
1 |
db_nested[k_item_prev] = {k_item: None} |
376 |
|
else: # number, string, boolean, ... or list but with not integer key |
377 |
1 |
raise DbException( |
378 |
|
"Cannot set '{}' on existing '{}={}'".format( |
379 |
|
k, k_item_prev, db_nested[k_item_prev] |
380 |
|
) |
381 |
|
) |
382 |
1 |
db_nested = db_nested[k_item_prev] |
383 |
1 |
k_item_prev = k_item |
384 |
1 |
return db_nested, k_item_prev, populated |
385 |
|
|
386 |
1 |
updated = False |
387 |
1 |
try: |
388 |
1 |
if update_dict: |
389 |
1 |
for dot_k, v in update_dict.items(): |
390 |
1 |
dict_to_update, key_to_update, _ = _iterate_keys(dot_k, db_item) |
391 |
1 |
dict_to_update[key_to_update] = v |
392 |
1 |
updated = True |
393 |
1 |
if unset: |
394 |
1 |
for dot_k in unset: |
395 |
1 |
try: |
396 |
1 |
dict_to_update, key_to_update, _ = _iterate_keys( |
397 |
|
dot_k, db_item, populate=False |
398 |
|
) |
399 |
1 |
del dict_to_update[key_to_update] |
400 |
1 |
updated = True |
401 |
1 |
except Exception as unset_error: |
402 |
1 |
self.logger.error(f"{unset_error} occured while updating DB.") |
403 |
1 |
if pull: |
404 |
1 |
for dot_k, v in pull.items(): |
405 |
1 |
try: |
406 |
1 |
dict_to_update, key_to_update, _ = _iterate_keys( |
407 |
|
dot_k, db_item, populate=False |
408 |
|
) |
409 |
1 |
except Exception as pull_error: |
410 |
1 |
self.logger.error(f"{pull_error} occured while updating DB.") |
411 |
1 |
continue |
412 |
|
|
413 |
1 |
if key_to_update not in dict_to_update: |
414 |
1 |
continue |
415 |
1 |
if not isinstance(dict_to_update[key_to_update], list): |
416 |
1 |
raise DbException( |
417 |
|
"Cannot pull '{}'. Target is not a list".format(dot_k) |
418 |
|
) |
419 |
1 |
while v in dict_to_update[key_to_update]: |
420 |
1 |
dict_to_update[key_to_update].remove(v) |
421 |
1 |
updated = True |
422 |
1 |
if pull_list: |
423 |
0 |
for dot_k, v in pull_list.items(): |
424 |
0 |
if not isinstance(v, list): |
425 |
0 |
raise DbException( |
426 |
|
"Invalid content at pull_list, '{}' must be an array".format( |
427 |
|
dot_k |
428 |
|
), |
429 |
|
http_code=HTTPStatus.BAD_REQUEST, |
430 |
|
) |
431 |
0 |
try: |
432 |
0 |
dict_to_update, key_to_update, _ = _iterate_keys( |
433 |
|
dot_k, db_item, populate=False |
434 |
|
) |
435 |
0 |
except Exception as iterate_error: |
436 |
0 |
self.logger.error( |
437 |
|
f"{iterate_error} occured while iterating keys in db update." |
438 |
|
) |
439 |
0 |
continue |
440 |
|
|
441 |
0 |
if key_to_update not in dict_to_update: |
442 |
0 |
continue |
443 |
0 |
if not isinstance(dict_to_update[key_to_update], list): |
444 |
0 |
raise DbException( |
445 |
|
"Cannot pull_list '{}'. Target is not a list".format(dot_k) |
446 |
|
) |
447 |
0 |
for single_v in v: |
448 |
0 |
while single_v in dict_to_update[key_to_update]: |
449 |
0 |
dict_to_update[key_to_update].remove(single_v) |
450 |
0 |
updated = True |
451 |
1 |
if push: |
452 |
1 |
for dot_k, v in push.items(): |
453 |
1 |
dict_to_update, key_to_update, populated = _iterate_keys( |
454 |
|
dot_k, db_item |
455 |
|
) |
456 |
1 |
if ( |
457 |
|
isinstance(dict_to_update, dict) |
458 |
|
and key_to_update not in dict_to_update |
459 |
|
): |
460 |
0 |
dict_to_update[key_to_update] = [v] |
461 |
0 |
updated = True |
462 |
1 |
elif populated and dict_to_update[key_to_update] is None: |
463 |
1 |
dict_to_update[key_to_update] = [v] |
464 |
1 |
updated = True |
465 |
1 |
elif not isinstance(dict_to_update[key_to_update], list): |
466 |
1 |
raise DbException( |
467 |
|
"Cannot push '{}'. Target is not a list".format(dot_k) |
468 |
|
) |
469 |
|
else: |
470 |
1 |
dict_to_update[key_to_update].append(v) |
471 |
1 |
updated = True |
472 |
1 |
if push_list: |
473 |
1 |
for dot_k, v in push_list.items(): |
474 |
1 |
if not isinstance(v, list): |
475 |
1 |
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 |
1 |
dict_to_update, key_to_update, populated = _iterate_keys( |
482 |
|
dot_k, db_item |
483 |
|
) |
484 |
1 |
if ( |
485 |
|
isinstance(dict_to_update, dict) |
486 |
|
and key_to_update not in dict_to_update |
487 |
|
): |
488 |
0 |
dict_to_update[key_to_update] = v.copy() |
489 |
0 |
updated = True |
490 |
1 |
elif populated and dict_to_update[key_to_update] is None: |
491 |
1 |
dict_to_update[key_to_update] = v.copy() |
492 |
1 |
updated = True |
493 |
1 |
elif not isinstance(dict_to_update[key_to_update], list): |
494 |
1 |
raise DbException( |
495 |
|
"Cannot push '{}'. Target is not a list".format(dot_k), |
496 |
|
http_code=HTTPStatus.CONFLICT, |
497 |
|
) |
498 |
|
else: |
499 |
1 |
dict_to_update[key_to_update] += v |
500 |
1 |
updated = True |
501 |
|
|
502 |
1 |
return updated |
503 |
1 |
except DbException: |
504 |
1 |
raise |
505 |
0 |
except Exception as e: # TODO refine |
506 |
0 |
raise DbException(str(e)) |
507 |
|
|
508 |
1 |
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 |
|
): |
520 |
|
""" |
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 |
531 |
|
:param pull_list: Same as pull but values are arrays where each item is removed from the array |
532 |
|
: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 |
534 |
|
:param push_list: Same as push but values are arrays where each item is and appended instead of appending the |
535 |
|
whole array |
536 |
|
:return: Dict with the number of entries modified. None if no matching is found. |
537 |
|
""" |
538 |
1 |
with self.lock: |
539 |
1 |
for i, db_item in self._find(table, self._format_filter(q_filter)): |
540 |
1 |
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 |
|
) |
549 |
1 |
return {"updated": 1 if updated else 0} |
550 |
|
else: |
551 |
0 |
if fail_on_empty: |
552 |
0 |
raise DbException( |
553 |
|
"Not found entry with _id='{}'".format(q_filter), |
554 |
|
HTTPStatus.NOT_FOUND, |
555 |
|
) |
556 |
0 |
return None |
557 |
|
|
558 |
1 |
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 |
|
): |
569 |
|
"""Modifies al matching entries at database. Same as push. Do not fail if nothing matches""" |
570 |
0 |
with self.lock: |
571 |
0 |
updated = 0 |
572 |
0 |
found = 0 |
573 |
0 |
for _, db_item in self._find(table, self._format_filter(q_filter)): |
574 |
0 |
found += 1 |
575 |
0 |
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 |
|
): |
584 |
0 |
updated += 1 |
585 |
|
# if not found and fail_on_empty: |
586 |
|
# raise DbException("Not found entry with '{}'".format(q_filter), HTTPStatus.NOT_FOUND) |
587 |
0 |
return {"updated": updated} if found else None |
588 |
|
|
589 |
1 |
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 |
|
""" |
599 |
0 |
try: |
600 |
0 |
with self.lock: |
601 |
0 |
for i, _ in self._find(table, self._format_filter({"_id": _id})): |
602 |
0 |
break |
603 |
|
else: |
604 |
0 |
if fail_on_empty: |
605 |
0 |
raise DbException( |
606 |
|
"Not found entry with _id='{}'".format(_id), |
607 |
|
HTTPStatus.NOT_FOUND, |
608 |
|
) |
609 |
0 |
return None |
610 |
0 |
self.db[table][i] = deepcopy(indata) |
611 |
0 |
return {"updated": 1} |
612 |
0 |
except DbException: |
613 |
0 |
raise |
614 |
0 |
except Exception as e: # TODO refine |
615 |
0 |
raise DbException(str(e)) |
616 |
|
|
617 |
1 |
def create(self, table, indata): |
618 |
|
""" |
619 |
|
Add a new entry at database |
620 |
|
:param table: collection or table |
621 |
|
:param indata: content to be added |
622 |
|
:return: database '_id' of the inserted element. Raises a DbException on error |
623 |
|
""" |
624 |
0 |
try: |
625 |
0 |
id = indata.get("_id") |
626 |
0 |
if not id: |
627 |
0 |
id = str(uuid4()) |
628 |
0 |
indata["_id"] = id |
629 |
0 |
with self.lock: |
630 |
0 |
if table not in self.db: |
631 |
0 |
self.db[table] = [] |
632 |
0 |
self.db[table].append(deepcopy(indata)) |
633 |
0 |
return id |
634 |
0 |
except Exception as e: # TODO refine |
635 |
0 |
raise DbException(str(e)) |
636 |
|
|
637 |
1 |
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 |
642 |
|
:return: list of inserted 'id's. Raises a DbException on error |
643 |
|
""" |
644 |
0 |
try: |
645 |
0 |
_ids = [] |
646 |
0 |
with self.lock: |
647 |
0 |
for indata in indata_list: |
648 |
0 |
_id = indata.get("_id") |
649 |
0 |
if not _id: |
650 |
0 |
_id = str(uuid4()) |
651 |
0 |
indata["_id"] = _id |
652 |
0 |
with self.lock: |
653 |
0 |
if table not in self.db: |
654 |
0 |
self.db[table] = [] |
655 |
0 |
self.db[table].append(deepcopy(indata)) |
656 |
0 |
_ids.append(_id) |
657 |
0 |
return _ids |
658 |
0 |
except Exception as e: # TODO refine |
659 |
0 |
raise DbException(str(e)) |
660 |
|
|
661 |
|
|
662 |
1 |
if __name__ == "__main__": |
663 |
|
# some test code |
664 |
0 |
db = DbMemory() |
665 |
0 |
db.create("test", {"_id": 1, "data": 1}) |
666 |
0 |
db.create("test", {"_id": 2, "data": 2}) |
667 |
0 |
db.create("test", {"_id": 3, "data": 3}) |
668 |
0 |
print("must be 3 items:", db.get_list("test")) |
669 |
0 |
print("must return item 2:", db.get_list("test", {"_id": 2})) |
670 |
0 |
db.del_one("test", {"_id": 2}) |
671 |
0 |
print("must be emtpy:", db.get_list("test", {"_id": 2})) |