1 # -*- coding: utf-8 -*-
3 # Copyright 2018 Telefonica S.A.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
19 from osm_common
.dbbase
import DbException
, DbBase
20 from osm_common
.dbmongo
import deep_update
21 from http
import HTTPStatus
22 from uuid
import uuid4
23 from copy
import deepcopy
25 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
28 class DbMemory(DbBase
):
30 def __init__(self
, logger_name
='db', lock
=False):
31 super().__init
__(logger_name
, lock
)
34 def db_connect(self
, config
):
37 :param config: Configuration of database
38 :return: None or raises DbException on error
40 if "logger_name" in config
:
41 self
.logger
= logging
.getLogger(config
["logger_name"])
42 master_key
= config
.get("commonkey") or config
.get("masterpassword")
44 self
.set_secret_key(master_key
)
47 def _format_filter(q_filter
):
49 # split keys with ANYINDEX in this way:
50 # {"A.B.ANYINDEX.C.D.ANYINDEX.E": v } -> {"A.B.ANYINDEX": {"C.D.ANYINDEX": {"E": v}}}
52 for k
, v
in q_filter
.items():
54 kleft
, _
, kright
= k
.rpartition(".ANYINDEX.")
56 k
= kleft
+ ".ANYINDEX"
58 kleft
, _
, kright
= k
.rpartition(".ANYINDEX.")
59 deep_update(db_filter
, {k
: db_v
})
63 def _find(self
, table
, q_filter
):
65 def recursive_find(key_list
, key_next_index
, content
, oper
, target
):
66 if key_next_index
== len(key_list
) or content
is None:
68 if oper
in ("eq", "cont"):
69 if isinstance(target
, list):
70 if isinstance(content
, list):
71 return any(content_item
in target
for content_item
in content
)
72 return content
in target
73 elif isinstance(content
, list):
74 return target
in content
76 return content
== target
77 elif oper
in ("neq", "ne", "ncont"):
78 if isinstance(target
, list):
79 if isinstance(content
, list):
80 return all(content_item
not in target
for content_item
in content
)
81 return content
not in target
82 elif isinstance(content
, list):
83 return target
not in content
85 return content
!= target
87 return content
> target
89 return content
>= target
91 return content
< target
93 return content
<= target
95 raise DbException("Unknown filter operator '{}' in key '{}'".
96 format(oper
, ".".join(key_list
)), http_code
=HTTPStatus
.BAD_REQUEST
)
100 elif isinstance(content
, dict):
101 return recursive_find(key_list
, key_next_index
+ 1, content
.get(key_list
[key_next_index
]), oper
,
103 elif isinstance(content
, list):
104 look_for_match
= True # when there is a match return immediately
105 if (target
is None) != (oper
in ("neq", "ne", "ncont")): # one True and other False (Xor)
106 look_for_match
= False # when there is not a match return immediately
108 for content_item
in content
:
109 if key_list
[key_next_index
] == "ANYINDEX" and isinstance(v
, dict):
111 for k2
, v2
in target
.items():
112 k_new_list
= k2
.split(".")
114 if k_new_list
[-1] in ("eq", "ne", "gt", "gte", "lt", "lte", "cont", "ncont", "neq"):
115 new_operator
= k_new_list
.pop()
116 if not recursive_find(k_new_list
, 0, content_item
, new_operator
, v2
):
121 matches
= recursive_find(key_list
, key_next_index
, content_item
, oper
, target
)
122 if matches
== look_for_match
:
124 if key_list
[key_next_index
].isdecimal() and int(key_list
[key_next_index
]) < len(content
):
125 matches
= recursive_find(key_list
, key_next_index
+ 1, content
[int(key_list
[key_next_index
])],
127 if matches
== look_for_match
:
129 return not look_for_match
130 else: # content is not dict, nor list neither None, so not found
131 if oper
in ("neq", "ne", "ncont"):
132 return target
is not None
134 return target
is None
136 for i
, row
in enumerate(self
.db
.get(table
, ())):
137 q_filter
= q_filter
or {}
138 for k
, v
in q_filter
.items():
139 k_list
= k
.split(".")
141 if k_list
[-1] in ("eq", "ne", "gt", "gte", "lt", "lte", "cont", "ncont", "neq"):
142 operator
= k_list
.pop()
143 matches
= recursive_find(k_list
, 0, row
, operator
, v
)
150 def get_list(self
, table
, q_filter
=None):
152 Obtain a list of entries matching q_filter
153 :param table: collection or table
154 :param q_filter: Filter
155 :return: a list (can be empty) with the found entries. Raises DbException on error
160 for _
, row
in self
._find
(table
, self
._format
_filter
(q_filter
)):
161 result
.append(deepcopy(row
))
165 except Exception as e
: # TODO refine
166 raise DbException(str(e
))
168 def count(self
, table
, q_filter
=None):
170 Count the number of entries matching q_filter
171 :param table: collection or table
172 :param q_filter: Filter
173 :return: number of entries found (can be zero)
174 :raise: DbException on error
178 return sum(1 for x
in self
._find
(table
, self
._format
_filter
(q_filter
)))
181 except Exception as e
: # TODO refine
182 raise DbException(str(e
))
184 def get_one(self
, table
, q_filter
=None, fail_on_empty
=True, fail_on_more
=True):
186 Obtain one entry matching q_filter
187 :param table: collection or table
188 :param q_filter: Filter
189 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
190 it raises a DbException
191 :param fail_on_more: If more than one matches filter it returns one of then unless this flag is set tu True, so
192 that it raises a DbException
193 :return: The requested element, or None
198 for _
, row
in self
._find
(table
, self
._format
_filter
(q_filter
)):
202 raise DbException("Found more than one entry with filter='{}'".format(q_filter
),
203 HTTPStatus
.CONFLICT
.value
)
205 if not result
and fail_on_empty
:
206 raise DbException("Not found entry with filter='{}'".format(q_filter
), HTTPStatus
.NOT_FOUND
)
207 return deepcopy(result
)
208 except Exception as e
: # TODO refine
209 raise DbException(str(e
))
211 def del_list(self
, table
, q_filter
=None):
213 Deletes all entries that match q_filter
214 :param table: collection or table
215 :param q_filter: Filter
216 :return: Dict with the number of entries deleted
221 for i
, _
in self
._find
(table
, self
._format
_filter
(q_filter
)):
223 deleted
= len(id_list
)
224 for i
in reversed(id_list
):
225 del self
.db
[table
][i
]
226 return {"deleted": deleted
}
229 except Exception as e
: # TODO refine
230 raise DbException(str(e
))
232 def del_one(self
, table
, q_filter
=None, fail_on_empty
=True):
234 Deletes one entry that matches q_filter
235 :param table: collection or table
236 :param q_filter: Filter
237 :param fail_on_empty: If nothing matches filter it returns '0' deleted unless this flag is set tu True, in
238 which case it raises a DbException
239 :return: Dict with the number of entries deleted
243 for i
, _
in self
._find
(table
, self
._format
_filter
(q_filter
)):
247 raise DbException("Not found entry with filter='{}'".format(q_filter
), HTTPStatus
.NOT_FOUND
)
249 del self
.db
[table
][i
]
250 return {"deleted": 1}
251 except Exception as e
: # TODO refine
252 raise DbException(str(e
))
254 def set_one(self
, table
, q_filter
, update_dict
, fail_on_empty
=True, unset
=None, pull
=None, push
=None):
256 Modifies an entry at database
257 :param table: collection or table
258 :param q_filter: Filter
259 :param update_dict: Plain dictionary with the content to be updated. It is a dot separated keys and a value
260 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
261 it raises a DbException
262 :param unset: Plain dictionary with the content to be removed if exist. It is a dot separated keys, value is
263 ignored. If not exist, it is ignored
264 :param pull: Plain dictionary with the content to be removed from an array. It is a dot separated keys and value
265 if exist in the array is removed. If not exist, it is ignored
266 :param push: Plain dictionary with the content to be appended to an array. It is a dot separated keys and value
267 is appended to the end of the array
268 :return: Dict with the number of entries modified. None if no matching is found.
272 for i
, db_item
in self
._find
(table
, self
._format
_filter
(q_filter
)):
276 raise DbException("Not found entry with _id='{}'".format(q_filter
), HTTPStatus
.NOT_FOUND
)
278 for k
, v
in update_dict
.items():
280 k_list
= k
.split(".")
281 k_item_prev
= k_list
[0]
282 for k_item
in k_list
[1:]:
283 if isinstance(db_nested
[k_item_prev
], dict):
284 if k_item
not in db_nested
[k_item_prev
]:
285 db_nested
[k_item_prev
][k_item
] = None
286 elif isinstance(db_nested
[k_item_prev
], list) and k_item
.isdigit():
287 # extend list with Nones if index greater than list
289 if k_item
>= len(db_nested
[k_item_prev
]):
290 db_nested
[k_item_prev
] += [None] * (k_item
- len(db_nested
[k_item_prev
]) + 1)
291 elif db_nested
[k_item_prev
] is None:
292 db_nested
[k_item_prev
] = {k_item
: None}
293 else: # number, string, boolean, ... or list but with not integer key
294 raise DbException("Cannot set '{}' on existing '{}={}'".format(k
, k_item_prev
,
295 db_nested
[k_item_prev
]))
297 db_nested
= db_nested
[k_item_prev
]
300 db_nested
[k_item_prev
] = v
301 return {"updated": 1}
304 except Exception as e
: # TODO refine
305 raise DbException(str(e
))
307 def replace(self
, table
, _id
, indata
, fail_on_empty
=True):
309 Replace the content of an entry
310 :param table: collection or table
311 :param _id: internal database id
312 :param indata: content to replace
313 :param fail_on_empty: If nothing matches filter it returns None unless this flag is set tu True, in which case
314 it raises a DbException
315 :return: Dict with the number of entries replaced
319 for i
, _
in self
._find
(table
, self
._format
_filter
({"_id": _id
})):
323 raise DbException("Not found entry with _id='{}'".format(_id
), HTTPStatus
.NOT_FOUND
)
325 self
.db
[table
][i
] = deepcopy(indata
)
326 return {"updated": 1}
329 except Exception as e
: # TODO refine
330 raise DbException(str(e
))
332 def create(self
, table
, indata
):
334 Add a new entry at database
335 :param table: collection or table
336 :param indata: content to be added
337 :return: database id of the inserted element. Raises a DbException on error
340 id = indata
.get("_id")
345 if table
not in self
.db
:
347 self
.db
[table
].append(deepcopy(indata
))
349 except Exception as e
: # TODO refine
350 raise DbException(str(e
))
352 def create_list(self
, table
, indata_list
):
354 Add a new entry at database
355 :param table: collection or table
356 :param indata_list: list content to be added
357 :return: database ids of the inserted element. Raises a DbException on error
362 for indata
in indata_list
:
363 _id
= indata
.get("_id")
368 if table
not in self
.db
:
370 self
.db
[table
].append(deepcopy(indata
))
373 except Exception as e
: # TODO refine
374 raise DbException(str(e
))
377 if __name__
== '__main__':
380 db
.create("test", {"_id": 1, "data": 1})
381 db
.create("test", {"_id": 2, "data": 2})
382 db
.create("test", {"_id": 3, "data": 3})
383 print("must be 3 items:", db
.get_list("test"))
384 print("must return item 2:", db
.get_list("test", {"_id": 2}))
385 db
.del_one("test", {"_id": 2})
386 print("must be emtpy:", db
.get_list("test", {"_id": 2}))