1
0
Fork 0
mirror of https://github.com/yweber/lodel2.git synced 2025-11-13 01:19:16 +01:00
lodel2_mirror/plugins/mongodb_datasource/datasource.py
Yann c3137b658f Replaced imports by LodelContext.expose_modules() calls
Theorically all of those imports were tested by unit testing, but we've got no inssurance about that.
And even if unit tests had check for syntax errors, all pieces of code were not tested. We cannot
be sure that an import was missed or forgotten...
2016-11-02 17:03:22 +01:00

872 lines
38 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
import re
import warnings
import copy
import functools
from bson.son import SON
from collections import OrderedDict
import pymongo
from pymongo.errors import BulkWriteError
from lodel.context import LodelContext
LodelContext.expose_modules(globals(), {
'lodel.logger': 'logger',
'lodel.leapi.leobject': ['CLASS_ID_FIELDNAME'],
'lodel.leapi.datahandlers.base_classes': ['Reference', 'MultipleRef'],
'lodel.exceptions': ['LodelException', 'LodelFatalError'],
'lodel.plugin.datasource_plugin': ['AbstractDatasource']})
from . import utils
from .exceptions import *
from .utils import object_collection_name, collection_name, \
MONGODB_SORT_OPERATORS_MAP, connection_string, mongo_fieldname
##@brief Datasource class
#@ingroup plugin_mongodb_datasource
class MongoDbDatasource(AbstractDatasource):
##@brief Stores existing connections
#
#The key of this dict is a hash of the connection string + ro parameter.
#The value is a dict with 2 keys :
# - conn_count : the number of instanciated datasource that use this
#connection
# - db : the pymongo database object instance
_connections = dict()
##@brief Mapping from lodel2 operators to mongodb operator
lodel2mongo_op_map = {
'=':'$eq', '<=':'$lte', '>=':'$gte', '!=':'$ne', '<':'$lt',
'>':'$gt', 'in':'$in', 'not in':'$nin' }
##@brief List of mongodb operators that expect re as value
mongo_op_re = ['$in', '$nin']
wildcard_re = re.compile('[^\\\\]\*')
##@brief instanciates a database object given a connection name
#@param host str : hostname or IP
#@param port int : mongodb listening port
#@param db_name str
#@param username str
#@param password str
#@param read_only bool : If True the Datasource is for read only, else the
#Datasource is write only !
def __init__(self, host, port, db_name, username, password, read_only = False):
##@brief Connections infos that can be kept securly
self.__db_infos = {'host': host, 'port': port, 'db_name': db_name}
##@brief Is the instance read only ? (if not it's write only)
self.__read_only = bool(read_only)
##@brief Uniq ID for mongodb connection
self.__conn_hash= None
##@brief Stores the database cursor
self.database = self.__connect(
username, password, db_name, self.__read_only)
##@brief Destructor that attempt to close connection to DB
#
#Decrease the conn_count of associated MongoDbDatasource::_connections
#item. If it reach 0 close the connection to the db
#@see MongoDbDatasource::__connect()
def __del__(self):
self._connections[self.__conn_hash]['conn_count'] -= 1
if self._connections[self.__conn_hash]['conn_count'] <= 0:
self._connections[self.__conn_hash]['db'].close()
del(self._connections[self.__conn_hash])
logger.info("Closing connection to database")
##@brief Provide a new uniq numeric ID
#@param emcomp LeObject subclass (not instance) : To know on wich things we
#have to be uniq
#@warning multiple UID broken by this method
#@return an integer
def new_numeric_id(self, emcomp):
target = emcomp.uid_source()
tuid = target._uid[0] # Multiple UID broken here
results = self.select(
target, field_list = [tuid], filters = [],
order=[(tuid, 'DESC')], limit = 1)
if len(results) == 0:
return 1
return results[0][tuid]+1
##@brief returns a selection of documents from the datasource
#@param target Emclass
#@param field_list list
#@param filters list : List of filters
#@param relational_filters list : List of relational filters
#@param order list : List of column to order. ex: order =
#[('title', 'ASC'),]
#@param group list : List of tupple representing the column used as
#"group by" fields. ex: group = [('title', 'ASC'),]
#@param limit int : Number of records to be returned
#@param offset int: used with limit to choose the start record
#@return list
#@todo Implement group for abstract LeObject childs
def select(self, target, field_list, filters = None,
relational_filters=None, order=None, group=None, limit=None,
offset=0):
if target.is_abstract():
#Reccursiv calls for abstract LeObject child
results = self.__act_on_abstract(target, filters,
relational_filters, self.select, field_list = field_list,
order = order, group = group, limit = limit)
#Here we may implement the group
#If sorted query we have to sort again
if order is not None:
results = sorted(results,
key=functools.cmp_to_key(
self.__generate_lambda_cmp_order(order)))
#If limit given apply limit again
if offset > len(results):
results = list()
else:
if limit is not None:
if limit + offset > len(results):
limit = len(results)-offset-1
results = results[offset:offset+limit]
return results
# Default behavior
if filters is None:
filters = list()
if relational_filters is None:
relational_filters = list()
collection_name = object_collection_name(target)
collection = self.database[collection_name]
query_filters = self.__process_filters(
target, filters, relational_filters)
query_result_ordering = None
if order is not None:
query_result_ordering = utils.parse_query_order(order)
if group is None:
if field_list is None:
field_list = dict()
else:
f_list=dict()
for fl in field_list:
f_list[fl] = 1
field_list = f_list
field_list['_id'] = 0
cursor = collection.find(
spec = query_filters,
fields=field_list,
skip=offset,
limit=limit if limit != None else 0,
sort=query_result_ordering)
else:
pipeline = list()
unwinding_list = list()
grouping_dict = OrderedDict()
sorting_list = list()
for group_param in group:
field_name = group_param[0]
field_sort_option = group_param[1]
sort_option = MONGODB_SORT_OPERATORS_MAP[field_sort_option]
unwinding_list.append({'$unwind': '$%s' % field_name})
grouping_dict[field_name] = '$%s' % field_name
sorting_list.append((field_name, sort_option))
sorting_list.extends(query_result_ordering)
pipeline.append({'$match': query_filters})
if field_list is not None:
pipeline.append({
'$project': SON([{field_name: 1}
for field_name in field_list])})
pipeline.extend(unwinding_list)
pipeline.append({'$group': grouping_dict})
pipeline.extend({'$sort': SON(sorting_list)})
if offset > 0:
pipeline.append({'$skip': offset})
if limit is not None:
pipeline.append({'$limit': limit})
results = list()
for document in cursor:
results.append(document)
return results
##@brief Deletes records according to given filters
#@param target Emclass : class of the record to delete
#@param filters list : List of filters
#@param relational_filters list : List of relational filters
#@return int : number of deleted records
def delete(self, target, filters, relational_filters):
if target.is_abstract():
logger.debug("Delete called on %s filtered by (%s,%s). Target is \
abstract, preparing reccursiv calls" % (target, filters, relational_filters))
#Deletion with abstract LeObject as target (reccursiv calls)
return self.__act_on_abstract(target, filters,
relational_filters, self.delete)
logger.debug("Delete called on %s filtered by (%s,%s)." % (
target, filters, relational_filters))
#Non abstract beahavior
mongo_filters = self.__process_filters(
target, filters, relational_filters)
#Updating backref before deletion
self.__update_backref_filtered(target, filters, relational_filters,
None)
res = self.__collection(target).remove(mongo_filters)
return res['n']
##@brief updates records according to given filters
#@param target Emclass : class of the object to insert
#@param filters list : List of filters
#@param relational_filters list : List of relational filters
#@param upd_datas dict : datas to update (new values)
#@return int : Number of updated records
def update(self, target, filters, relational_filters, upd_datas):
self._data_cast(upd_datas)
#fetching current datas state
mongo_filters = self.__process_filters(
target, filters, relational_filters)
old_datas_l = self.__collection(target).find(
mongo_filters)
old_datas_l = list(old_datas_l)
#Running update
res = self.__update_no_backref(target, filters, relational_filters,
upd_datas)
#updating backref
self.__update_backref_filtered(target, filters, relational_filters,
upd_datas, old_datas_l)
return res
##@brief Designed to be called by backref update in order to avoid
#infinite updates between back references
#@see update()
def __update_no_backref(self, target, filters, relational_filters,
upd_datas):
logger.debug("Update called on %s filtered by (%s,%s) with datas \
%s" % (target, filters, relational_filters, upd_datas))
if target.is_abstract():
#Update using abstract LeObject as target (reccursiv calls)
return self.__act_on_abstract(target, filters,
relational_filters, self.update, upd_datas = upd_datas)
#Non abstract beahavior
mongo_filters = self.__process_filters(
target, filters, relational_filters)
self._data_cast(upd_datas)
mongo_arg = {'$set': upd_datas }
res = self.__collection(target).update(mongo_filters, mongo_arg)
return res['n']
## @brief Inserts a record in a given collection
# @param target Emclass : class of the object to insert
# @param new_datas dict : datas to insert
# @return the inserted uid
def insert(self, target, new_datas):
self._data_cast(new_datas)
logger.debug("Insert called on %s with datas : %s"% (
target, new_datas))
uidname = target.uid_fieldname()[0] #MULTIPLE UID BROKEN HERE
if uidname not in new_datas:
raise MongoDataSourceError("Missing UID data will inserting a new \
%s" % target.__class__)
res = self.__collection(target).insert(new_datas)
self.__update_backref(target, new_datas[uidname], None, new_datas)
return str(res)
## @brief Inserts a list of records in a given collection
# @param target Emclass : class of the objects inserted
# @param datas_list list : list of dict
# @return list : list of the inserted records' ids
def insert_multi(self, target, datas_list):
for datas in datas_list:
self._data_cast(datas)
res = self.__collection(target).insert_many(datas_list)
for new_datas in datas_list:
self.__update_backref(target, None, new_datas)
target.make_consistency(datas=new_datas)
return list(res.inserted_ids)
##@brief Update backref giving an action
#@param target leObject child class
#@param filters
#@param relational_filters,
#@param new_datas None | dict : optional new datas if None mean we are deleting
#@param old_datas_l None | list : if None fetch old datas from db (usefull
#when modifications are made on instance before updating backrefs)
#@return nothing (for the moment
def __update_backref_filtered(self, target,
filters, relational_filters, new_datas = None, old_datas_l = None):
#Getting all the UID of the object that will be deleted in order
#to update back_references
if old_datas_l is None:
mongo_filters = self.__process_filters(
target, filters, relational_filters)
old_datas_l = self.__collection(target).find(
mongo_filters)
old_datas_l = list(old_datas_l)
uidname = target.uid_fieldname()[0] #MULTIPLE UID BROKEN HERE
for old_datas in old_datas_l:
self.__update_backref(
target, old_datas[uidname], old_datas, new_datas)
##@brief Update back references of an object
#@ingroup plugin_mongodb_bref_op
#
#old_datas and new_datas arguments are set to None to indicate
#insertion or deletion. Calls examples :
#@par LeObject insert __update backref call
#<pre>
#Insert(datas):
# self.make_insert(datas)
# self.__update_backref(self.__class__, None, datas)
#</pre>
#@par LeObject delete __update backref call
#Delete()
# old_datas = self.datas()
# self.make_delete()
#  self.__update_backref(self.__class__, old_datas, None)
#@par LeObject update __update_backref call
#<pre>
#Update(new_datas):
# old_datas = self.datas()
# self.make_udpdate(new_datas)
#  self.__update_backref(self.__class__, old_datas, new_datas)
#</pre>
#
#@param target LeObject child classa
#@param tuid mixed : The target UID (the value that will be inserted in
#back references)
#@param old_datas dict : datas state before update
#@param new_datas dict : datas state after the update process
#retun None
def __update_backref(self, target, tuid, old_datas, new_datas):
#upd_dict is the dict that will allow to run updates in an optimized
#way (or try to help doing it)
#
#It's struct looks like :
# { LeoCLASS : {
# UID1: (
# LeoINSTANCE,
# { fname1 : value, fname2: value }),
# UID2 (LeoINSTANCE, {fname...}),
# },
# LeoClass2: {...
#
upd_dict = {}
for fname, fdh in target.reference_handlers().items():
oldd = old_datas is not None and fname in old_datas and \
(not hasattr(fdh, 'default') or old_datas[fname] != fdh.default) \
and not old_datas[fname] is None
newd = new_datas is not None and fname in new_datas and \
(not hasattr(fdh, 'default') or new_datas[fname] != fdh.default) \
and not new_datas[fname] is None
if (oldd and newd and old_datas[fname] == new_datas[fname])\
or not(oldd or newd):
#No changes or not concerned
continue
bref_cls = fdh.back_reference[0]
bref_fname = fdh.back_reference[1]
if issubclass(fdh.__class__, MultipleRef):
#fdh is a multiple ref. So the update preparation will be
#divided into two loops :
#- one loop for deleting old datas
#- one loop for inserting updated datas
#
#Preparing the list of values to delete or to add
if newd and oldd:
old_values = old_datas[fname]
new_values = new_datas[fname]
to_del = [ val
for val in old_values
if val not in new_values]
to_add = [ val
for val in new_values
if val not in old_values]
elif oldd and not newd:
to_del = old_datas[fname]
to_add = []
elif not oldd and newd:
to_del = []
to_add = new_datas[fname]
#Calling __back_ref_upd_one_value() with good arguments
for vtype, vlist in [('old',to_del), ('new', to_add)]:
for value in vlist:
#fetching backref infos
bref_infos = self.__bref_get_check(
bref_cls, value, bref_fname)
#preparing the upd_dict
upd_dict = self.__update_backref_upd_dict_prepare(
upd_dict, bref_infos, bref_fname, value)
#preparing updated bref_infos
bref_cls, bref_leo, bref_dh, bref_value = bref_infos
bref_infos = (bref_cls, bref_leo, bref_dh,
upd_dict[bref_cls][value][1][bref_fname])
vdict = {vtype: value}
#fetch and store updated value
new_bref_val = self.__back_ref_upd_one_value(
fname, fdh, tuid, bref_infos, **vdict)
upd_dict[bref_cls][value][1][bref_fname] = new_bref_val
else:
#fdh is a single ref so the process is simpler, we do not have
#to loop and we may do an update in only one
#__back_ref_upd_one_value() call by giving both old and new
#value
vdict = {}
if oldd:
vdict['old'] = old_datas[fname]
uid_val = vdict['old']
if newd:
vdict['new'] = new_datas[fname]
if not oldd:
uid_val = vdict['new']
#Fetching back ref infos
bref_infos = self.__bref_get_check(
bref_cls, uid_val, bref_fname)
#prepare the upd_dict
upd_dict = self.__update_backref_upd_dict_prepare(
upd_dict, bref_infos, bref_fname, uid_val)
#forging update bref_infos
bref_cls, bref_leo, bref_dh, bref_value = bref_infos
bref_infos = (bref_cls, bref_leo, bref_dh,
upd_dict[bref_cls][uid_val][1][bref_fname])
#fetche and store updated value
new_bref_val = self.__back_ref_upd_one_value(
fname, fdh, tuid, bref_infos, **vdict)
upd_dict[bref_cls][uid_val][1][bref_fname] = new_bref_val
#Now we've got our upd_dict ready.
#running the updates
for bref_cls, uid_dict in upd_dict.items():
for uidval, (leo, datas) in uid_dict.items():
#MULTIPLE UID BROKEN 2 LINES BELOW
self.__update_no_backref(
leo.__class__, [(leo.uid_fieldname()[0], '=', uidval)],
[], datas)
##@brief Utility function designed to handle the upd_dict of
#__update_backref()
#
#Basically checks if a key exists at some level, if not create it with
#the good default value (in most case dict())
#@param upd_dict dict : in & out args modified by reference
#@param bref_infos tuple : as returned by __bref_get_check()
#@param bref_fname str : name of the field in referenced class
#@param uid_val mixed : the UID of the referenced object
#@return the updated version of upd_dict
@staticmethod
def __update_backref_upd_dict_prepare(upd_dict,bref_infos, bref_fname,
uid_val):
bref_cls, bref_leo, bref_dh, bref_value = bref_infos
if bref_cls not in upd_dict:
upd_dict[bref_cls] = {}
if uid_val not in upd_dict[bref_cls]:
upd_dict[bref_cls][uid_val] = (bref_leo, {})
if bref_fname not in upd_dict[bref_cls][uid_val]:
upd_dict[bref_cls][uid_val][1][bref_fname] = bref_value
return upd_dict
##@brief Prepare a one value back reference update
#@param fname str : the source Reference field name
#@param fdh DataHandler : the source Reference DataHandler
#@param tuid mixed : the uid of the Leo that make reference to the backref
#@param bref_infos tuple : as returned by __bref_get_check() method
#@param old mixed : (optional **values) the old value
#@param new mixed : (optional **values) the new value
#@return the new back reference field value
def __back_ref_upd_one_value(self, fname, fdh, tuid, bref_infos, **values):
bref_cls, bref_leo, bref_dh, bref_val = bref_infos
oldd = 'old' in values
newdd = 'new' in values
if bref_val is None:
bref_val = bref_dh.empty()
if issubclass(bref_dh.__class__, MultipleRef):
if oldd and newdd:
if tuid not in bref_val:
raise MongoDbConsistencyError("The value we want to \
delete in this back reference update was not found in the back referenced \
object : %s. Value was : '%s'" % (bref_leo, tuid))
return bref_val
elif oldd and not newdd:
#deletion
old_value = values['old']
if tuid not in bref_val:
raise MongoDbConsistencyError("The value we want to \
delete in this back reference update was not found in the back referenced \
object : %s. Value was : '%s'" % (bref_leo, tuid))
if isinstance(bref_val, tuple):
bref_val = set(bref_val)
if isinstance(bref_val, set):
bref_val -= set([tuid])
else:
del(bref_val[bref_val.index(tuid)])
elif not oldd and newdd:
if tuid in bref_val:
raise MongoDbConsistencyError("The value we want to \
add in this back reference update was found in the back referenced \
object : %s. Value was : '%s'" % (bref_leo, tuid))
if isinstance(bref_val, tuple):
bref_val = set(bref_val)
if isinstance(bref_val, set):
bref_val |= set([tuid])
else:
bref_val.append(tuid)
else:
#Single value backref
if oldd and newdd:
if bref_val != tuid:
raise MongoDbConsistencyError("The backreference doesn't \
have expected value. Expected was %s but found %s in %s" % (
tuid, bref_val, bref_leo))
return bref_val
elif oldd and not newdd:
#deletion
if not hasattr(bref_dh, "default"):
raise MongoDbConsistencyError("Unable to delete a \
value for a back reference update. The concerned field don't have a default \
value : in %s field %s" % (bref_leo,fname))
bref_val = getattr(bref_dh, "default")
elif not oldd and newdd:
bref_val = tuid
return bref_val
##@brief Fetch back reference informations
#@warning thank's to __update_backref_act() this method is useless
#@param bref_cls LeObject child class : __back_reference[0]
#@param uidv mixed : UID value (the content of the reference field)
#@param bref_fname str : the name of the back_reference field
#@return tuple(bref_class, bref_LeObect_instance, bref_datahandler,
#bref_value)
#@throw MongoDbConsistencyError when LeObject instance not found given
#uidv
#@throw LodelFatalError if the back reference field is not a Reference
#subclass (major failure)
def __bref_get_check(self, bref_cls, uidv, bref_fname):
bref_leo = bref_cls.get_from_uid(uidv)
if bref_leo is None:
raise MongoDbConsistencyError("Unable to get the object we make \
reference to : %s with uid = %s" % (bref_cls, repr(uidv)))
bref_dh = bref_leo.data_handler(bref_fname)
if not isinstance(bref_dh, Reference):
raise LodelFatalError("Found a back reference field that \
is not a reference : '%s' field '%s'" % (bref_leo, bref_fname))
bref_val = bref_leo.data(bref_fname)
return (bref_leo.__class__, bref_leo, bref_dh, bref_val)
##@brief Act on abstract LeObject child
#
#This method is designed to be called by insert, select and delete method
#when they encounter an abtract class
#@param target LeObject child class
#@param filters
#@param relational_filters
#@param act function : the caller method
#@param **kwargs other arguments
#@return sum of results (if it's an array it will result in a concat)
#@todo optimization implementing a cache for __bref_get_check()
def __act_on_abstract(self,
target, filters, relational_filters, act, **kwargs):
result = list() if act == self.select else 0
if not target.is_abstract():
target_childs = target
else:
target_childs = [tc for tc in target.child_classes()
if not tc.is_abstract()]
for target_child in target_childs:
#Add target_child to filter
new_filters = copy.copy(filters)
for i in range(len(filters)):
fname, op, val = filters[i]
if fname == CLASS_ID_FIELDNAME:
logger.warning("Dirty drop of filter : '%s %s %s'" % (
fname, op, val))
del(new_filters[i])
new_filters.append(
(CLASS_ID_FIELDNAME, '=',
collection_name(target_child.__name__)))
result += act(
target = target_child,
filters = new_filters,
relational_filters = relational_filters,
**kwargs)
return result
##@brief Connect to database
#@note this method avoid opening two times the same connection using
#MongoDbDatasource::_connections static attribute
#@param username str
#@param password str
#@param ro bool : If True the Datasource is for read only, else the
def __connect(self, username, password, db_name, ro):
conn_string = connection_string(
username = username, password = password,
host = self.__db_infos['host'],
port = self.__db_infos['port'],
db_name = db_name,
ro = ro)
self.__conn_hash = conn_h = hash(conn_string)
if conn_h in self._connections:
self._connections[conn_h]['conn_count'] += 1
return self._connections[conn_h]['db'][self.__db_infos['db_name']]
else:
logger.info("Opening a new connection to database")
self._connections[conn_h] = {
'conn_count': 1,
'db': utils.connect(conn_string)}
return self._connections[conn_h]['db'][self.__db_infos['db_name']]
##@brief Return a pymongo collection given a LeObject child class
#@param leobject LeObject child class (no instance)
#return a pymongo.collection instance
def __collection(self, leobject):
return self.database[object_collection_name(leobject)]
##@brief Perform subqueries implies by relational filters and append the
# result to existing filters
#
#The processing is divided in multiple steps :
# - determine (for each relational field of the target) every collection
#that are involved
# - generate subqueries for relational_filters that concerns a different
#collection than target collection
#filters
# - execute subqueries
# - transform subqueries results in filters
# - merge subqueries generated filters with existing filters
#
#@param target LeObject subclass (no instance) : Target class
#@param filters list : List of tuple(FIELDNAME, OP, VALUE)
#@param relational_filters : same composition thant filters except that
# FIELD is represented by a tuple(FIELDNAME, {CLASS1:RFIELD1,
# CLASS2:RFIELD2})
#@return a list of pymongo filters ( dict {FIELD:{OPERATOR:VALUE}} )
def __process_filters(self,target, filters, relational_filters):
# Simple filters lodel2 -> pymongo converting
res = self.__filters2mongo(filters, target)
rfilters = self.__prepare_relational_filters(target, relational_filters)
#Now that everything is well organized, begin to forge subquerie
#filters
self.__subqueries_from_relational_filters(target, rfilters)
# Executing subqueries, creating filters from result, and injecting
# them in original filters of the query
if len(rfilters) > 0:
logger.debug("Begining subquery execution")
for fname in rfilters:
if fname not in res:
res[fname] = dict()
subq_results = set()
for leobject, sq_filters in rfilters[fname].items():
uid_fname = mongo_fieldname(leobject._uid)
log_msg = "Subquery running on collection {coll} with filters \
'{filters}'"
logger.debug(log_msg.format(
coll=object_collection_name(leobject),
filters=sq_filters))
cursor = self.__collection(leobject).find(
filter=sq_filters,
projection=uid_fname)
subq_results |= set(doc[uid_fname] for doc in cursor)
#generating new filter from result
if '$in' in res[fname]:
#WARNING we allready have a IN on this field, doing dedup
#from result
deduped = set(res[fname]['$in']) & subq_results
if len(deduped) == 0:
del(res[fname]['$in'])
else:
res[fname]['$in'] = list(deduped)
else:
res[fname]['$in'] = list(subq_results)
if len(rfilters) > 0:
logger.debug("End of subquery execution")
return res
##@brief Generate subqueries from rfilters tree
#
#Returned struct organization :
# - 1st level keys : relational field name of target
# - 2nd level keys : referenced leobject
# - 3th level values : pymongo filters (dict)
#
#@note The only caller of this method is __process_filters
#@warning No return value, the rfilters arguement is modified by
#reference
#
#@param target LeObject subclass (no instance) : Target class
#@param rfilters dict : A struct as returned by
#MongoDbDatasource.__prepare_relational_filters()
#@return None, the rfilters argument is modified by reference
@classmethod
def __subqueries_from_relational_filters(cls, target, rfilters):
for fname in rfilters:
for leobject in rfilters[fname]:
for rfield in rfilters[fname][leobject]:
#This way of doing is not optimized but allows to trigger
#warnings in some case (2 different values for a same op
#on a same field on a same collection)
mongofilters = cls.__op_value_listconv(
rfilters[fname][leobject][rfield], target.field(fname))
rfilters[fname][leobject][rfield] = mongofilters
##@brief Generate a tree from relational_filters
#
#The generated struct is a dict with :
# - 1st level keys : relational field name of target
# - 2nd level keys : referenced leobject
# - 3th level keys : referenced field in referenced class
# - 4th level values : list of tuple(op, value)
#
#@note The only caller of this method is __process_filters
#@warning An assertion is done : if two leobject are stored in the same
#collection they share the same uid
#
#@param target LeObject subclass (no instance) : Target class
#@param relational_filters : same composition thant filters except that
#@return a struct as described above
@classmethod
def __prepare_relational_filters(cls, target, relational_filters):
# We are going to regroup relationnal filters by reference field
# then by collection
rfilters = dict()
if relational_filters is None:
relational_filters = []
for (fname, rfields), op, value in relational_filters:
if fname not in rfilters:
rfilters[fname] = dict()
rfilters[fname] = dict()
# Stores the representative leobject for associated to a collection
# name
leo_collname = dict()
# WARNING ! Here we assert that all leobject that are stored
# in a same collection are identified by the same field
for leobject, rfield in rfields.items():
#here we are filling a dict with leobject as index but
#we are doing a UNIQ on collection name
cur_collname = object_collection_name(leobject)
if cur_collname not in leo_collname:
leo_collname[cur_collname] = leobject
rfilters[fname][leobject] = dict()
#Fecthing the collection's representative leobject
repr_leo = leo_collname[cur_collname]
if rfield not in rfilters[fname][repr_leo]:
rfilters[fname][repr_leo][rfield] = list()
rfilters[fname][repr_leo][rfield].append((op, value))
return rfilters
##@brief Convert lodel2 filters to pymongo conditions
#@param filters list : list of lodel filters
#@return dict representing pymongo conditions
@classmethod
def __filters2mongo(cls, filters, target):
res = dict()
eq_fieldname = [] #Stores field with equal comparison OP
for fieldname, op, value in filters:
oop = op
ovalue = value
op, value = cls.__op_value_conv(op, value, target.field(fieldname))
if op == '=':
eq_fieldname.append(fieldname)
if fieldname in res:
logger.warning("Dropping previous condition. Overwritten \
by an equality filter")
res[fieldname] = value
continue
if fieldname in eq_fieldname:
logger.warning("Dropping condition : '%s %s %s'" % (
fieldname, op, value))
continue
if fieldname not in res:
res[fieldname] = dict()
if op in res[fieldname]:
logger.warning("Dropping condition : '%s %s %s'" % (
fieldname, op, value))
else:
if op not in cls.lodel2mongo_op_map:
raise ValueError("Invalid operator : '%s'" % op)
new_op = cls.lodel2mongo_op_map[op]
res[fieldname][new_op] = value
return res
##@brief Convert lodel2 operator and value to pymongo struct
#
#Convertion is done using MongoDbDatasource::lodel2mongo_op_map
#@param op str : take value in LeFilteredQuery::_query_operators
#@param value mixed : the value
#@return a tuple(mongo_op, mongo_value)
@classmethod
def __op_value_conv(cls, op, value, dhdl):
if op not in cls.lodel2mongo_op_map:
msg = "Invalid operator '%s' found" % op
raise MongoDbDataSourceError(msg)
mongop = cls.lodel2mongo_op_map[op]
mongoval = value
#Converting lodel2 wildcarded string into a case insensitive
#mongodb re
if mongop in cls.mongo_op_re:
if value.startswith('(') and value.endswith(')') and ',' in value:
if (dhdl.cast_type is not None):
mongoval = [ dhdl.cast_type(item) for item in mongoval[1:-1].split(',') ]
else:
mongoval = [ item for item in mongoval[1:-1].split(',') ]
elif mongop == 'like':
#unescaping \
mongoval = value.replace('\\\\','\\')
if not mongoval.startswith('*'):
mongoval = '^'+mongoval
#For the end of the string it's harder to detect escaped *
if not (mongoval[-1] == '*' and mongoval[-2] != '\\'):
mongoval += '$'
#Replacing every other unescaped wildcard char
mongoval = cls.wildcard_re.sub('.*', mongoval)
mongoval = {'$regex': mongoval, '$options': 'i'}
return (op, mongoval)
##@brief Convert a list of tuple(OP, VALUE) into a pymongo filter dict
#@return a dict with mongo op as key and value as value...
@classmethod
def __op_value_listconv(cls, op_value_list, dhdl):
result = dict()
for op, value in op_value_list:
mongop, mongoval = cls.__op_value_conv(op, value, dhdl)
if mongop in result:
warnings.warn("Duplicated value given for a single \
field/operator couple in a query. We will keep only the first one")
else:
result[mongop] = mongoval
return result
##@brief Generate a comparison function for post reccursion sorting in
#select
#@return a lambda function that take 2 dict as arguement
@classmethod
def __generate_lambda_cmp_order(cls, order):
if len(order) == 0:
return lambda a,b: 0
glco = cls.__generate_lambda_cmp_order
fname, cmpdir = order[0]
order = order[1:]
return lambda a,b: glco(order) if a[fname] == b[fname] else (\
1 if (a[fname]>b[fname] if cmpdir == 'ASC' else a[fname]<b[fname])\
else -1)
##@brief Correct some datas before giving them to pymongo
#
#For example sets has to be casted to lise
#@param datas
#@return datas
@classmethod
def _data_cast(cls, datas):
for dname in datas:
if isinstance(datas[dname], set):
#pymongo raises :
#bson.errors.InvalidDocument: Cannot encode object: {...}
#with sets
datas[dname] = list(datas[dname])
return datas