Преглед на файлове

Move of the MongoDb Datasource in a plugin

Roland Haroutiounian преди 8 години
родител
ревизия
bf2940a3e3

+ 2
- 2
lodel/datasource/mongodb/utils.py Целия файл

@@ -39,7 +39,7 @@ MONGODB_SORT_OPERATORS_MAP = {
39 39
 }
40 40
 
41 41
 
42
-MANDATORY_CONNECTION_ARGS = ('host', 'port', 'login', 'password', 'dbname')
42
+MANDATORY_CONNECTION_ARGS = ('host', 'port', 'username', 'password', 'db_name')
43 43
 
44 44
 
45 45
 class MongoDbConnectionError(Exception):
@@ -71,7 +71,7 @@ def mongodbconnect(connection_name):
71 71
 # @return dict
72 72
 # @todo Use the settings module to store the connections parameters
73 73
 def get_connection_args(connnection_name='default'):
74
-    return {'host': 'localhost', 'port': 28015, 'login': 'lodel_admin', 'password': 'lapwd', 'dbname': 'lodel'}
74
+    return {'host': 'localhost', 'port': 28015, 'username': 'lodel_admin', 'password': 'lapwd', 'db_name': 'lodel'}
75 75
 
76 76
 
77 77
 ## @brief Checks the settings given a connection name

+ 16
- 0
plugins/mongodb_datasource/__init__.py Целия файл

@@ -0,0 +1,16 @@
1
+from lodel.settings.validator import SettingValidator
2
+
3
+
4
+__loader__ = "main.py"
5
+__confspec__ = "confspec.py"
6
+__author__ = "Lodel2 dev team"
7
+__fullname__ = "MongoDB plugin"
8
+
9
+
10
+## @brief Activates the plugin
11
+#
12
+# @note It is possible there to add some specific actions (like checks, etc ...) for the plugin
13
+#
14
+# @return bool|str : True if all the checks are OK, an error message if not
15
+def _activate():
16
+    return True

+ 13
- 0
plugins/mongodb_datasource/confspec.py Целия файл

@@ -0,0 +1,13 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+from lodel.settings.validator import SettingValidator
4
+
5
+CONFSPEC = {
6
+    'lodel2.datasource.mongodb_datasource.*':{
7
+        'host': ('localhost', SettingValidator('host')),
8
+        'port': (None, SettingValidator('string')),
9
+        'db_name':('lodel', SettingValidator('string')),
10
+        'username': (None, SettingValidator('string')),
11
+        'password': (None, SettingValidator('string'))
12
+    }
13
+}

+ 124
- 0
plugins/mongodb_datasource/main.py Целия файл

