backend.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """Event Man(ager) database backend
  2. Classes and functions used to manage events and attendees database.
  3. Copyright 2015 Davide Alberani <da@erlug.linux.it>
  4. RaspiBO <info@raspibo.org>
  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 http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. """
  14. import re
  15. import pymongo
  16. from bson.objectid import ObjectId
  17. re_objectid = re.compile(r'[0-9a-f]{24}')
  18. class EventManDB(object):
  19. """MongoDB connector."""
  20. db = None
  21. connection = None
  22. def __init__(self, url=None, dbName='eventman'):
  23. """Initialize the instance, connecting to the database.
  24. :param url: URL of the database
  25. :type url: str (or None to connect to localhost)
  26. """
  27. self._url = url
  28. self._dbName = dbName
  29. self.connect(url)
  30. def connect(self, url=None, dbName=None):
  31. """Connect to the database.
  32. :param url: URL of the database
  33. :type url: str (or None to connect to localhost)
  34. :return: the database we're connected to
  35. :rtype: :class:`~pymongo.database.Database`
  36. """
  37. if self.db is not None:
  38. return self.db
  39. if url:
  40. self._url = url
  41. if dbName:
  42. self._dbName = dbName
  43. self.connection = pymongo.MongoClient(self._url)
  44. self.db = self.connection[self._dbName]
  45. return self.db
  46. def convert_obj(self, obj):
  47. """Convert an object in a format suitable to be stored in MongoDB.
  48. :param obj: object to convert
  49. :return: object that can be stored in MongoDB.
  50. """
  51. if obj is None:
  52. return None
  53. try:
  54. return ObjectId(obj)
  55. except:
  56. pass
  57. try:
  58. return int(obj)
  59. except:
  60. pass
  61. return obj
  62. def convert(self, seq):
  63. """Convert an object in a format suitable to be stored in MongoDB,
  64. descending lists, tuples and dictionaries (a copy is returned).
  65. :param seq: sequence or object to convert
  66. :return: object that can be stored in MongoDB.
  67. """
  68. if isinstance(seq, dict):
  69. d = {}
  70. for key, item in seq.iteritems():
  71. d[key] = self.convert(item)
  72. return d
  73. if isinstance(seq, (list, tuple)):
  74. return [self.convert(x) for x in seq]
  75. return self.convert_obj(seq)
  76. def get(self, collection, _id):
  77. """Get a single document with the specified `_id`.
  78. :param collection: search the document in this collection
  79. :type collection: str
  80. :param _id: unique ID of the document
  81. :type _id: str or :class:`~bson.objectid.ObjectId`
  82. :return: the document with the given `_id`
  83. :rtype: dict
  84. """
  85. results = self.query(collection, self.convert({'_id': _id}))
  86. return results and results[0] or {}
  87. def query(self, collection, query=None):
  88. """Get multiple documents matching a query.
  89. :param collection: search for documents in this collection
  90. :type collection: str
  91. :param query: search for documents with those attributes
  92. :type query: dict or None
  93. :return: list of matching documents
  94. :rtype: list
  95. """
  96. db = self.connect()
  97. query = self.convert(query or {})
  98. return list(db[collection].find(query))
  99. def add(self, collection, data):
  100. """Insert a new document.
  101. :param collection: insert the document in this collection
  102. :type collection: str
  103. :param data: the document to store
  104. :type data: dict
  105. :return: the document, as created in the database
  106. :rtype: dict
  107. """
  108. db = self.connect()
  109. data = self.convert(data)
  110. _id = db[collection].insert(data)
  111. return self.get(collection, _id)
  112. def insertOne(self, collection, data):
  113. """Insert a document, avoiding duplicates.
  114. :param collection: update a document in this collection
  115. :type collection: str
  116. :param data: the document information to store
  117. :type data: dict
  118. :return: True if the document was already present
  119. :rtype: bool
  120. """
  121. db = self.connect()
  122. data = self.convert(data)
  123. ret = db[collection].update(data, {'$set': data}, upsert=True)
  124. return ret['updatedExisting']
  125. def _buildSearchPattern(self, data, searchBy):
  126. """Return an OR condition."""
  127. _or = []
  128. for searchPattern in searchBy:
  129. try:
  130. _or.append(dict([(k, data[k]) for k in searchPattern if k in data]))
  131. except KeyError:
  132. continue
  133. return _or
  134. def update(self, collection, _id_or_query, data, operator='$set', create=True):
  135. """Update an existing document or create it, if requested.
  136. _id_or_query can be an ID, a dict representing a query or a list of tuples.
  137. In the latter case, the tuples are put in OR; a tuple match if all of its
  138. items from 'data' are contained in the document.
  139. :param collection: update a document in this collection
  140. :type collection: str
  141. :param _id_or_query: ID of the document to be updated, or a query or a list of attributes in the data that must match
  142. :type _id_or_query: str or :class:`~bson.objectid.ObjectId` or iterable
  143. :param data: the updated information to store
  144. :type data: dict
  145. :param operator: operator used to update the document
  146. :type operator: str
  147. :param create: if True, the document is created if no document matches
  148. :type create: bool
  149. :return: a boolean (True if an existing document was updated) and the document after the update
  150. :rtype: tuple of (bool, dict)
  151. """
  152. db = self.connect()
  153. data = self.convert(data or {})
  154. _id_or_query = self.convert(_id_or_query)
  155. if isinstance(_id_or_query, (list, tuple)):
  156. _id_or_query = {'$or': self._buildSearchPattern(data, _id_or_query)}
  157. elif not isinstance(_id_or_query, dict):
  158. _id_or_query = {'_id': _id_or_query}
  159. if '_id' in data:
  160. del data['_id']
  161. res = db[collection].find_and_modify(query=_id_or_query,
  162. update={operator: data}, full_response=True, new=True, upsert=create)
  163. lastErrorObject = res.get('lastErrorObject') or {}
  164. return lastErrorObject.get('updatedExisting', False), res.get('value') or {}
  165. def delete(self, collection, _id_or_query=None, force=False):
  166. """Remove one or more documents from a collection.
  167. :param collection: search the documents in this collection
  168. :type collection: str
  169. :param _id_or_query: unique ID of the document or query to match multiple documents
  170. :type _id_or_query: str or :class:`~bson.objectid.ObjectId` or dict
  171. :param force: force the deletion of all documents, when `_id_or_query` is empty
  172. :type force: bool
  173. """
  174. if not _id_or_query and not force:
  175. return
  176. db = self.connect()
  177. if not isinstance(_id_or_query, dict):
  178. _id_or_query = {'_id': _id_or_query}
  179. _id_or_query = self.convert(_id_or_query)
  180. db[collection].remove(_id_or_query)