objectid.py 8.9 KB

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