objectid.py 9.2 KB

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