@@ -0,0 +1,124 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import bson
4
+from bson.son import SON
5
+from collections import OrderedDict
6
+import pymongo
7
+from pymongo.errors import BulkWriteError
8
+import urllib
9
+
10
+from .utils import mongodbconnect, object_collection_name, parse_query_filters, parse_query_order, MONGODB_SORT_OPERATORS_MAP
11
+
12
+class MongoDbDataSourceError(Exception):
13
+    pass
14
+
15
+
16
+class MongoDbDatasource(object):
17
+
18
+    ## @brief instanciates a database object given a connection name
19
+    # @param connection_name str
20
+    def __init__(self, connection_name):
21
+        self.database = mongodbconnect(connection_name)
22
+
23
+    ## @brief returns a selection of documents from the datasource
24
+    # @param target Emclass
25
+    # @param field_list list
26
+    # @param filters list : List of filters
27
+    # @param rel_filters list : List of relational filters
28
+    # @param order list : List of column to order. ex: order = [('title', 'ASC'),]
29
+    # @param group list : List of tupple representing the column used as "group by" fields. ex: group = [('title', 'ASC'),]
30
+    # @param limit int : Number of records to be returned
31
+    # @param offset int: used with limit to choose the start record
32
+    # @param instanciate bool : If true, the records are returned as instances, else they are returned as dict
33
+    # @return list
34
+    # @todo Implement the relations
35
+    def select(self, target, field_list, filters, rel_filters=None, order=None, group=None, limit=None, offset=0, instanciate=True):
36
+        collection_name = object_collection_name(target.__class__)
37
+        collection = self.database[collection_name]
38
+        query_filters = parse_query_filters(filters)
39
+        query_result_ordering = parse_query_order(order) if order is not None else None
40
+        results_field_list = None if len(field_list) == 0 else field_list
41
+        limit = limit if limit is not None else 0
42
+
43
+        if group is None:
44
+            cursor = collection.find(filter=query_filters, projection=results_field_list, skip=offset, limit=limit, sort=query_result_ordering)
45
+        else:
46
+            pipeline = list()
47
+            unwinding_list = list()
48
+            grouping_dict = OrderedDict()
49
+            sorting_list = list()
50
+            for group_param in group:
51
+                field_name = group_param[0]
52
+                field_sort_option = group_param[1]
53
+                sort_option = MONGODB_SORT_OPERATORS_MAP[field_sort_option]
54
+                unwinding_list.append({'$unwind': '$%s' % field_name})
55
+                grouping_dict[field_name] = '$%s' % field_name
56
+                sorting_list.append((field_name, sort_option))
57
+
58
+            sorting_list.extends(query_result_ordering)
59
+
60
+            pipeline.append({'$match': query_filters})
61
+            if results_field_list is not None:
62
+                pipeline.append({'$project': SON([{field_name: 1} for field_name in field_list])})
63
+            pipeline.extend(unwinding_list)
64
+            pipeline.append({'$group': grouping_dict})
65
+            pipeline.extend({'$sort': SON(sorting_list)})
66
+            if offset > 0:
67
+                pipeline.append({'$skip': offset})
68
+            if limit is not None:
69
+                pipeline.append({'$limit': limit})
70
+
71
+        results = list()
72
+        for document in cursor:
73
+            results.append(document)
74
+
75
+        return results
76
+
77
+    ## @brief Deletes one record defined by its uid
78
+    # @param target Emclass : class of the record to delete
79
+    # @param uid dict|list : a dictionary of fields and values composing the unique identifier of the record or a list of several dictionaries
80
+    # @return int : number of deleted records
81
+    # @TODO Implement the error management
82
+    def delete(self, target, uid):
83
+        if isinstance(uid, dict):
84
+            uid = [uid]
85
+        collection_name = object_collection_name(target.__class__)
86
+        collection = self.database[collection_name]
87
+        result = collection.delete_many(uid)
88
+        return result.deleted_count
89
+
90
+    ## @brief updates one or a list of records
91
+    # @param target Emclass : class of the object to insert
92
+    # @param uids list : list of uids to update
93
+    # @param datas dict : datas to update (new values)
94
+    # @return int : Number of updated records
95
+    # @todo check if the values need to be parsed
96
+    def update(self, target, uids, **datas):
97
+        if not isinstance(uids, list):
98
+            uids = [uids]
99
+        collection_name = object_collection_name(target.__class__)
100
+        collection = self.database[collection_name]
101
+        results = collection.update_many({'uid': {'$in': uids}}, datas)
102
+        return results.modified_count()
103
+
104
+    ## @brief Inserts a record in a given collection
105
+    # @param target Emclass : class of the object to insert
106
+    # @param datas dict : datas to insert
107
+    # @return bool
108
+    # @TODO Implement the error management
109
+    def insert(self, target, **datas):
110
+        collection_name = object_collection_name(target.__class__)
111
+        collection = self.database[collection_name]
112
+        result = collection.insert_one(datas)
113
+        return len(result.inserted_id)
114
+
115
+    ## @brief Inserts a list of records in a given collection
116
+    # @param target Emclass : class of the objects inserted
117
+    # @param datas_list
118
+    # @return list : list of the inserted records' ids
119
+    # @TODO Implement the error management
120
+    def insert_multi(self, target, datas_list):
121
+        collection_name = object_collection_name(target.__class__)
122
+        collection = self.database[collection_name]
123
+        result = collection.insert_many(datas_list)
124
+        return len(result.inserted_ids)

+ 147
- 0
plugins/mongodb_datasource/utils.py Целия файл

