objectid.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. # Copyright 2009-2015 MongoDB, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You 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 implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Tools for working with MongoDB `ObjectIds
  15. <http://dochub.mongodb.org/core/objectids>`_.
  16. """
  17. import binascii
  18. import calendar
  19. import datetime
  20. import hashlib
  21. import os
  22. import random
  23. import socket
  24. import struct
  25. import threading
  26. import time
  27. from bson.errors import InvalidId
  28. from bson.py3compat import PY3, bytes_from_hex, string_type, text_type
  29. from bson.tz_util import utc
  30. def _machine_bytes():
  31. """Get the machine portion of an ObjectId.
  32. """
  33. machine_hash = hashlib.md5()
  34. if PY3:
  35. # gethostname() returns a unicode string in python 3.x
  36. # while update() requires a byte string.
  37. machine_hash.update(socket.gethostname().encode())
  38. else:
  39. # Calling encode() here will fail with non-ascii hostnames
  40. machine_hash.update(socket.gethostname())
  41. return machine_hash.digest()[0:3]
  42. def _raise_invalid_id(oid):
  43. raise InvalidId(
  44. "%r is not a valid ObjectId, it must be a 12-byte input"
  45. " or a 24-character hex string" % oid)
  46. class ObjectId(object):
  47. """A MongoDB ObjectId.
  48. """
  49. _inc = random.randint(0, 0xFFFFFF)
  50. _inc_lock = threading.Lock()
  51. _machine_bytes = _machine_bytes()
  52. __slots__ = ('__id')
  53. _type_marker = 7
  54. def __init__(self, oid=None):
  55. """Initialize a new ObjectId.
  56. An ObjectId is a 12-byte unique identifier consisting of:
  57. - a 4-byte value representing the seconds since the Unix epoch,
  58. - a 3-byte machine identifier,
  59. - a 2-byte process id, and
  60. - a 3-byte counter, starting with a random value.
  61. By default, ``ObjectId()`` creates a new unique identifier. The
  62. optional parameter `oid` can be an :class:`ObjectId`, or any 12
  63. :class:`bytes` or, in Python 2, any 12-character :class:`str`.
  64. For example, the 12 bytes b'foo-bar-quux' do not follow the ObjectId
  65. specification but they are acceptable input::
  66. >>> ObjectId(b'foo-bar-quux')
  67. ObjectId('666f6f2d6261722d71757578')
  68. `oid` can also be a :class:`unicode` or :class:`str` of 24 hex digits::
  69. >>> ObjectId('0123456789ab0123456789ab')
  70. ObjectId('0123456789ab0123456789ab')
  71. >>>
  72. >>> # A u-prefixed unicode literal:
  73. >>> ObjectId(u'0123456789ab0123456789ab')
  74. ObjectId('0123456789ab0123456789ab')
  75. Raises :class:`~bson.errors.InvalidId` if `oid` is not 12 bytes nor
  76. 24 hex digits, or :class:`TypeError` if `oid` is not an accepted type.
  77. :Parameters:
  78. - `oid` (optional): a valid ObjectId.
  79. .. mongodoc:: objectids
  80. """
  81. if oid is None:
  82. self.__generate()
  83. elif isinstance(oid, bytes) and len(oid) == 12:
  84. self.__id = oid
  85. else:
  86. self.__validate(oid)
  87. @classmethod
  88. def from_datetime(cls, generation_time):
  89. """Create a dummy ObjectId instance with a specific generation time.
  90. This method is useful for doing range queries on a field
  91. containing :class:`ObjectId` instances.
  92. .. warning::
  93. It is not safe to insert a document containing an ObjectId
  94. generated using this method. This method deliberately
  95. eliminates the uniqueness guarantee that ObjectIds
  96. generally provide. ObjectIds generated with this method
  97. should be used exclusively in queries.
  98. `generation_time` will be converted to UTC. Naive datetime
  99. instances will be treated as though they already contain UTC.
  100. An example using this helper to get documents where ``"_id"``
  101. was generated before January 1, 2010 would be:
  102. >>> gen_time = datetime.datetime(2010, 1, 1)
  103. >>> dummy_id = ObjectId.from_datetime(gen_time)
  104. >>> result = collection.find({"_id": {"$lt": dummy_id}})
  105. :Parameters:
  106. - `generation_time`: :class:`~datetime.datetime` to be used
  107. as the generation time for the resulting ObjectId.
  108. """
  109. if generation_time.utcoffset() is not None:
  110. generation_time = generation_time - generation_time.utcoffset()
  111. timestamp = calendar.timegm(generation_time.timetuple())
  112. oid = struct.pack(
  113. ">i", int(timestamp)) + b"\x00\x00\x00\x00\x00\x00\x00\x00"
  114. return cls(oid)
  115. @classmethod
  116. def is_valid(cls, oid):
  117. """Checks if a `oid` string is valid or not.
  118. :Parameters:
  119. - `oid`: the object id to validate
  120. .. versionadded:: 2.3
  121. """
  122. if not oid:
  123. return False
  124. try:
  125. ObjectId(oid)
  126. return True
  127. except (InvalidId, TypeError):
  128. return False
  129. def __generate(self):
  130. """Generate a new value for this ObjectId.
  131. """
  132. # 4 bytes current time
  133. oid = struct.pack(">i", int(time.time()))
  134. # 3 bytes machine
  135. oid += ObjectId._machine_bytes
  136. # 2 bytes pid
  137. oid += struct.pack(">H", os.getpid() % 0xFFFF)
  138. # 3 bytes inc
  139. with ObjectId._inc_lock:
  140. oid += struct.pack(">i", ObjectId._inc)[1:4]
  141. ObjectId._inc = (ObjectId._inc + 1) % 0xFFFFFF
  142. self.__id = oid
  143. def __validate(self, oid):
  144. """Validate and use the given id for this ObjectId.
  145. Raises TypeError if id is not an instance of
  146. (:class:`basestring` (:class:`str` or :class:`bytes`
  147. in python 3), ObjectId) and InvalidId if it is not a
  148. valid ObjectId.
  149. :Parameters:
  150. - `oid`: a valid ObjectId
  151. """
  152. if isinstance(oid, ObjectId):
  153. self.__id = oid.binary
  154. # bytes or unicode in python 2, str in python 3
  155. elif isinstance(oid, string_type):
  156. if len(oid) == 24:
  157. try:
  158. self.__id = bytes_from_hex(oid)
  159. except (TypeError, ValueError):
  160. _raise_invalid_id(oid)
  161. else:
  162. _raise_invalid_id(oid)
  163. else:
  164. raise TypeError("id must be an instance of (bytes, %s, ObjectId), "
  165. "not %s" % (text_type.__name__, type(oid)))
  166. @property
  167. def binary(self):
  168. """12-byte binary representation of this ObjectId.
  169. """
  170. return self.__id
  171. @property
  172. def generation_time(self):
  173. """A :class:`datetime.datetime` instance representing the time of
  174. generation for this :class:`ObjectId`.
  175. The :class:`datetime.datetime` is timezone aware, and
  176. represents the generation time in UTC. It is precise to the
  177. second.
  178. """
  179. timestamp = struct.unpack(">i", self.__id[0:4])[0]
  180. return datetime.datetime.fromtimestamp(timestamp, utc)
  181. def __getstate__(self):
  182. """return value of object for pickling.
  183. needed explicitly because __slots__() defined.
  184. """
  185. return self.__id
  186. def __setstate__(self, value):
  187. """explicit state set from pickling
  188. """
  189. # Provide backwards compatability with OIDs
  190. # pickled with pymongo-1.9 or older.
  191. if isinstance(value, dict):
  192. oid = value["_ObjectId__id"]
  193. else:
  194. oid = value
  195. # ObjectIds pickled in python 2.x used `str` for __id.
  196. # In python 3.x this has to be converted to `bytes`
  197. # by encoding latin-1.
  198. if PY3 and isinstance(oid, text_type):
  199. self.__id = oid.encode('latin-1')
  200. else:
  201. self.__id = oid
  202. def __str__(self):
  203. if PY3:
  204. return binascii.hexlify(self.__id).decode()
  205. return binascii.hexlify(self.__id)
  206. def __repr__(self):
  207. return "ObjectId('%s')" % (str(self),)
  208. def __eq__(self, other):
  209. if isinstance(other, ObjectId):
  210. return self.__id == other.binary
  211. return NotImplemented
  212. def __ne__(self, other):
  213. if isinstance(other, ObjectId):
  214. return self.__id != other.binary
  215. return NotImplemented
  216. def __lt__(self, other):
  217. if isinstance(other, ObjectId):
  218. return self.__id < other.binary
  219. return NotImplemented
  220. def __le__(self, other):
  221. if isinstance(other, ObjectId):
  222. return self.__id <= other.binary
  223. return NotImplemented
  224. def __gt__(self, other):
  225. if isinstance(other, ObjectId):
  226. return self.__id > other.binary
  227. return NotImplemented
  228. def __ge__(self, other):
  229. if isinstance(other, ObjectId):
  230. return self.__id >= other.binary
  231. return NotImplemented
  232. def __hash__(self):
  233. """Get a hash value for this :class:`ObjectId`."""
  234. return hash(self.__id)