monco.py 9.6 KB

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