3 from pymongo
import MongoClient
, errors
4 from osm_common
.dbbase
import DbException
, DbBase
5 from http
import HTTPStatus
6 from time
import time
, sleep
7 from copy
import deepcopy
9 __author__
= "Alfonso Tierno <alfonso.tiernosepulveda@telefonica.com>"
11 # TODO consider use this decorator for database access retries
13 # def retry_mongocall(call):
14 # def _retry_mongocall(*args, **kwargs):
18 # return call(*args, **kwargs)
19 # except pymongo.AutoReconnect as e:
21 # raise DbException(str(e))
23 # return _retry_mongocall
26 def deep_update(to_update
, update_with
):
28 Update 'to_update' dict with the content 'update_with' dict recursively
29 :param to_update: must be a dictionary to be modified
30 :param update_with: must be a dictionary. It is not changed
33 for key
in update_with
:
35 if isinstance(to_update
[key
], dict) and isinstance(update_with
[key
], dict):
36 deep_update(to_update
[key
], update_with
[key
])
38 to_update
[key
] = deepcopy(update_with
[key
])
42 class DbMongo(DbBase
):
43 conn_initial_timout
= 120
46 def __init__(self
, logger_name
='db'):
47 self
.logger
= logging
.getLogger(logger_name
)
49 def db_connect(self
, config
):
51 if "logger_name" in config
:
52 self
.logger
= logging
.getLogger(config
["logger_name"])
53 self
.client
= MongoClient(config
["host"], config
["port"])
54 self
.db
= self
.client
[config
["name"]]
55 if "loglevel" in config
:
56 self
.logger
.setLevel(getattr(logging
, config
['loglevel']))
57 # get data to try a connection
61 self
.db
.users
.find_one({"username": "admin"})
63 except errors
.ConnectionFailure
as e
:
64 if time() - now
>= self
.conn_initial_timout
:
66 self
.logger
.info("Waiting to database up {}".format(e
))
68 except errors
.PyMongoError
as e
:
69 raise DbException(str(e
))
71 def db_disconnect(self
):
75 def _format_filter(q_filter
):
77 Translate query string filter into mongo database filter
78 :param q_filter: Query string content. Follows SOL005 section 4.3.2 guidelines, with the follow extensions and
80 It accept ".nq" (not equal) in addition to ".neq".
81 For arrays you can specify index (concrete index must match), nothing (any index may match) or 'ANYINDEX'
82 (two or more matches applies for the same array element). Examples:
83 with database register: {A: [{B: 1, C: 2}, {B: 6, C: 9}]}
84 query 'A.B=6' matches because array A contains one element with B equal to 6
85 query 'A.0.B=6' does no match because index 0 of array A contains B with value 1, but not 6
86 query 'A.B=6&A.C=2' matches because one element of array matches B=6 and other matchesC=2
87 query 'A.ANYINDEX.B=6&A.ANYINDEX.C=2' does not match because it is needed the same element of the
90 Examples of translations from SOL005 to >> mongo # comment
91 A=B; A.eq=B >> A: B # must contain key A and equal to B or be a list that contains B
93 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
95 A.cont=B&A.cont=C; A.cont=B,C >> A: {$in: [B, C]}
96 A.ncont=B >> A: {$nin: B} # must not contain key A or if present not equal to B or if a list,
97 # it must not not contain B
98 A.ncont=B,C; A.ncont=B&A.ncont=C >> A: {$nin: [B,C]} # must not contain key A or if present not equal
99 # neither B nor C; or if a list, it must not contain neither B nor C
100 A.ne=B&A.ne=C; A.ne=B,C >> A: {$nin: [B, C]}
101 A.gt=B >> A: {$gt: B} # must contain key A and greater than B
102 A.ne=B; A.neq=B >> A: {$ne: B} # must not contain key A or if present not equal to B, or if
103 # an array not contain B
104 A.ANYINDEX.B=C >> A: {$elemMatch: {B=C}
105 :return: database mongo filter
109 for query_k
, query_v
in q_filter
.items():
110 dot_index
= query_k
.rfind(".")
111 if dot_index
> 1 and query_k
[dot_index
+1:] in ("eq", "ne", "gt", "gte", "lt", "lte", "cont",
113 operator
= "$" + query_k
[dot_index
+ 1:]
114 if operator
== "$neq":
116 k
= query_k
[:dot_index
]
122 if isinstance(v
, list):
123 if operator
in ("$eq", "$cont"):
126 elif operator
in ("$ne", "$ncont"):
130 v
= query_v
.join(",")
132 if operator
in ("$eq", "$cont"):
133 # v cannot be a comma separated list, because operator would have been changed to $in
135 elif operator
== "$ncount":
136 # v cannot be a comma separated list, because operator would have been changed to $nin
141 # process the ANYINDEX word at k.
142 kleft
, _
, kright
= k
.rpartition(".ANYINDEX.")
145 db_v
= {"$elemMatch": {kright
: db_v
}}
146 kleft
, _
, kright
= k
.rpartition(".ANYINDEX.")
148 # insert in db_filter
149 # maybe db_filter[k] exist. e.g. in the query string for values between 5 and 8: "a.gt=5&a.lt=8"
150 deep_update(db_filter
, {k
: db_v
})
153 except Exception as e
:
154 raise DbException("Invalid query string filter at {}:{}. Error: {}".format(query_k
, v
, e
),
155 http_code
=HTTPStatus
.BAD_REQUEST
)
157 def get_list(self
, table
, filter={}):
160 collection
= self
.db
[table
]
161 db_filter
= self
._format
_filter
(filter)
162 rows
= collection
.find(db_filter
)
168 except Exception as e
: # TODO refine
169 raise DbException(str(e
))
171 def get_one(self
, table
, filter={}, fail_on_empty
=True, fail_on_more
=True):
174 filter = self
._format
_filter
(filter)
175 collection
= self
.db
[table
]
176 if not (fail_on_empty
and fail_on_more
):
177 return collection
.find_one(filter)
178 rows
= collection
.find(filter)
179 if rows
.count() == 0:
181 raise DbException("Not found any {} with filter='{}'".format(table
[:-1], filter),
182 HTTPStatus
.NOT_FOUND
)
184 elif rows
.count() > 1:
186 raise DbException("Found more than one {} with filter='{}'".format(table
[:-1], filter),
189 except Exception as e
: # TODO refine
190 raise DbException(str(e
))
192 def del_list(self
, table
, filter={}):
194 collection
= self
.db
[table
]
195 rows
= collection
.delete_many(self
._format
_filter
(filter))
196 return {"deleted": rows
.deleted_count
}
199 except Exception as e
: # TODO refine
200 raise DbException(str(e
))
202 def del_one(self
, table
, filter={}, fail_on_empty
=True):
204 collection
= self
.db
[table
]
205 rows
= collection
.delete_one(self
._format
_filter
(filter))
206 if rows
.deleted_count
== 0:
208 raise DbException("Not found any {} with filter='{}'".format(table
[:-1], filter),
209 HTTPStatus
.NOT_FOUND
)
211 return {"deleted": rows
.deleted_count
}
212 except Exception as e
: # TODO refine
213 raise DbException(str(e
))
215 def create(self
, table
, indata
):
217 collection
= self
.db
[table
]
218 data
= collection
.insert_one(indata
)
219 return data
.inserted_id
220 except Exception as e
: # TODO refine
221 raise DbException(str(e
))
223 def set_one(self
, table
, filter, update_dict
, fail_on_empty
=True):
225 collection
= self
.db
[table
]
226 rows
= collection
.update_one(self
._format
_filter
(filter), {"$set": update_dict
})
227 if rows
.matched_count
== 0:
229 raise DbException("Not found any {} with filter='{}'".format(table
[:-1], filter),
230 HTTPStatus
.NOT_FOUND
)
232 return {"modified": rows
.modified_count
}
233 except Exception as e
: # TODO refine
234 raise DbException(str(e
))
236 def replace(self
, table
, id, indata
, fail_on_empty
=True):
238 _filter
= {"_id": id}
239 collection
= self
.db
[table
]
240 rows
= collection
.replace_one(_filter
, indata
)
241 if rows
.matched_count
== 0:
243 raise DbException("Not found any {} with filter='{}'".format(table
[:-1], _filter
),
244 HTTPStatus
.NOT_FOUND
)
246 return {"replaced": rows
.modified_count
}
247 except Exception as e
: # TODO refine
248 raise DbException(str(e
))