@@ -0,0 +1,147 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+import pymongo
4
+from pymongo import MongoClient
5
+
6
+from lodel.settings.settings import Settings as settings
7
+
8
+common_collections = {
9
+    'object': 'objects',
10
+    'relation': 'relation'
11
+}
12
+
13
+collection_prefix = {
14
+    'relation': 'rel_',
15
+    'object': 'class_'
16
+}
17
+
18
+LODEL_OPERATORS_MAP = {
19
+    '=': {'name': '$eq', 'value_type': None},
20
+    '<=': {'name': '$lte', 'value_type': None},
21
+    '>=': {'name': '$gte', 'value_type': None},
22
+    '!=': {'name': '$ne', 'value_type': None},
23
+    '<': {'name': '$lt', 'value_type': None},
24
+    '>': {'name': '$gt', 'value_type': None},
25
+    'in': {'name': '$in', 'value_type': list},
26
+    'not in': {'name': '$nin', 'value_type': list},
27
+    'OR': {'name': '$or', 'value_type': list},
28
+    'AND': {'name': '$and', 'value_type': list}
29
+}
30
+
31
+LODEL_SORT_OPERATORS_MAP = {
32
+    'ASC': pymongo.ASCENDING,
33
+    'DESC': pymongo.DESCENDING
34
+}
35
+
36
+MONGODB_SORT_OPERATORS_MAP = {
37
+    'ASC': 1,
38
+    'DESC': -1
39
+}
40
+
41
+MANDATORY_CONNECTION_ARGS = ('host', 'port', 'login', 'password', 'dbname')
42
+
43
+
44
+class MongoDbConnectionError(Exception):
45
+    pass
46
+
47
+
48
+## @brief gets the settings given a connection name
49
+# @param connection_name str
50
+# @return dict
51
+# @todo Use the settings module to store the connections parameters
52
+def get_connection_args(connnection_name='default'):
53
+    return {'host': 'localhost', 'port': 28015, 'login': 'lodel_admin', 'password': 'lapwd', 'dbname': 'lodel'}
54
+
55
+
56
+## @brief Creates a connection to a MongoDb Database
57
+# @param connection_name str
58
+# @return MongoClient
59
+def mongodbconnect(connection_name):
60
+    login, password, host, port, dbname = get_connection_args(connection_name)
61
+    connection_string = 'mongodb://%s:%s@%s:%s' % (login, password, host, port)
62
+    connection = MongoClient(connection_string)
63
+    database = connection[dbname]
64
+    return database
65
+
66
+
67
+## @brief Returns a collection name given a EmClass
68
+# @param class_object EmClass
69
+# @return str
70
+def object_collection_name(class_object):
71
+    if not class_object.pure_abstract:
72
+        class_parent = class_object.parents[0].uid
73
+        collection_name = ("%s%s" % (collection_prefix['object'], class_parent)).lower()
74
+    else:
75
+        collection_name = ("%s%s" % (collection_prefix['object'], class_object.name)).lower()
76
+
77
+    return collection_name
78
+
79
+
80
+## @brief Converts a Lodel query filter into a MongoDB filter
81
+# @param filter_params tuple: (FIELD, OPERATOR, VALUE) representing the query filter to convert
82
+# @return dict : {KEY:{OPERATOR:VALUE}}
83
+# @todo Add an error management for the operator mismatch
84
+def convert_filter(filter_params):
85
+    key, operator, value = filter_params
86
+
87
+    if operator == 'in' and not isinstance(value, list):
88
+            raise ValueError('A list should be used as value for an IN operator, %s given' % value.__class__)
89
+
90
+    if operator not in ('like', 'not like'):
91
+        converted_operator = LODEL_OPERATORS_MAP[operator]['name']
92
+        converted_filter = {key: {converted_operator: value}}
93
+    else:
94
+        is_starting_with = value.endswith('*')
95
+        is_ending_with = value.startswith('*')
96
+
97
+        if is_starting_with and not is is_ending_with:
98
+            regex_pattern = value.replace('*', '^')
99
+        elif is_ending_with and not is_starting_with:
100
+            regex_pattern = value.replace('*', '$')
101
+        elif is_starting_with and is_ending_with:
102
+            regex_pattern = '%s' % value
103
+        else:
104
+            regex_pattern = '^%s$' % value
105
+
106
+        regex_condition = {'$regex': regex_pattern, '$options': 'i'}
107
+        converted_filter = {key: regex_condition}
108
+        if operator.startswith('not'):
109
+            converted_filter = {key: {'$not': regex_condition}}
110
+
111
+    return converted_filter
112
+
113
+
114
+## @brief converts the query filters into MongoDB filters
115
+# @param query_filters list : list of query_filters as tuples or dicts
116
+# @param as_list bool : defines if the output will be a list (default: False)
117
+# @return dict|list
118
+def parse_query_filters(query_filters, as_list = False):
119
+    parsed_filters = dict() if not as_list else list()
120
+    for query_filter in query_filters:
121
+        if isinstance(query_filters, tuple):
122
+            if as_list:
123
+                parsed_filters.append(convert_filter(query_filter))
124
+            else:
125
+                parsed_filters.update(convert_filter(query_filter))
126
+        elif isinstance(query_filter, dict):
127
+            query_item = list(query_filter.items())[0]
128
+            key = LODEL_OPERATORS_MAP[query_item[0]]
129
+            if as_list:
130
+                parsed_filters.append({key: parse_query_filters(query_item[1], as_list=True)})
131
+            else:
132
+                parsed_filters.update({key: parse_query_filters(query_item[1], as_list=True)})
133
+        else:
134
+            # TODO add an exception management here in case the filter is neither a tuple nor a dict
135
+            pass
136
+    return parsed_filters
137
+
138
+
139
+## @brief Returns a list of orting options
140
+# @param query_filters_order list
141
+# @return list
142
+def parse_query_order(query_filters_order):
143
+    ordering = list()
144
+    for query_filter_order in query_filters_order:
145
+        field, direction = query_filter_order
146
+        ordering.append((field, LODEL_SORT_OPERATORS_MAP[direction]))
147
+    return ordering

Loading…
Отказ
Запис