connection.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. from pymongo import MongoClient, ReadPreference, uri_parser
  2. import six
  3. from mongoengine.python_support import IS_PYMONGO_3
  4. __all__ = ['MongoEngineConnectionError', 'connect', 'register_connection',
  5. 'DEFAULT_CONNECTION_NAME']
  6. DEFAULT_CONNECTION_NAME = 'default'
  7. if IS_PYMONGO_3:
  8. READ_PREFERENCE = ReadPreference.PRIMARY
  9. else:
  10. from pymongo import MongoReplicaSetClient
  11. READ_PREFERENCE = False
  12. class MongoEngineConnectionError(Exception):
  13. """Error raised when the database connection can't be established or
  14. when a connection with a requested alias can't be retrieved.
  15. """
  16. pass
  17. _connection_settings = {}
  18. _connections = {}
  19. _dbs = {}
  20. def register_connection(alias, db=None, name=None, host=None, port=None,
  21. read_preference=READ_PREFERENCE,
  22. username=None, password=None,
  23. authentication_source=None,
  24. authentication_mechanism=None,
  25. **kwargs):
  26. """Add a connection.
  27. :param alias: the name that will be used to refer to this connection
  28. throughout MongoEngine
  29. :param name: the name of the specific database to use
  30. :param db: the name of the database to use, for compatibility with connect
  31. :param host: the host name of the :program:`mongod` instance to connect to
  32. :param port: the port that the :program:`mongod` instance is running on
  33. :param read_preference: The read preference for the collection
  34. ** Added pymongo 2.1
  35. :param username: username to authenticate with
  36. :param password: password to authenticate with
  37. :param authentication_source: database to authenticate against
  38. :param authentication_mechanism: database authentication mechanisms.
  39. By default, use SCRAM-SHA-1 with MongoDB 3.0 and later,
  40. MONGODB-CR (MongoDB Challenge Response protocol) for older servers.
  41. :param is_mock: explicitly use mongomock for this connection
  42. (can also be done by using `mongomock://` as db host prefix)
  43. :param kwargs: ad-hoc parameters to be passed into the pymongo driver,
  44. for example maxpoolsize, tz_aware, etc. See the documentation
  45. for pymongo's `MongoClient` for a full list.
  46. .. versionchanged:: 0.10.6 - added mongomock support
  47. """
  48. conn_settings = {
  49. 'name': name or db or 'test',
  50. 'host': host or 'localhost',
  51. 'port': port or 27017,
  52. 'read_preference': read_preference,
  53. 'username': username,
  54. 'password': password,
  55. 'authentication_source': authentication_source,
  56. 'authentication_mechanism': authentication_mechanism
  57. }
  58. conn_host = conn_settings['host']
  59. # Host can be a list or a string, so if string, force to a list.
  60. if isinstance(conn_host, six.string_types):
  61. conn_host = [conn_host]
  62. resolved_hosts = []
  63. for entity in conn_host:
  64. # Handle Mongomock
  65. if entity.startswith('mongomock://'):
  66. conn_settings['is_mock'] = True
  67. # `mongomock://` is not a valid url prefix and must be replaced by `mongodb://`
  68. resolved_hosts.append(entity.replace('mongomock://', 'mongodb://', 1))
  69. # Handle URI style connections, only updating connection params which
  70. # were explicitly specified in the URI.
  71. elif '://' in entity:
  72. uri_dict = uri_parser.parse_uri(entity)
  73. resolved_hosts.append(entity)
  74. if uri_dict.get('database'):
  75. conn_settings['name'] = uri_dict.get('database')
  76. for param in ('read_preference', 'username', 'password'):
  77. if uri_dict.get(param):
  78. conn_settings[param] = uri_dict[param]
  79. uri_options = uri_dict['options']
  80. if 'replicaset' in uri_options:
  81. conn_settings['replicaSet'] = uri_options['replicaset']
  82. if 'authsource' in uri_options:
  83. conn_settings['authentication_source'] = uri_options['authsource']
  84. if 'authmechanism' in uri_options:
  85. conn_settings['authentication_mechanism'] = uri_options['authmechanism']
  86. if IS_PYMONGO_3 and 'readpreference' in uri_options:
  87. read_preferences = (
  88. ReadPreference.NEAREST,
  89. ReadPreference.PRIMARY,
  90. ReadPreference.PRIMARY_PREFERRED,
  91. ReadPreference.SECONDARY,
  92. ReadPreference.SECONDARY_PREFERRED)
  93. read_pf_mode = uri_options['readpreference'].lower()
  94. for preference in read_preferences:
  95. if preference.name.lower() == read_pf_mode:
  96. conn_settings['read_preference'] = preference
  97. break
  98. else:
  99. resolved_hosts.append(entity)
  100. conn_settings['host'] = resolved_hosts
  101. # Deprecated parameters that should not be passed on
  102. kwargs.pop('slaves', None)
  103. kwargs.pop('is_slave', None)
  104. conn_settings.update(kwargs)
  105. _connection_settings[alias] = conn_settings
  106. def disconnect(alias=DEFAULT_CONNECTION_NAME):
  107. """Close the connection with a given alias."""
  108. if alias in _connections:
  109. get_connection(alias=alias).close()
  110. del _connections[alias]
  111. if alias in _dbs:
  112. del _dbs[alias]
  113. def get_connection(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
  114. """Return a connection with a given alias."""
  115. # Connect to the database if not already connected
  116. if reconnect:
  117. disconnect(alias)
  118. # If the requested alias already exists in the _connections list, return
  119. # it immediately.
  120. if alias in _connections:
  121. return _connections[alias]
  122. # Validate that the requested alias exists in the _connection_settings.
  123. # Raise MongoEngineConnectionError if it doesn't.
  124. if alias not in _connection_settings:
  125. if alias == DEFAULT_CONNECTION_NAME:
  126. msg = 'You have not defined a default connection'
  127. else:
  128. msg = 'Connection with alias "%s" has not been defined' % alias
  129. raise MongoEngineConnectionError(msg)
  130. def _clean_settings(settings_dict):
  131. # set literal more efficient than calling set function
  132. irrelevant_fields_set = {
  133. 'name', 'username', 'password',
  134. 'authentication_source', 'authentication_mechanism'
  135. }
  136. return {
  137. k: v for k, v in settings_dict.items()
  138. if k not in irrelevant_fields_set
  139. }
  140. # Retrieve a copy of the connection settings associated with the requested
  141. # alias and remove the database name and authentication info (we don't
  142. # care about them at this point).
  143. conn_settings = _clean_settings(_connection_settings[alias].copy())
  144. # Determine if we should use PyMongo's or mongomock's MongoClient.
  145. is_mock = conn_settings.pop('is_mock', False)
  146. if is_mock:
  147. try:
  148. import mongomock
  149. except ImportError:
  150. raise RuntimeError('You need mongomock installed to mock '
  151. 'MongoEngine.')
  152. connection_class = mongomock.MongoClient
  153. else:
  154. connection_class = MongoClient
  155. # For replica set connections with PyMongo 2.x, use
  156. # MongoReplicaSetClient.
  157. # TODO remove this once we stop supporting PyMongo 2.x.
  158. if 'replicaSet' in conn_settings and not IS_PYMONGO_3:
  159. connection_class = MongoReplicaSetClient
  160. conn_settings['hosts_or_uri'] = conn_settings.pop('host', None)
  161. # hosts_or_uri has to be a string, so if 'host' was provided
  162. # as a list, join its parts and separate them by ','
  163. if isinstance(conn_settings['hosts_or_uri'], list):
  164. conn_settings['hosts_or_uri'] = ','.join(
  165. conn_settings['hosts_or_uri'])
  166. # Discard port since it can't be used on MongoReplicaSetClient
  167. conn_settings.pop('port', None)
  168. # Iterate over all of the connection settings and if a connection with
  169. # the same parameters is already established, use it instead of creating
  170. # a new one.
  171. existing_connection = None
  172. connection_settings_iterator = (
  173. (db_alias, settings.copy())
  174. for db_alias, settings in _connection_settings.items()
  175. )
  176. for db_alias, connection_settings in connection_settings_iterator:
  177. connection_settings = _clean_settings(connection_settings)
  178. if conn_settings == connection_settings and _connections.get(db_alias):
  179. existing_connection = _connections[db_alias]
  180. break
  181. # If an existing connection was found, assign it to the new alias
  182. if existing_connection:
  183. _connections[alias] = existing_connection
  184. else:
  185. # Otherwise, create the new connection for this alias. Raise
  186. # MongoEngineConnectionError if it can't be established.
  187. try:
  188. _connections[alias] = connection_class(**conn_settings)
  189. except Exception as e:
  190. raise MongoEngineConnectionError(
  191. 'Cannot connect to database %s :\n%s' % (alias, e))
  192. return _connections[alias]
  193. def get_db(alias=DEFAULT_CONNECTION_NAME, reconnect=False):
  194. if reconnect:
  195. disconnect(alias)
  196. if alias not in _dbs:
  197. conn = get_connection(alias)
  198. conn_settings = _connection_settings[alias]
  199. db = conn[conn_settings['name']]
  200. auth_kwargs = {'source': conn_settings['authentication_source']}
  201. if conn_settings['authentication_mechanism'] is not None:
  202. auth_kwargs['mechanism'] = conn_settings['authentication_mechanism']
  203. # Authenticate if necessary
  204. if conn_settings['username'] and (conn_settings['password'] or
  205. conn_settings['authentication_mechanism'] == 'MONGODB-X509'):
  206. db.authenticate(conn_settings['username'], conn_settings['password'], **auth_kwargs)
  207. _dbs[alias] = db
  208. return _dbs[alias]
  209. def connect(db=None, alias=DEFAULT_CONNECTION_NAME, **kwargs):
  210. """Connect to the database specified by the 'db' argument.
  211. Connection settings may be provided here as well if the database is not
  212. running on the default port on localhost. If authentication is needed,
  213. provide username and password arguments as well.
  214. Multiple databases are supported by using aliases. Provide a separate
  215. `alias` to connect to a different instance of :program:`mongod`.
  216. See the docstring for `register_connection` for more details about all
  217. supported kwargs.
  218. .. versionchanged:: 0.6 - added multiple database support.
  219. """
  220. if alias not in _connections:
  221. register_connection(alias, db, **kwargs)
  222. return get_connection(alias)
  223. # Support old naming convention
  224. _get_connection = get_connection
  225. _get_db = get_db