backend.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  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. _force_conversion = {
  19. 'seq_hex': str,
  20. 'tickets.seq_hex': str
  21. }
  22. def convert_obj(obj):
  23. """Convert an object in a format suitable to be stored in MongoDB.
  24. :param obj: object to convert
  25. :returns: object that can be stored in MongoDB.
  26. """
  27. if obj is None:
  28. return None
  29. if isinstance(obj, bool):
  30. return obj
  31. try:
  32. return ObjectId(obj)
  33. except:
  34. pass
  35. try:
  36. i_obj = int(obj)
  37. if i_obj > 2**64 - 1:
  38. return obj
  39. return i_obj
  40. except:
  41. pass
  42. return obj
  43. def convert(seq):
  44. """Convert an object to a format suitable to be stored in MongoDB,
  45. descending lists, tuples and dictionaries (a copy is returned).
  46. :param seq: sequence or object to convert
  47. :returns: object that can be stored in MongoDB.
  48. """
  49. if isinstance(seq, dict):
  50. d = {}
  51. for key, item in seq.iteritems():
  52. if key in _force_conversion:
  53. d[key] = _force_conversion[key](item)
  54. else:
  55. d[key] = convert(item)
  56. return d
  57. if isinstance(seq, (list, tuple)):
  58. return [convert(x) for x in seq]
  59. return convert_obj(seq)
  60. class EventManDB(object):
  61. """MongoDB connector."""
  62. db = None
  63. connection = None
  64. # map operations on lists of items.
  65. _operations = {
  66. 'update': '$set',
  67. 'append': '$push',
  68. 'appendUnique': '$addToSet',
  69. 'delete': '$pull',
  70. 'increment': '$inc'
  71. }
  72. def __init__(self, url=None, dbName='eventman'):
  73. """Initialize the instance, connecting to the database.
  74. :param url: URL of the database
  75. :type url: str (or None to connect to localhost)
  76. """
  77. self._url = url
  78. self._dbName = dbName
  79. self.connect(url)
  80. def connect(self, url=None, dbName=None):
  81. """Connect to the database.
  82. :param url: URL of the database
  83. :type url: str (or None to connect to localhost)
  84. :returns: the database we're connected to
  85. :rtype: :class:`~pymongo.database.Database`
  86. """
  87. if self.db is not None:
  88. return self.db
  89. if url:
  90. self._url = url
  91. if dbName:
  92. self._dbName = dbName
  93. self.connection = pymongo.MongoClient(self._url)
  94. self.db = self.connection[self._dbName]
  95. return self.db
  96. def get(self, collection, _id):
  97. """Get a single document with the specified `_id`.
  98. :param collection: search the document in this collection
  99. :type collection: str
  100. :param _id: unique ID of the document
  101. :type _id: str or :class:`~bson.objectid.ObjectId`
  102. :returns: the document with the given `_id`
  103. :rtype: dict
  104. """
  105. results = self.query(collection, convert({'_id': _id}))
  106. return results and results[0] or {}
  107. def query(self, collection, query=None, condition='or'):
  108. """Get multiple documents matching a query.
  109. :param collection: search for documents in this collection
  110. :type collection: str
  111. :param query: search for documents with those attributes
  112. :type query: dict or None
  113. :returns: list of matching documents
  114. :rtype: list
  115. """
  116. db = self.connect()
  117. query = convert(query or {})
  118. if isinstance(query, (list, tuple)):
  119. query = {'$%s' % condition: query}
  120. return list(db[collection].find(query))
  121. def add(self, collection, data, _id=None):
  122. """Insert a new document.
  123. :param collection: insert the document in this collection
  124. :type collection: str
  125. :param data: the document to store
  126. :type data: dict
  127. :param _id: the _id of the document to store; if None, it's generated
  128. :type _id: object
  129. :returns: the document, as created in the database
  130. :rtype: dict
  131. """
  132. db = self.connect()
  133. data = convert(data)
  134. if _id is not None:
  135. data['_id'] = _id
  136. _id = db[collection].insert(data)
  137. return self.get(collection, _id)
  138. def insertOne(self, collection, data):
  139. """Insert a document, avoiding duplicates.
  140. :param collection: update a document in this collection
  141. :type collection: str
  142. :param data: the document information to store
  143. :type data: dict
  144. :returns: True if the document was already present
  145. :rtype: bool
  146. """
  147. db = self.connect()
  148. data = convert(data)
  149. ret = db[collection].update(data, {'$set': data}, upsert=True)
  150. return ret['updatedExisting']
  151. def _buildSearchPattern(self, data, searchBy):
  152. """Return an OR condition."""
  153. _or = []
  154. for searchPattern in searchBy:
  155. try:
  156. _or.append(dict([(k, data[k]) for k in searchPattern if k in data]))
  157. except KeyError:
  158. continue
  159. return _or
  160. def update(self, collection, _id_or_query, data, operation='update',
  161. updateList=None, create=True):
  162. """Update an existing document or create it, if requested.
  163. _id_or_query can be an ID, a dict representing a query or a list of tuples.
  164. In the latter case, the tuples are put in OR; a tuple match if all of its
  165. items from 'data' are contained in the document.
  166. :param collection: update a document in this collection
  167. :type collection: str
  168. :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
  169. :type _id_or_query: str or :class:`~bson.objectid.ObjectId` or iterable
  170. :param data: the updated information to store
  171. :type data: dict
  172. :param operation: operation used to update the document or a portion of it, like a list (update, append, appendUnique, delete, increment)
  173. :type operation: str
  174. :param updateList: if set, it's considered the name of a list (the first matching element will be updated)
  175. :type updateList: str
  176. :param create: if True, the document is created if no document matches
  177. :type create: bool
  178. :returns: a boolean (True if an existing document was updated) and the document after the update
  179. :rtype: tuple of (bool, dict)
  180. """
  181. db = self.connect()
  182. data = convert(data or {})
  183. _id_or_query = convert(_id_or_query)
  184. if isinstance(_id_or_query, (list, tuple)):
  185. _id_or_query = {'$or': self._buildSearchPattern(data, _id_or_query)}
  186. elif not isinstance(_id_or_query, dict):
  187. _id_or_query = {'_id': _id_or_query}
  188. if '_id' in data:
  189. del data['_id']
  190. operator = self._operations.get(operation)
  191. if updateList:
  192. newData = {}
  193. for key, value in data.iteritems():
  194. newData['%s.$.%s' % (updateList, key)] = value
  195. data = newData
  196. res = db[collection].find_and_modify(query=_id_or_query,
  197. update={operator: data}, full_response=True, new=True, upsert=create)
  198. lastErrorObject = res.get('lastErrorObject') or {}
  199. return lastErrorObject.get('updatedExisting', False), res.get('value') or {}
  200. def delete(self, collection, _id_or_query=None, force=False):
  201. """Remove one or more documents from a collection.
  202. :param collection: search the documents in this collection
  203. :type collection: str
  204. :param _id_or_query: unique ID of the document or query to match multiple documents
  205. :type _id_or_query: str or :class:`~bson.objectid.ObjectId` or dict
  206. :param force: force the deletion of all documents, when `_id_or_query` is empty
  207. :type force: bool
  208. """
  209. if not _id_or_query and not force:
  210. return
  211. db = self.connect()
  212. if not isinstance(_id_or_query, dict):
  213. _id_or_query = {'_id': _id_or_query}
  214. _id_or_query = convert(_id_or_query)
  215. db[collection].remove(_id_or_query)