common.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. # Copyright 2011-2015 MongoDB, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you
  4. # may not use this file except in compliance with the License. You
  5. # may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. # implied. See the License for the specific language governing
  13. # permissions and limitations under the License.
  14. """Functions and classes common to multiple pymongo modules."""
  15. import collections
  16. from bson.binary import (STANDARD, PYTHON_LEGACY,
  17. JAVA_LEGACY, CSHARP_LEGACY)
  18. from bson.codec_options import CodecOptions
  19. from bson.py3compat import string_type, integer_types
  20. from pymongo.auth import MECHANISMS
  21. from pymongo.errors import ConfigurationError
  22. from pymongo.read_preferences import (read_pref_mode_from_name,
  23. _ServerMode)
  24. from pymongo.ssl_support import validate_cert_reqs
  25. from pymongo.write_concern import WriteConcern
  26. # Defaults until we connect to a server and get updated limits.
  27. MAX_BSON_SIZE = 16 * (1024 ** 2)
  28. MAX_MESSAGE_SIZE = 2 * MAX_BSON_SIZE
  29. MIN_WIRE_VERSION = 0
  30. MAX_WIRE_VERSION = 0
  31. MAX_WRITE_BATCH_SIZE = 1000
  32. # What this version of PyMongo supports.
  33. MIN_SUPPORTED_WIRE_VERSION = 0
  34. MAX_SUPPORTED_WIRE_VERSION = 3
  35. # Frequency to call ismaster on servers, in seconds.
  36. HEARTBEAT_FREQUENCY = 10
  37. # Frequency to process kill-cursors, in seconds. See MongoClient.close_cursor.
  38. KILL_CURSOR_FREQUENCY = 1
  39. # How long to wait, in seconds, for a suitable server to be found before
  40. # aborting an operation. For example, if the client attempts an insert
  41. # during a replica set election, SERVER_SELECTION_TIMEOUT governs the
  42. # longest it is willing to wait for a new primary to be found.
  43. SERVER_SELECTION_TIMEOUT = 30
  44. # Spec requires at least 500ms between ismaster calls.
  45. MIN_HEARTBEAT_INTERVAL = 0.5
  46. # Default connectTimeout in seconds.
  47. CONNECT_TIMEOUT = 20.0
  48. # Default value for maxPoolSize.
  49. MAX_POOL_SIZE = 100
  50. # Default value for localThresholdMS.
  51. LOCAL_THRESHOLD_MS = 15
  52. # mongod/s 2.6 and above return code 59 when a
  53. # command doesn't exist. mongod versions previous
  54. # to 2.6 and mongos 2.4.x return no error code
  55. # when a command does exist. mongos versions previous
  56. # to 2.4.0 return code 13390 when a command does not
  57. # exist.
  58. COMMAND_NOT_FOUND_CODES = (59, 13390, None)
  59. def partition_node(node):
  60. """Split a host:port string into (host, int(port)) pair."""
  61. host = node
  62. port = 27017
  63. idx = node.rfind(':')
  64. if idx != -1:
  65. host, port = node[:idx], int(node[idx + 1:])
  66. if host.startswith('['):
  67. host = host[1:-1]
  68. return host, port
  69. def clean_node(node):
  70. """Split and normalize a node name from an ismaster response."""
  71. host, port = partition_node(node)
  72. # Normalize hostname to lowercase, since DNS is case-insensitive:
  73. # http://tools.ietf.org/html/rfc4343
  74. # This prevents useless rediscovery if "foo.com" is in the seed list but
  75. # "FOO.com" is in the ismaster response.
  76. return host.lower(), port
  77. def raise_config_error(key, dummy):
  78. """Raise ConfigurationError with the given key name."""
  79. raise ConfigurationError("Unknown option %s" % (key,))
  80. # Mapping of URI uuid representation options to valid subtypes.
  81. _UUID_REPRESENTATIONS = {
  82. 'standard': STANDARD,
  83. 'pythonLegacy': PYTHON_LEGACY,
  84. 'javaLegacy': JAVA_LEGACY,
  85. 'csharpLegacy': CSHARP_LEGACY
  86. }
  87. def validate_boolean(option, value):
  88. """Validates that 'value' is True or False."""
  89. if isinstance(value, bool):
  90. return value
  91. raise TypeError("%s must be True or False" % (option,))
  92. def validate_boolean_or_string(option, value):
  93. """Validates that value is True, False, 'true', or 'false'."""
  94. if isinstance(value, string_type):
  95. if value not in ('true', 'false'):
  96. raise ValueError("The value of %s must be "
  97. "'true' or 'false'" % (option,))
  98. return value == 'true'
  99. return validate_boolean(option, value)
  100. def validate_integer(option, value):
  101. """Validates that 'value' is an integer (or basestring representation).
  102. """
  103. if isinstance(value, integer_types):
  104. return value
  105. elif isinstance(value, string_type):
  106. if not value.isdigit():
  107. raise ValueError("The value of %s must be "
  108. "an integer" % (option,))
  109. return int(value)
  110. raise TypeError("Wrong type for %s, value must be an integer" % (option,))
  111. def validate_positive_integer(option, value):
  112. """Validate that 'value' is a positive integer.
  113. """
  114. val = validate_integer(option, value)
  115. if val < 0:
  116. raise ValueError("The value of %s must be "
  117. "a positive integer" % (option,))
  118. return val
  119. def validate_readable(option, value):
  120. """Validates that 'value' is file-like and readable.
  121. """
  122. if value is None:
  123. return value
  124. # First make sure its a string py3.3 open(True, 'r') succeeds
  125. # Used in ssl cert checking due to poor ssl module error reporting
  126. value = validate_string(option, value)
  127. open(value, 'r').close()
  128. return value
  129. def validate_positive_integer_or_none(option, value):
  130. """Validate that 'value' is a positive integer or None.
  131. """
  132. if value is None:
  133. return value
  134. return validate_positive_integer(option, value)
  135. def validate_string(option, value):
  136. """Validates that 'value' is an instance of `basestring` for Python 2
  137. or `str` for Python 3.
  138. """
  139. if isinstance(value, string_type):
  140. return value
  141. raise TypeError("Wrong type for %s, value must be "
  142. "an instance of %s" % (option, string_type.__name__))
  143. def validate_string_or_none(option, value):
  144. """Validates that 'value' is an instance of `basestring` or `None`.
  145. """
  146. if value is None:
  147. return value
  148. return validate_string(option, value)
  149. def validate_int_or_basestring(option, value):
  150. """Validates that 'value' is an integer or string.
  151. """
  152. if isinstance(value, integer_types):
  153. return value
  154. elif isinstance(value, string_type):
  155. if value.isdigit():
  156. return int(value)
  157. return value
  158. raise TypeError("Wrong type for %s, value must be an "
  159. "integer or a string" % (option,))
  160. def validate_positive_float(option, value):
  161. """Validates that 'value' is a float, or can be converted to one, and is
  162. positive.
  163. """
  164. errmsg = "%s must be an integer or float" % (option,)
  165. try:
  166. value = float(value)
  167. except ValueError:
  168. raise ValueError(errmsg)
  169. except TypeError:
  170. raise TypeError(errmsg)
  171. # float('inf') doesn't work in 2.4 or 2.5 on Windows, so just cap floats at
  172. # one billion - this is a reasonable approximation for infinity
  173. if not 0 < value < 1e9:
  174. raise ValueError("%s must be greater than 0 and "
  175. "less than one billion" % (option,))
  176. return value
  177. def validate_positive_float_or_zero(option, value):
  178. """Validates that 'value' is 0 or a positive float, or can be converted to
  179. 0 or a positive float.
  180. """
  181. if value == 0 or value == "0":
  182. return 0
  183. return validate_positive_float(option, value)
  184. def validate_timeout_or_none(option, value):
  185. """Validates a timeout specified in milliseconds returning
  186. a value in floating point seconds.
  187. """
  188. if value is None:
  189. return value
  190. return validate_positive_float(option, value) / 1000.0
  191. def validate_timeout_or_zero(option, value):
  192. """Validates a timeout specified in milliseconds returning
  193. a value in floating point seconds for the case where None is an error
  194. and 0 is valid. Setting the timeout to nothing in the URI string is a
  195. config error.
  196. """
  197. if value is None:
  198. raise ConfigurationError("%s cannot be None" % (option, ))
  199. if value == 0 or value == "0":
  200. return 0
  201. return validate_positive_float(option, value) / 1000.0
  202. def validate_read_preference(dummy, value):
  203. """Validate a read preference.
  204. """
  205. if not isinstance(value, _ServerMode):
  206. raise TypeError("%r is not a read preference." % (value,))
  207. return value
  208. def validate_read_preference_mode(dummy, name):
  209. """Validate read preference mode for a MongoReplicaSetClient.
  210. """
  211. try:
  212. return read_pref_mode_from_name(name)
  213. except ValueError:
  214. raise ValueError("%s is not a valid read preference" % (name,))
  215. def validate_auth_mechanism(option, value):
  216. """Validate the authMechanism URI option.
  217. """
  218. # CRAM-MD5 is for server testing only. Undocumented,
  219. # unsupported, may be removed at any time. You have
  220. # been warned.
  221. if value not in MECHANISMS and value != 'CRAM-MD5':
  222. raise ValueError("%s must be in %s" % (option, tuple(MECHANISMS)))
  223. return value
  224. def validate_uuid_representation(dummy, value):
  225. """Validate the uuid representation option selected in the URI.
  226. """
  227. try:
  228. return _UUID_REPRESENTATIONS[value]
  229. except KeyError:
  230. raise ValueError("%s is an invalid UUID representation. "
  231. "Must be one of "
  232. "%s" % (value, tuple(_UUID_REPRESENTATIONS)))
  233. def validate_read_preference_tags(name, value):
  234. """Parse readPreferenceTags if passed as a client kwarg.
  235. """
  236. if not isinstance(value, list):
  237. value = [value]
  238. tag_sets = []
  239. for tag_set in value:
  240. if tag_set == '':
  241. tag_sets.append({})
  242. continue
  243. try:
  244. tag_sets.append(dict([tag.split(":")
  245. for tag in tag_set.split(",")]))
  246. except Exception:
  247. raise ValueError("%r not a valid "
  248. "value for %s" % (tag_set, name))
  249. return tag_sets
  250. _MECHANISM_PROPS = frozenset(['SERVICE_NAME'])
  251. def validate_auth_mechanism_properties(option, value):
  252. """Validate authMechanismProperties."""
  253. value = validate_string(option, value)
  254. props = {}
  255. for opt in value.split(','):
  256. try:
  257. key, val = opt.split(':')
  258. if key not in _MECHANISM_PROPS:
  259. raise ValueError("%s is not a supported auth "
  260. "mechanism property. Must be one of "
  261. "%s." % (key, tuple(_MECHANISM_PROPS)))
  262. props[key] = val
  263. except ValueError:
  264. raise ValueError("auth mechanism properties must be "
  265. "key:value pairs like SERVICE_NAME:"
  266. "mongodb, not %s." % (opt,))
  267. return props
  268. def validate_document_class(option, value):
  269. """Validate the document_class option."""
  270. if not issubclass(value, collections.MutableMapping):
  271. raise TypeError("%s must be dict, bson.son.SON, or another "
  272. "sublass of collections.MutableMapping" % (option,))
  273. return value
  274. def validate_is_mapping(option, value):
  275. """Validate the type of method arguments that expect a document."""
  276. if not isinstance(value, collections.Mapping):
  277. raise TypeError("%s must be an instance of dict, bson.son.SON, or "
  278. "other type that inherits from "
  279. "collections.Mapping" % (option,))
  280. def validate_is_mutable_mapping(option, value):
  281. """Validate the type of method arguments that expect a mutable document."""
  282. if not isinstance(value, collections.MutableMapping):
  283. raise TypeError("%s must be an instance of dict, bson.son.SON, or "
  284. "other type that inherits from "
  285. "collections.MutableMapping" % (option,))
  286. def validate_ok_for_replace(replacement):
  287. """Validate a replacement document."""
  288. validate_is_mapping("replacement", replacement)
  289. # Replacement can be {}
  290. if replacement:
  291. first = next(iter(replacement))
  292. if first.startswith('$'):
  293. raise ValueError('replacement can not include $ operators')
  294. def validate_ok_for_update(update):
  295. """Validate an update document."""
  296. validate_is_mapping("update", update)
  297. # Update can not be {}
  298. if not update:
  299. raise ValueError('update only works with $ operators')
  300. first = next(iter(update))
  301. if not first.startswith('$'):
  302. raise ValueError('update only works with $ operators')
  303. # journal is an alias for j,
  304. # wtimeoutms is an alias for wtimeout,
  305. VALIDATORS = {
  306. 'replicaset': validate_string_or_none,
  307. 'w': validate_int_or_basestring,
  308. 'wtimeout': validate_integer,
  309. 'wtimeoutms': validate_integer,
  310. 'fsync': validate_boolean_or_string,
  311. 'j': validate_boolean_or_string,
  312. 'journal': validate_boolean_or_string,
  313. 'connecttimeoutms': validate_timeout_or_none,
  314. 'maxpoolsize': validate_positive_integer_or_none,
  315. 'socketkeepalive': validate_boolean_or_string,
  316. 'sockettimeoutms': validate_timeout_or_none,
  317. 'waitqueuetimeoutms': validate_timeout_or_none,
  318. 'waitqueuemultiple': validate_positive_integer_or_none,
  319. 'ssl': validate_boolean_or_string,
  320. 'ssl_keyfile': validate_readable,
  321. 'ssl_certfile': validate_readable,
  322. 'ssl_cert_reqs': validate_cert_reqs,
  323. 'ssl_ca_certs': validate_readable,
  324. 'ssl_match_hostname': validate_boolean,
  325. 'read_preference': validate_read_preference,
  326. 'readpreference': validate_read_preference_mode,
  327. 'readpreferencetags': validate_read_preference_tags,
  328. 'localthresholdms': validate_positive_float_or_zero,
  329. 'serverselectiontimeoutms': validate_timeout_or_zero,
  330. 'authmechanism': validate_auth_mechanism,
  331. 'authsource': validate_string,
  332. 'authmechanismproperties': validate_auth_mechanism_properties,
  333. 'document_class': validate_document_class,
  334. 'tz_aware': validate_boolean_or_string,
  335. 'uuidrepresentation': validate_uuid_representation,
  336. }
  337. _AUTH_OPTIONS = frozenset(['authmechanismproperties'])
  338. def validate_auth_option(option, value):
  339. """Validate optional authentication parameters.
  340. """
  341. lower, value = validate(option, value)
  342. if lower not in _AUTH_OPTIONS:
  343. raise ConfigurationError('Unknown '
  344. 'authentication option: %s' % (option,))
  345. return lower, value
  346. def validate(option, value):
  347. """Generic validation function.
  348. """
  349. lower = option.lower()
  350. validator = VALIDATORS.get(lower, raise_config_error)
  351. value = validator(option, value)
  352. return lower, value
  353. WRITE_CONCERN_OPTIONS = frozenset([
  354. 'w',
  355. 'wtimeout',
  356. 'wtimeoutms',
  357. 'fsync',
  358. 'j',
  359. 'journal'
  360. ])
  361. class BaseObject(object):
  362. """A base class that provides attributes and methods common
  363. to multiple pymongo classes.
  364. SHOULD NOT BE USED BY DEVELOPERS EXTERNAL TO MONGODB.
  365. """
  366. def __init__(self, codec_options, read_preference, write_concern):
  367. if not isinstance(codec_options, CodecOptions):
  368. raise TypeError("codec_options must be an instance of "
  369. "bson.codec_options.CodecOptions")
  370. self.__codec_options = codec_options
  371. if not isinstance(read_preference, _ServerMode):
  372. raise TypeError("%r is not valid for read_preference. See "
  373. "pymongo.read_preferences for valid "
  374. "options." % (read_preference,))
  375. self.__read_preference = read_preference
  376. if not isinstance(write_concern, WriteConcern):
  377. raise TypeError("write_concern must be an instance of "
  378. "pymongo.write_concern.WriteConcern")
  379. self.__write_concern = write_concern
  380. @property
  381. def codec_options(self):
  382. """Read only access to the :class:`~bson.codec_options.CodecOptions`
  383. of this instance.
  384. """
  385. return self.__codec_options
  386. @property
  387. def write_concern(self):
  388. """Read only access to the :class:`~pymongo.write_concern.WriteConcern`
  389. of this instance.
  390. .. versionchanged:: 3.0
  391. The :attr:`write_concern` attribute is now read only.
  392. """
  393. return self.__write_concern
  394. @property
  395. def read_preference(self):
  396. """Read only access to the read preference of this instance.
  397. .. versionchanged:: 3.0
  398. The :attr:`read_preference` attribute is now read only.
  399. """
  400. return self.__read_preference