Codebase list python-mockupdb / upstream/1.7.0
New upstream version 1.7.0 Ondřej Nový 5 years ago
29 changed file(s) with 337 addition(s) and 4233 deletion(s). Raw diff Collapse all Expand all
99 Contributors
1010 ------------
1111
12 None yet. Why not be the first?
12 * George Wilson
13 * Shane Harvey
11
22 Changelog
33 =========
4
5 1.7.0 (2018-12-02)
6 ------------------
7
8 Improve datetime support in match expressions. Python datetimes have microsecond
9 precision but BSON only has milliseconds, so expressions like this always
10 failed::
11
12 server.receives(Command('foo', when=datetime(2018, 12, 1, 6, 6, 6, 12345)))
13
14 Now, the matching logic has been rewritten to recurse through arrays and
15 subdocuments, comparing them value by value. It compares datetime values with
16 only millisecond precision.
17
18 1.6.0 (2018-11-16)
19 ------------------
20
21 Remove vendored BSON library. Instead, require PyMongo and use its BSON library.
22 This avoids surprising problems where a BSON type created with PyMongo does not
23 appear equal to one created with MockupDB, and it avoids the occasional need to
24 update the vendored code to support new BSON features.
25
26 1.5.0 (2018-11-02)
27 ------------------
28
29 Support for Unix domain paths with ``uds_path`` parameter.
30
31 The ``interactive_server()`` function now prepares the server to autorespond to
32 the ``getFreeMonitoringStatus`` command from the mongo shell.
433
534 1.4.1 (2018-06-30)
635 ------------------
00 Metadata-Version: 1.1
11 Name: mockupdb
2 Version: 1.4.1
2 Version: 1.7.0
33 Summary: MongoDB Wire Protocol server library
44 Home-page: https://github.com/ajdavis/mongo-mockup-db
55 Author: A. Jesse Jiryu Davis
2020
2121 Changelog
2222 =========
23
24 1.7.0 (2018-12-02)
25 ------------------
26
27 Improve datetime support in match expressions. Python datetimes have microsecond
28 precision but BSON only has milliseconds, so expressions like this always
29 failed::
30
31 server.receives(Command('foo', when=datetime(2018, 12, 1, 6, 6, 6, 12345)))
32
33 Now, the matching logic has been rewritten to recurse through arrays and
34 subdocuments, comparing them value by value. It compares datetime values with
35 only millisecond precision.
36
37 1.6.0 (2018-11-16)
38 ------------------
39
40 Remove vendored BSON library. Instead, require PyMongo and use its BSON library.
41 This avoids surprising problems where a BSON type created with PyMongo does not
42 appear equal to one created with MockupDB, and it avoids the occasional need to
43 update the vendored code to support new BSON features.
44
45 1.5.0 (2018-11-02)
46 ------------------
47
48 Support for Unix domain paths with ``uds_path`` parameter.
49
50 The ``interactive_server()`` function now prepares the server to autorespond to
51 the ``getFreeMonitoringStatus`` command from the mongo shell.
2352
2453 1.4.1 (2018-06-30)
2554 ------------------
107136
108137 Keywords: mongo,mongodb,wire protocol,mockupdb,mock
109138 Platform: UNKNOWN
110 Classifier: Development Status :: 2 - Pre-Alpha
139 Classifier: Development Status :: 5 - Production/Stable
111140 Classifier: Intended Audience :: Developers
112141 Classifier: License :: OSI Approved :: Apache Software License
113142 Classifier: Natural Language :: English
3333
3434 Image Credit: `gnuckx <https://www.flickr.com/photos/34409164@N06/4708707234/>`_
3535
36 .. _MongoDB Wire Protocol: http://docs.mongodb.org/meta-driver/latest/legacy/mongodb-wire-protocol/
36 .. _MongoDB Wire Protocol: https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/
518518
519519 .. _PyMongo: https://pypi.python.org/pypi/pymongo/
520520
521 .. _MongoDB Wire Protocol: http://docs.mongodb.org/meta-driver/latest/legacy/mongodb-wire-protocol/
521 .. _MongoDB Wire Protocol: https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/
522522
523523 .. _serverStatus: http://docs.mongodb.org/manual/reference/server-status/
524524
1818
1919 __author__ = 'A. Jesse Jiryu Davis'
2020 __email__ = 'jesse@mongodb.com'
21 __version__ = '1.4.1'
21 __version__ = '1.7.0'
2222
2323 import atexit
2424 import contextlib
25 import datetime
2526 import errno
2627 import functools
2728 import inspect
5859 except ImportError:
5960 from cStringIO import StringIO
6061
61 # Pure-Python bson lib vendored in from PyMongo 3.0.3.
62 from mockupdb import _bson
63 import mockupdb._bson.codec_options as _codec_options
64 import mockupdb._bson.json_util as _json_util
65
66 CODEC_OPTIONS = _codec_options.CodecOptions(document_class=OrderedDict)
62 try:
63 from urllib.parse import quote_plus
64 except ImportError:
65 # Python 2
66 from urllib import quote_plus
67
68 import bson
69 from bson import codec_options, json_util
70
71 CODEC_OPTIONS = codec_options.CodecOptions(document_class=OrderedDict)
6772
6873 PY3 = sys.version_info[0] == 3
6974 if PY3:
7075 string_type = str
7176 text_type = str
77
7278
7379 def reraise(exctype, value, trace=None):
7480 raise exctype(str(value)).with_traceback(trace)
7783 text_type = unicode
7884
7985 # "raise x, y, z" raises SyntaxError in Python 3.
80 exec("""def reraise(exctype, value, trace=None):
86 exec ("""def reraise(exctype, value, trace=None):
8187 raise exctype, str(value), trace
8288 """)
83
8489
8590 __all__ = [
8691 'MockupDB', 'go', 'going', 'Future', 'wait_until', 'interactive_server',
120125 'return value'
121126 """
122127 if not callable(fn):
123 raise TypeError('go() requires a function, not %r' % (fn, ))
128 raise TypeError('go() requires a function, not %r' % (fn,))
124129 result = [None]
125130 error = []
126131
283288 return _utf_8_decode(data[position:end], None, True)[0], end + 1
284289
285290
286 def _bson_values_equal(a, b):
287 # Check if values are either from our vendored bson or PyMongo's bson.
288 for value in (a, b):
289 if not hasattr(value, '_type_marker'):
290 # Normal equality.
291 return a == b
292
293 def marker(obj):
294 return getattr(obj, '_type_marker', None)
295
296 if marker(a) != marker(b):
297 return a == b
298
299 # Instances of Binary, ObjectId, etc. from our vendored bson don't equal
300 # instances of PyMongo's bson classes that users pass in as message specs,
301 # since isinstance() fails. Reimplement equality checks for each class.
302 key_fn = {
303 # Binary.
304 5: lambda obj: (obj.subtype, bytes(obj)),
305 # ObjectId.
306 7: lambda obj: obj.binary,
307 # Regex.
308 11: lambda obj: (obj.pattern, obj.flags),
309 # Code.
310 13: lambda obj: (obj.scope, str(obj)),
311 # Timestamp.
312 17: lambda obj: (obj.time, obj.inc),
313 # Decimal128.
314 19: lambda obj: obj.bid,
315 # DBRef.
316 100: lambda obj: (obj.database, obj.collection, obj.id),
317 # MaxKey.
318 127: lambda obj: 127,
319 # MinKey.
320 255: lambda obj: 255,
321 }.get(marker(a))
322
323 if key_fn:
324 return key_fn(a) == key_fn(b)
325
326 return a == b
327
328
329291 class _PeekableQueue(Queue):
330292 """Only safe from one consumer thread at a time."""
331293 _NO_ITEM = object()
348310 return item
349311 else:
350312 return Queue.get(self, block, timeout)
313
314
315 def _ismap(obj):
316 return isinstance(obj, Mapping)
317
318
319 def _islist(obj):
320 return isinstance(obj, list)
321
322
323 def _dt_rounded(dt):
324 """Python datetimes have microsecond precision, BSON only milliseconds."""
325 return dt.replace(microsecond=dt.microsecond - dt.microsecond % 1000)
351326
352327
353328 class Request(object):
386361 self._verbose = self._server and self._server.verbose
387362 self._server_port = kwargs.pop('server_port', None)
388363 self._docs = make_docs(*args, **kwargs)
389 if not all(isinstance(doc, Mapping) for doc in self._docs):
364 if not all(_ismap(doc) for doc in self._docs):
390365 raise_args_err()
391366
392367 @property
431406 @property
432407 def client_port(self):
433408 """Client connection's TCP port."""
434 return self._client.getpeername()[1]
409 address = self._client.getpeername()
410 if isinstance(address, tuple):
411 return address[1]
412
413 # Maybe a Unix domain socket connection.
414 return 0
435415
436416 @property
437417 def server(self):
504484
505485 def _matches_docs(self, docs, other_docs):
506486 """Overridable method."""
507 for i, doc in enumerate(docs):
508 other_doc = other_docs[i]
509 for key, value in doc.items():
510 if value is absent:
511 if key in other_doc:
512 return False
513 elif not _bson_values_equal(value, other_doc.get(key, None)):
487 for doc, other_doc in zip(docs, other_docs):
488 if not self._match_map(doc, other_doc):
489 return False
490
491 return True
492
493 def _match_map(self, doc, other_doc):
494 for key, val in doc.items():
495 if val is absent:
496 if key in other_doc:
514497 return False
515 if isinstance(doc, (OrderedDict, _bson.SON)):
516 if not isinstance(other_doc, (OrderedDict, _bson.SON)):
517 raise TypeError(
518 "Can't compare ordered and unordered document types:"
519 " %r, %r" % (doc, other_doc))
520 keys = [key for key, value in doc.items()
521 if value is not absent]
522 if not seq_match(keys, list(other_doc.keys())):
523 return False
498 elif not self._match_val(val, other_doc.get(key, None)):
499 return False
500
501 if isinstance(doc, (OrderedDict, bson.SON)):
502 if not isinstance(other_doc, (OrderedDict, bson.SON)):
503 raise TypeError(
504 "Can't compare ordered and unordered document types:"
505 " %r, %r" % (doc, other_doc))
506 keys = [key for key, val in doc.items()
507 if val is not absent]
508 if not seq_match(keys, list(other_doc.keys())):
509 return False
510
511 return True
512
513 def _match_list(self, lst, other_lst):
514 if len(lst) != len(other_lst):
515 return False
516
517 for val, other_val in zip(lst, other_lst):
518 if not self._match_val(val, other_val):
519 return False
520
521 return True
522
523 def _match_val(self, val, other_val):
524 if _ismap(val) and _ismap(other_val):
525 if not self._match_map(val, other_val):
526 return False
527 elif _islist(val) and _islist(other_val):
528 if not self._match_list(val, other_val):
529 return False
530 elif (isinstance(val, datetime.datetime)
531 and isinstance(other_val, datetime.datetime)):
532 if _dt_rounded(val) != _dt_rounded(other_val):
533 return False
534 elif val != other_val:
535 return False
536
524537 return True
525538
526539 def _replies(self, *args, **kwargs):
569582 is_command = True
570583
571584 # Check command name case-insensitively.
572 _non_matched_attrs = Request._non_matched_attrs + ('command_name', )
585 _non_matched_attrs = Request._non_matched_attrs + ('command_name',)
573586
574587 @property
575588 def command_name(self):
594607 if items and other_items:
595608 if items[0][0].lower() != other_items[0][0].lower():
596609 return False
597 if not _bson_values_equal(items[0][1], other_items[0][1]):
610 if items[0][1] != other_items[0][1]:
598611 return False
599612 return super(CommandBase, self)._matches_docs(
600613 [OrderedDict(items[1:])],
616629 """
617630 flags, = _UNPACK_UINT(msg[:4])
618631 pos = 4
619 first_payload_type, = _UNPACK_BYTE(msg[pos:pos+1])
632 first_payload_type, = _UNPACK_BYTE(msg[pos:pos + 1])
620633 pos += 1
621 first_payload_size, = _UNPACK_INT(msg[pos:pos+4])
634 first_payload_size, = _UNPACK_INT(msg[pos:pos + 4])
622635 if flags != 0 and flags != 2:
623636 raise ValueError('OP_MSG flag must be 0 or 2 not %r' % (flags,))
624637 if first_payload_type != 0:
626639 first_payload_type,))
627640
628641 # Parse the initial document and add the optional payload type 1.
629 payload_document = _bson.decode_all(msg[pos:pos+first_payload_size],
630 CODEC_OPTIONS)[0]
642 payload_document = bson.decode_all(msg[pos:pos + first_payload_size],
643 CODEC_OPTIONS)[0]
631644 pos += first_payload_size
632645 if len(msg) != pos:
633 payload_type, = _UNPACK_BYTE(msg[pos:pos+1])
646 payload_type, = _UNPACK_BYTE(msg[pos:pos + 1])
634647 pos += 1
635648 if payload_type != 1:
636649 raise ValueError('Second OP_MSG payload type must be 1 not %r'
637650 % (payload_type,))
638 section_size, = _UNPACK_INT(msg[pos:pos+4])
651 section_size, = _UNPACK_INT(msg[pos:pos + 4])
639652 if len(msg) != pos + section_size:
640653 raise ValueError('More than two OP_MSG sections unsupported')
641654 pos += 4
642655 identifier, pos = _get_c_string(msg, pos)
643 documents = _bson.decode_all(msg[pos:], CODEC_OPTIONS)
656 documents = bson.decode_all(msg[pos:], CODEC_OPTIONS)
644657 payload_document[identifier] = documents
645658
646659 database = payload_document['$db']
683696 else:
684697 if len(reply.docs) > 1:
685698 raise ValueError('OP_MSG reply with multiple documents: %s'
686 % (reply.docs, ))
699 % (reply.docs,))
687700 reply.doc.setdefault('ok', 1)
688701 super(OpMsg, self)._replies(reply)
689702
712725 pos += 4
713726 num_to_return, = _UNPACK_INT(msg[pos:pos + 4])
714727 pos += 4
715 docs = _bson.decode_all(msg[pos:], CODEC_OPTIONS)
728 docs = bson.decode_all(msg[pos:], CODEC_OPTIONS)
716729 if is_command:
717730 assert len(docs) == 1
718731 command_ns = namespace[:-len('.$cmd')]
732745
733746 def __init__(self, *args, **kwargs):
734747 fields = kwargs.pop('fields', None)
735 if fields is not None and not isinstance(fields, Mapping):
748 if fields is not None and not _ismap(fields):
736749 raise_args_err()
737750 self._fields = fields
738751 self._num_to_skip = kwargs.pop('num_to_skip', None)
779792 else:
780793 if len(reply.docs) > 1:
781794 raise ValueError('Command reply with multiple documents: %s'
782 % (reply.docs, ))
795 % (reply.docs,))
783796 reply.doc.setdefault('ok', 1)
784797 super(Command, self)._replies(reply)
785798
797810
798811 class OpGetMore(Request):
799812 """An OP_GET_MORE the client executes on the server."""
813
800814 @classmethod
801815 def unpack(cls, msg, client, server, request_id):
802816 """Parse message and return an `OpGetMore`.
831845
832846 class OpKillCursors(Request):
833847 """An OP_KILL_CURSORS the client executes on the server."""
848
834849 @classmethod
835850 def unpack(cls, msg, client, server, _):
836851 """Parse message and return an `OpKillCursors`.
843858 cursor_ids = []
844859 pos = 8
845860 for _ in range(num_of_cursor_ids):
846 cursor_ids.append(_UNPACK_INT(msg[pos:pos+4])[0])
861 cursor_ids.append(_UNPACK_INT(msg[pos:pos + 4])[0])
847862 pos += 4
848863 return OpKillCursors(_client=client, cursor_ids=cursor_ids,
849864 _server=server)
879894 """
880895 flags, = _UNPACK_INT(msg[:4])
881896 namespace, pos = _get_c_string(msg, 4)
882 docs = _bson.decode_all(msg[pos:], CODEC_OPTIONS)
897 docs = bson.decode_all(msg[pos:], CODEC_OPTIONS)
883898 return cls(*docs, namespace=namespace, flags=flags, _client=client,
884899 request_id=request_id, _server=server)
885900
899914 # First 4 bytes of OP_UPDATE are "reserved".
900915 namespace, pos = _get_c_string(msg, 4)
901916 flags, = _UNPACK_INT(msg[pos:pos + 4])
902 docs = _bson.decode_all(msg[pos + 4:], CODEC_OPTIONS)
917 docs = bson.decode_all(msg[pos + 4:], CODEC_OPTIONS)
903918 return cls(*docs, namespace=namespace, flags=flags, _client=client,
904919 request_id=request_id, _server=server)
905920
919934 # First 4 bytes of OP_DELETE are "reserved".
920935 namespace, pos = _get_c_string(msg, 4)
921936 flags, = _UNPACK_INT(msg[pos:pos + 4])
922 docs = _bson.decode_all(msg[pos + 4:], CODEC_OPTIONS)
937 docs = bson.decode_all(msg[pos + 4:], CODEC_OPTIONS)
923938 return cls(*docs, namespace=namespace, flags=flags, _client=client,
924939 request_id=request_id, _server=server)
925940
926941
927942 class Reply(object):
928943 """A reply from `MockupDB` to the client."""
944
929945 def __init__(self, *args, **kwargs):
930946 self._flags = kwargs.pop('flags', 0)
931947 self._docs = make_docs(*args, **kwargs)
955971
956972 class OpReply(Reply):
957973 """An OP_REPLY reply from `MockupDB` to the client."""
974
958975 def __init__(self, *args, **kwargs):
959976 self._cursor_id = kwargs.pop('cursor_id', 0)
960977 self._starting_from = kwargs.pop('starting_from', 0)
9901007 response_to = request.request_id
9911008
9921009 data = b''.join([flags, cursor_id, starting_from, number_returned])
993 data += b''.join([_bson.BSON.encode(doc) for doc in self._docs])
1010 data += b''.join([bson.BSON.encode(doc) for doc in self._docs])
9941011
9951012 message = struct.pack("<i", 16 + len(data))
9961013 message += struct.pack("<i", reply_id)
10011018
10021019 class OpMsgReply(Reply):
10031020 """A OP_MSG reply from `MockupDB` to the client."""
1021
10041022 def __init__(self, *args, **kwargs):
10051023 super(OpMsgReply, self).__init__(*args, **kwargs)
10061024 assert len(self._docs) <= 1, 'OpMsgReply can only have one document'
10301048 """Take a `Request` and return an OP_MSG message as bytes."""
10311049 flags = struct.pack("<I", self._flags)
10321050 payload_type = struct.pack("<b", 0)
1033 payload_data = _bson.BSON.encode(self.doc)
1051 payload_data = bson.BSON.encode(self.doc)
10341052 data = b''.join([flags, payload_type, payload_data])
10351053
10361054 reply_id = random.randint(0, 1000000)
10621080 and by `~MockupDB.got` to test if it did and return ``True`` or ``False``.
10631081 Used by `.autoresponds` to match requests with autoresponses.
10641082 """
1083
10651084 def __init__(self, *args, **kwargs):
10661085 self._kwargs = kwargs
10671086 self._prototype = make_prototype_request(*args, **kwargs)
11021121
11031122 def _synchronized(meth):
11041123 """Call method while holding a lock."""
1124
11051125 @functools.wraps(meth)
11061126 def wrapper(self, *args, **kwargs):
11071127 with self._lock:
11441164 # ourselves in __init__.
11451165 request.replies(*self._args, **self._kwargs)
11461166 return True
1147
1167
11481168 def cancel(self):
11491169 """Stop autoresponding."""
11501170 self._server.cancel_responder(self)
11931213 if `auto_ismaster` is True, default 0.
11941214 - `max_wire_version`: the maxWireVersion to include in ismaster responses
11951215 if `auto_ismaster` is True, default 6.
1216 - `uds_path`: a Unix domain socket path. MockupDB will attempt to delete
1217 the path if it already exists.
11961218 """
1219
11971220 def __init__(self, port=None, verbose=False,
11981221 request_timeout=10, auto_ismaster=None,
1199 ssl=False, min_wire_version=0, max_wire_version=6):
1200 self._address = ('localhost', port)
1222 ssl=False, min_wire_version=0, max_wire_version=6,
1223 uds_path=None):
1224 if port is not None and uds_path is not None:
1225 raise TypeError(
1226 ("You can't pass port=%s and uds_path=%s,"
1227 " pass only one or neither") % (port, uds_path))
1228
1229 self._uds_path = uds_path
1230 if uds_path:
1231 self._address = (uds_path, 0)
1232 else:
1233 self._address = ('localhost', port)
1234
12011235 self._verbose = verbose
12021236 self._label = None
12031237 self._ssl = ssl
12301264
12311265 @_synchronized
12321266 def run(self):
1233 """Begin serving. Returns the bound port."""
1234 self._listening_sock, self._address = bind_socket(self._address)
1267 """Begin serving. Returns the bound port, or 0 for domain socket."""
1268 self._listening_sock, self._address = (
1269 bind_domain_socket(self._address)
1270 if self._uds_path
1271 else bind_tcp_socket(self._address))
1272
12351273 if self._ssl:
12361274 certfile = os.path.join(os.path.dirname(__file__), 'server.pem')
12371275 self._listening_sock = _ssl.wrap_socket(
12641302 with self._unlock():
12651303 for thread in threads:
12661304 thread.join(10)
1305
1306 if self._uds_path:
1307 try:
1308 os.unlink(self._uds_path)
1309 except OSError:
1310 pass
12671311
12681312 def receives(self, *args, **kwargs):
12691313 """Pop the next `Request` and assert it matches.
14661510
14671511 subscribe = autoresponds
14681512 """Synonym for `.autoresponds`."""
1469
1513
14701514 @_synchronized
14711515 def cancel_responder(self, responder):
14721516 """Cancel a responder that was registered with `autoresponds`."""
14801524 @property
14811525 def address_string(self):
14821526 """The listening "host:port"."""
1483 return '%s:%d' % self._address
1527 return format_addr(self._address)
14841528
14851529 @property
14861530 def host(self):
14951539 @property
14961540 def uri(self):
14971541 """Connection string to pass to `~pymongo.mongo_client.MongoClient`."""
1498 assert self.host and self.port
1499 uri = 'mongodb://%s:%s' % self._address
1542 if self._uds_path:
1543 uri = 'mongodb://%s' % (quote_plus(self._uds_path),)
1544 else:
1545 uri = 'mongodb://%s' % (format_addr(self._address),)
15001546 return uri + '/?ssl=true' if self._ssl else uri
15011547
15021548 @property
15541600 if select.select([self._listening_sock.fileno()], [], [], 1):
15551601 client, client_addr = self._listening_sock.accept()
15561602 client.setblocking(True)
1557 self._log('connection from %s:%s' % client_addr)
1603 self._log('connection from %s' % format_addr(client_addr))
15581604 server_thread = threading.Thread(
15591605 target=functools.partial(
15601606 self._server_loop, client, client_addr))
15681614 server_thread.start()
15691615 except socket.error as error:
15701616 if error.errno not in (
1571 errno.EAGAIN, errno.EBADF, errno.EWOULDBLOCK):
1617 errno.EAGAIN, errno.EBADF, errno.EWOULDBLOCK):
15721618 raise
15731619 except select.error as error:
15741620 if error.args[0] == errno.EBADF:
16101656 traceback.print_exc()
16111657 break
16121658
1613 self._log('disconnected: %s:%d' % client_addr)
1659 self._log('disconnected: %s' % format_addr(client_addr))
16141660 client.close()
16151661
16161662 def _log(self, msg):
16411687 __next__ = next
16421688
16431689 def __repr__(self):
1690 if self._uds_path:
1691 return 'MockupDB(uds_path=%s)' % (self._uds_path,)
1692
16441693 return 'MockupDB(%s, %s)' % self._address
16451694
16461695
1647 def bind_socket(address):
1696 def format_addr(address):
1697 """Turn a TCP or Unix domain socket address into a string."""
1698 if isinstance(address, tuple):
1699 if address[1]:
1700 return '%s:%d' % address
1701 else:
1702 return address[0]
1703
1704 return address
1705
1706
1707 def bind_tcp_socket(address):
16481708 """Takes (host, port) and returns (socket_object, (host, port)).
16491709
16501710 If the passed-in port is None, bind an unused port and return it.
16661726 return sock, (host, bound_port)
16671727
16681728 raise socket.error('could not bind socket')
1729
1730
1731 def bind_domain_socket(address):
1732 """Takes (socket path, 0) and returns (socket_object, (path, 0))."""
1733 path, _ = address
1734 try:
1735 os.unlink(path)
1736 except OSError:
1737 pass
1738
1739 sock = socket.socket(socket.AF_UNIX)
1740 sock.bind(path)
1741 sock.listen(128)
1742 return sock, (path, 0)
16691743
16701744
16711745 OPCODES = {OP_MSG: OpMsg,
18481922 >>> print(docs_repr(OrderedDict([(u'ts', now)])))
18491923 {"ts": {"$date": 123456000}}
18501924 >>>
1851 >>> oid = _bson.ObjectId(b'123456781234567812345678')
1925 >>> oid = bson.ObjectId(b'123456781234567812345678')
18521926 >>> print(docs_repr(OrderedDict([(u'oid', oid)])))
18531927 {"oid": {"$oid": "123456781234567812345678"}}
18541928 """
18561930 for doc_idx, doc in enumerate(args):
18571931 if doc_idx > 0:
18581932 sio.write(u', ')
1859 sio.write(text_type(_json_util.dumps(doc)))
1933 sio.write(text_type(json_util.dumps(doc)))
18601934 return sio.getvalue()
18611935
18621936
19221996
19231997
19241998 def interactive_server(port=27017, verbose=True, all_ok=False, name='MockupDB',
1925 ssl=False):
1999 ssl=False, uds_path=None):
19262000 """A `MockupDB` that the mongo shell can connect to.
19272001
19282002 Call `~.MockupDB.run` on the returned server, and clean it up with
19312005 If ``all_ok`` is True, replies {ok: 1} to anything unmatched by a specific
19322006 responder.
19332007 """
2008 if uds_path is not None:
2009 port = None
2010
19342011 server = MockupDB(port=port,
19352012 verbose=verbose,
19362013 request_timeout=int(1e6),
19372014 ssl=ssl,
1938 auto_ismaster=True)
2015 auto_ismaster=True,
2016 uds_path=uds_path)
19392017 if all_ok:
19402018 server.autoresponds({})
19412019 server.autoresponds('whatsmyuri', you='localhost:12345')
19442022 server.autoresponds(OpMsg('buildInfo'), version='MockupDB ' + __version__)
19452023 server.autoresponds(OpMsg('listCollections'))
19462024 server.autoresponds('replSetGetStatus', ok=0)
2025 server.autoresponds('getFreeMonitoringStatus', ok=0)
19472026 return server
+0
-1078
mockupdb/_bson/__init__.py less more
0 # Copyright 2009-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """BSON (Binary JSON) encoding and decoding.
15
16 The mapping from Python types to BSON types is as follows:
17
18 ======================================= ============= ===================
19 Python Type BSON Type Supported Direction
20 ======================================= ============= ===================
21 None null both
22 bool boolean both
23 int [#int]_ int32 / int64 py -> bson
24 long int64 py -> bson
25 `bson.int64.Int64` int64 both
26 float number (real) both
27 string string py -> bson
28 unicode string both
29 list array both
30 dict / `SON` object both
31 datetime.datetime [#dt]_ [#dt2]_ date both
32 `bson.regex.Regex` regex both
33 compiled re [#re]_ regex py -> bson
34 `bson.binary.Binary` binary both
35 `bson.objectid.ObjectId` oid both
36 `bson.dbref.DBRef` dbref both
37 None undefined bson -> py
38 unicode code bson -> py
39 `bson.code.Code` code py -> bson
40 unicode symbol bson -> py
41 bytes (Python 3) [#bytes]_ binary both
42 ======================================= ============= ===================
43
44 Note that, when using Python 2.x, to save binary data it must be wrapped as
45 an instance of `bson.binary.Binary`. Otherwise it will be saved as a BSON
46 string and retrieved as unicode. Users of Python 3.x can use the Python bytes
47 type.
48
49 .. [#int] A Python int will be saved as a BSON int32 or BSON int64 depending
50 on its size. A BSON int32 will always decode to a Python int. A BSON
51 int64 will always decode to a :class:`~bson.int64.Int64`.
52 .. [#dt] datetime.datetime instances will be rounded to the nearest
53 millisecond when saved
54 .. [#dt2] all datetime.datetime instances are treated as *naive*. clients
55 should always use UTC.
56 .. [#re] :class:`~bson.regex.Regex` instances and regular expression
57 objects from ``re.compile()`` are both saved as BSON regular expressions.
58 BSON regular expressions are decoded as :class:`~bson.regex.Regex`
59 instances.
60 .. [#bytes] The bytes type from Python 3.x is encoded as BSON binary with
61 subtype 0. In Python 3.x it will be decoded back to bytes. In Python 2.x
62 it will be decoded to an instance of :class:`~bson.binary.Binary` with
63 subtype 0.
64 """
65
66 import calendar
67 import datetime
68 import itertools
69 import re
70 import struct
71 import sys
72 import uuid
73
74 from codecs import (utf_8_decode as _utf_8_decode,
75 utf_8_encode as _utf_8_encode)
76
77 from mockupdb._bson.binary import (Binary, OLD_UUID_SUBTYPE,
78 JAVA_LEGACY, CSHARP_LEGACY,
79 UUIDLegacy)
80 from mockupdb._bson.code import Code
81 from mockupdb._bson.codec_options import (
82 CodecOptions, DEFAULT_CODEC_OPTIONS, _raw_document_class)
83 from mockupdb._bson.dbref import DBRef
84 from mockupdb._bson.decimal128 import Decimal128
85 from mockupdb._bson.errors import (InvalidBSON,
86 InvalidDocument,
87 InvalidStringData)
88 from mockupdb._bson.int64 import Int64
89 from mockupdb._bson.max_key import MaxKey
90 from mockupdb._bson.min_key import MinKey
91 from mockupdb._bson.objectid import ObjectId
92 from mockupdb._bson.py3compat import (abc,
93 b,
94 PY3,
95 iteritems,
96 text_type,
97 string_type,
98 reraise)
99 from mockupdb._bson.regex import Regex
100 from mockupdb._bson.son import SON, RE_TYPE
101 from mockupdb._bson.timestamp import Timestamp
102 from mockupdb._bson.tz_util import utc
103
104
105 try:
106 from mockupdb._bson import _cbson
107 _USE_C = True
108 except ImportError:
109 _USE_C = False
110
111
112 EPOCH_AWARE = datetime.datetime.fromtimestamp(0, utc)
113 EPOCH_NAIVE = datetime.datetime.utcfromtimestamp(0)
114
115
116 BSONNUM = b"\x01" # Floating point
117 BSONSTR = b"\x02" # UTF-8 string
118 BSONOBJ = b"\x03" # Embedded document
119 BSONARR = b"\x04" # Array
120 BSONBIN = b"\x05" # Binary
121 BSONUND = b"\x06" # Undefined
122 BSONOID = b"\x07" # ObjectId
123 BSONBOO = b"\x08" # Boolean
124 BSONDAT = b"\x09" # UTC Datetime
125 BSONNUL = b"\x0A" # Null
126 BSONRGX = b"\x0B" # Regex
127 BSONREF = b"\x0C" # DBRef
128 BSONCOD = b"\x0D" # Javascript code
129 BSONSYM = b"\x0E" # Symbol
130 BSONCWS = b"\x0F" # Javascript code with scope
131 BSONINT = b"\x10" # 32bit int
132 BSONTIM = b"\x11" # Timestamp
133 BSONLON = b"\x12" # 64bit int
134 BSONDEC = b"\x13" # Decimal128
135 BSONMIN = b"\xFF" # Min key
136 BSONMAX = b"\x7F" # Max key
137
138
139 _UNPACK_FLOAT = struct.Struct("<d").unpack
140 _UNPACK_INT = struct.Struct("<i").unpack
141 _UNPACK_LENGTH_SUBTYPE = struct.Struct("<iB").unpack
142 _UNPACK_LONG = struct.Struct("<q").unpack
143 _UNPACK_TIMESTAMP = struct.Struct("<II").unpack
144
145
146 def _raise_unknown_type(element_type, element_name):
147 """Unknown type helper."""
148 raise InvalidBSON("Detected unknown BSON type %r for fieldname '%s'. Are "
149 "you using the latest driver version?" % (
150 element_type, element_name))
151
152
153 def _get_int(data, position, dummy0, dummy1, dummy2):
154 """Decode a BSON int32 to python int."""
155 end = position + 4
156 return _UNPACK_INT(data[position:end])[0], end
157
158
159 def _get_c_string(data, position, opts):
160 """Decode a BSON 'C' string to python unicode string."""
161 end = data.index(b"\x00", position)
162 return _utf_8_decode(data[position:end],
163 opts.unicode_decode_error_handler, True)[0], end + 1
164
165
166 def _get_float(data, position, dummy0, dummy1, dummy2):
167 """Decode a BSON double to python float."""
168 end = position + 8
169 return _UNPACK_FLOAT(data[position:end])[0], end
170
171
172 def _get_string(data, position, obj_end, opts, dummy):
173 """Decode a BSON string to python unicode string."""
174 length = _UNPACK_INT(data[position:position + 4])[0]
175 position += 4
176 if length < 1 or obj_end - position < length:
177 raise InvalidBSON("invalid string length")
178 end = position + length - 1
179 if data[end:end + 1] != b"\x00":
180 raise InvalidBSON("invalid end of string")
181 return _utf_8_decode(data[position:end],
182 opts.unicode_decode_error_handler, True)[0], end + 1
183
184
185 def _get_object(data, position, obj_end, opts, dummy):
186 """Decode a BSON subdocument to opts.document_class or bson.dbref.DBRef."""
187 obj_size = _UNPACK_INT(data[position:position + 4])[0]
188 end = position + obj_size - 1
189 if data[end:position + obj_size] != b"\x00":
190 raise InvalidBSON("bad eoo")
191 if end >= obj_end:
192 raise InvalidBSON("invalid object length")
193 if _raw_document_class(opts.document_class):
194 return (opts.document_class(data[position:end + 1], opts),
195 position + obj_size)
196
197 obj = _elements_to_dict(data, position + 4, end, opts)
198
199 position += obj_size
200 if "$ref" in obj:
201 return (DBRef(obj.pop("$ref"), obj.pop("$id", None),
202 obj.pop("$db", None), obj), position)
203 return obj, position
204
205
206 def _get_array(data, position, obj_end, opts, element_name):
207 """Decode a BSON array to python list."""
208 size = _UNPACK_INT(data[position:position + 4])[0]
209 end = position + size - 1
210 if data[end:end + 1] != b"\x00":
211 raise InvalidBSON("bad eoo")
212
213 position += 4
214 end -= 1
215 result = []
216
217 # Avoid doing global and attibute lookups in the loop.
218 append = result.append
219 index = data.index
220 getter = _ELEMENT_GETTER
221
222 while position < end:
223 element_type = data[position:position + 1]
224 # Just skip the keys.
225 position = index(b'\x00', position) + 1
226 try:
227 value, position = getter[element_type](
228 data, position, obj_end, opts, element_name)
229 except KeyError:
230 _raise_unknown_type(element_type, element_name)
231 append(value)
232
233 if position != end + 1:
234 raise InvalidBSON('bad array length')
235 return result, position + 1
236
237
238 def _get_binary(data, position, obj_end, opts, dummy1):
239 """Decode a BSON binary to bson.binary.Binary or python UUID."""
240 length, subtype = _UNPACK_LENGTH_SUBTYPE(data[position:position + 5])
241 position += 5
242 if subtype == 2:
243 length2 = _UNPACK_INT(data[position:position + 4])[0]
244 position += 4
245 if length2 != length - 4:
246 raise InvalidBSON("invalid binary (st 2) - lengths don't match!")
247 length = length2
248 end = position + length
249 if length < 0 or end > obj_end:
250 raise InvalidBSON('bad binary object length')
251 if subtype == 3:
252 # Java Legacy
253 uuid_representation = opts.uuid_representation
254 if uuid_representation == JAVA_LEGACY:
255 java = data[position:end]
256 value = uuid.UUID(bytes=java[0:8][::-1] + java[8:16][::-1])
257 # C# legacy
258 elif uuid_representation == CSHARP_LEGACY:
259 value = uuid.UUID(bytes_le=data[position:end])
260 # Python
261 else:
262 value = uuid.UUID(bytes=data[position:end])
263 return value, end
264 if subtype == 4:
265 return uuid.UUID(bytes=data[position:end]), end
266 # Python3 special case. Decode subtype 0 to 'bytes'.
267 if PY3 and subtype == 0:
268 value = data[position:end]
269 else:
270 value = Binary(data[position:end], subtype)
271 return value, end
272
273
274 def _get_oid(data, position, dummy0, dummy1, dummy2):
275 """Decode a BSON ObjectId to bson.objectid.ObjectId."""
276 end = position + 12
277 return ObjectId(data[position:end]), end
278
279
280 def _get_boolean(data, position, dummy0, dummy1, dummy2):
281 """Decode a BSON true/false to python True/False."""
282 end = position + 1
283 boolean_byte = data[position:end]
284 if boolean_byte == b'\x00':
285 return False, end
286 elif boolean_byte == b'\x01':
287 return True, end
288 raise InvalidBSON('invalid boolean value: %r' % boolean_byte)
289
290
291 def _get_date(data, position, dummy0, opts, dummy1):
292 """Decode a BSON datetime to python datetime.datetime."""
293 end = position + 8
294 millis = _UNPACK_LONG(data[position:end])[0]
295 return _millis_to_datetime(millis, opts), end
296
297
298 def _get_code(data, position, obj_end, opts, element_name):
299 """Decode a BSON code to bson.code.Code."""
300 code, position = _get_string(data, position, obj_end, opts, element_name)
301 return Code(code), position
302
303
304 def _get_code_w_scope(data, position, obj_end, opts, element_name):
305 """Decode a BSON code_w_scope to bson.code.Code."""
306 code_end = position + _UNPACK_INT(data[position:position + 4])[0]
307 code, position = _get_string(
308 data, position + 4, code_end, opts, element_name)
309 scope, position = _get_object(data, position, code_end, opts, element_name)
310 if position != code_end:
311 raise InvalidBSON('scope outside of javascript code boundaries')
312 return Code(code, scope), position
313
314
315 def _get_regex(data, position, dummy0, opts, dummy1):
316 """Decode a BSON regex to bson.regex.Regex or a python pattern object."""
317 pattern, position = _get_c_string(data, position, opts)
318 bson_flags, position = _get_c_string(data, position, opts)
319 bson_re = Regex(pattern, bson_flags)
320 return bson_re, position
321
322
323 def _get_ref(data, position, obj_end, opts, element_name):
324 """Decode (deprecated) BSON DBPointer to bson.dbref.DBRef."""
325 collection, position = _get_string(
326 data, position, obj_end, opts, element_name)
327 oid, position = _get_oid(data, position, obj_end, opts, element_name)
328 return DBRef(collection, oid), position
329
330
331 def _get_timestamp(data, position, dummy0, dummy1, dummy2):
332 """Decode a BSON timestamp to bson.timestamp.Timestamp."""
333 end = position + 8
334 inc, timestamp = _UNPACK_TIMESTAMP(data[position:end])
335 return Timestamp(timestamp, inc), end
336
337
338 def _get_int64(data, position, dummy0, dummy1, dummy2):
339 """Decode a BSON int64 to bson.int64.Int64."""
340 end = position + 8
341 return Int64(_UNPACK_LONG(data[position:end])[0]), end
342
343
344 def _get_decimal128(data, position, dummy0, dummy1, dummy2):
345 """Decode a BSON decimal128 to bson.decimal128.Decimal128."""
346 end = position + 16
347 return Decimal128.from_bid(data[position:end]), end
348
349
350 # Each decoder function's signature is:
351 # - data: bytes
352 # - position: int, beginning of object in 'data' to decode
353 # - obj_end: int, end of object to decode in 'data' if variable-length type
354 # - opts: a CodecOptions
355 _ELEMENT_GETTER = {
356 BSONNUM: _get_float,
357 BSONSTR: _get_string,
358 BSONOBJ: _get_object,
359 BSONARR: _get_array,
360 BSONBIN: _get_binary,
361 BSONUND: lambda v, w, x, y, z: (None, w), # Deprecated undefined
362 BSONOID: _get_oid,
363 BSONBOO: _get_boolean,
364 BSONDAT: _get_date,
365 BSONNUL: lambda v, w, x, y, z: (None, w),
366 BSONRGX: _get_regex,
367 BSONREF: _get_ref, # Deprecated DBPointer
368 BSONCOD: _get_code,
369 BSONSYM: _get_string, # Deprecated symbol
370 BSONCWS: _get_code_w_scope,
371 BSONINT: _get_int,
372 BSONTIM: _get_timestamp,
373 BSONLON: _get_int64,
374 BSONDEC: _get_decimal128,
375 BSONMIN: lambda v, w, x, y, z: (MinKey(), w),
376 BSONMAX: lambda v, w, x, y, z: (MaxKey(), w)}
377
378
379 def _element_to_dict(data, position, obj_end, opts):
380 """Decode a single key, value pair."""
381 element_type = data[position:position + 1]
382 position += 1
383 element_name, position = _get_c_string(data, position, opts)
384 try:
385 value, position = _ELEMENT_GETTER[element_type](data, position,
386 obj_end, opts,
387 element_name)
388 except KeyError:
389 _raise_unknown_type(element_type, element_name)
390 return element_name, value, position
391 if _USE_C:
392 _element_to_dict = _cbson._element_to_dict
393
394
395 def _iterate_elements(data, position, obj_end, opts):
396 end = obj_end - 1
397 while position < end:
398 (key, value, position) = _element_to_dict(data, position, obj_end, opts)
399 yield key, value, position
400
401
402 def _elements_to_dict(data, position, obj_end, opts):
403 """Decode a BSON document."""
404 result = opts.document_class()
405 pos = position
406 for key, value, pos in _iterate_elements(data, position, obj_end, opts):
407 result[key] = value
408 if pos != obj_end:
409 raise InvalidBSON('bad object or element length')
410 return result
411
412
413 def _bson_to_dict(data, opts):
414 """Decode a BSON string to document_class."""
415 try:
416 obj_size = _UNPACK_INT(data[:4])[0]
417 except struct.error as exc:
418 raise InvalidBSON(str(exc))
419 if obj_size != len(data):
420 raise InvalidBSON("invalid object size")
421 if data[obj_size - 1:obj_size] != b"\x00":
422 raise InvalidBSON("bad eoo")
423 try:
424 if _raw_document_class(opts.document_class):
425 return opts.document_class(data, opts)
426 return _elements_to_dict(data, 4, obj_size - 1, opts)
427 except InvalidBSON:
428 raise
429 except Exception:
430 # Change exception type to InvalidBSON but preserve traceback.
431 _, exc_value, exc_tb = sys.exc_info()
432 reraise(InvalidBSON, exc_value, exc_tb)
433 if _USE_C:
434 _bson_to_dict = _cbson._bson_to_dict
435
436
437 _PACK_FLOAT = struct.Struct("<d").pack
438 _PACK_INT = struct.Struct("<i").pack
439 _PACK_LENGTH_SUBTYPE = struct.Struct("<iB").pack
440 _PACK_LONG = struct.Struct("<q").pack
441 _PACK_TIMESTAMP = struct.Struct("<II").pack
442 _LIST_NAMES = tuple(b(str(i)) + b"\x00" for i in range(1000))
443
444
445 def gen_list_name():
446 """Generate "keys" for encoded lists in the sequence
447 b"0\x00", b"1\x00", b"2\x00", ...
448
449 The first 1000 keys are returned from a pre-built cache. All
450 subsequent keys are generated on the fly.
451 """
452 for name in _LIST_NAMES:
453 yield name
454
455 counter = itertools.count(1000)
456 while True:
457 yield b(str(next(counter))) + b"\x00"
458
459
460 def _make_c_string_check(string):
461 """Make a 'C' string, checking for embedded NUL characters."""
462 if isinstance(string, bytes):
463 if b"\x00" in string:
464 raise InvalidDocument("BSON keys / regex patterns must not "
465 "contain a NUL character")
466 try:
467 _utf_8_decode(string, None, True)
468 return string + b"\x00"
469 except UnicodeError:
470 raise InvalidStringData("strings in documents must be valid "
471 "UTF-8: %r" % string)
472 else:
473 if "\x00" in string:
474 raise InvalidDocument("BSON keys / regex patterns must not "
475 "contain a NUL character")
476 return _utf_8_encode(string)[0] + b"\x00"
477
478
479 def _make_c_string(string):
480 """Make a 'C' string."""
481 if isinstance(string, bytes):
482 try:
483 _utf_8_decode(string, None, True)
484 return string + b"\x00"
485 except UnicodeError:
486 raise InvalidStringData("strings in documents must be valid "
487 "UTF-8: %r" % string)
488 else:
489 return _utf_8_encode(string)[0] + b"\x00"
490
491
492 if PY3:
493 def _make_name(string):
494 """Make a 'C' string suitable for a BSON key."""
495 # Keys can only be text in python 3.
496 if "\x00" in string:
497 raise InvalidDocument("BSON keys / regex patterns must not "
498 "contain a NUL character")
499 return _utf_8_encode(string)[0] + b"\x00"
500 else:
501 # Keys can be unicode or bytes in python 2.
502 _make_name = _make_c_string_check
503
504
505 def _encode_float(name, value, dummy0, dummy1):
506 """Encode a float."""
507 return b"\x01" + name + _PACK_FLOAT(value)
508
509
510 if PY3:
511 def _encode_bytes(name, value, dummy0, dummy1):
512 """Encode a python bytes."""
513 # Python3 special case. Store 'bytes' as BSON binary subtype 0.
514 return b"\x05" + name + _PACK_INT(len(value)) + b"\x00" + value
515 else:
516 def _encode_bytes(name, value, dummy0, dummy1):
517 """Encode a python str (python 2.x)."""
518 try:
519 _utf_8_decode(value, None, True)
520 except UnicodeError:
521 raise InvalidStringData("strings in documents must be valid "
522 "UTF-8: %r" % (value,))
523 return b"\x02" + name + _PACK_INT(len(value) + 1) + value + b"\x00"
524
525
526 def _encode_mapping(name, value, check_keys, opts):
527 """Encode a mapping type."""
528 if _raw_document_class(value):
529 return b'\x03' + name + value.raw
530 data = b"".join([_element_to_bson(key, val, check_keys, opts)
531 for key, val in iteritems(value)])
532 return b"\x03" + name + _PACK_INT(len(data) + 5) + data + b"\x00"
533
534
535 def _encode_dbref(name, value, check_keys, opts):
536 """Encode bson.dbref.DBRef."""
537 buf = bytearray(b"\x03" + name + b"\x00\x00\x00\x00")
538 begin = len(buf) - 4
539
540 buf += _name_value_to_bson(b"$ref\x00",
541 value.collection, check_keys, opts)
542 buf += _name_value_to_bson(b"$id\x00",
543 value.id, check_keys, opts)
544 if value.database is not None:
545 buf += _name_value_to_bson(
546 b"$db\x00", value.database, check_keys, opts)
547 for key, val in iteritems(value._DBRef__kwargs):
548 buf += _element_to_bson(key, val, check_keys, opts)
549
550 buf += b"\x00"
551 buf[begin:begin + 4] = _PACK_INT(len(buf) - begin)
552 return bytes(buf)
553
554
555 def _encode_list(name, value, check_keys, opts):
556 """Encode a list/tuple."""
557 lname = gen_list_name()
558 data = b"".join([_name_value_to_bson(next(lname), item,
559 check_keys, opts)
560 for item in value])
561 return b"\x04" + name + _PACK_INT(len(data) + 5) + data + b"\x00"
562
563
564 def _encode_text(name, value, dummy0, dummy1):
565 """Encode a python unicode (python 2.x) / str (python 3.x)."""
566 value = _utf_8_encode(value)[0]
567 return b"\x02" + name + _PACK_INT(len(value) + 1) + value + b"\x00"
568
569
570 def _encode_binary(name, value, dummy0, dummy1):
571 """Encode bson.binary.Binary."""
572 subtype = value.subtype
573 if subtype == 2:
574 value = _PACK_INT(len(value)) + value
575 return b"\x05" + name + _PACK_LENGTH_SUBTYPE(len(value), subtype) + value
576
577
578 def _encode_uuid(name, value, dummy, opts):
579 """Encode uuid.UUID."""
580 uuid_representation = opts.uuid_representation
581 # Python Legacy Common Case
582 if uuid_representation == OLD_UUID_SUBTYPE:
583 return b"\x05" + name + b'\x10\x00\x00\x00\x03' + value.bytes
584 # Java Legacy
585 elif uuid_representation == JAVA_LEGACY:
586 from_uuid = value.bytes
587 data = from_uuid[0:8][::-1] + from_uuid[8:16][::-1]
588 return b"\x05" + name + b'\x10\x00\x00\x00\x03' + data
589 # C# legacy
590 elif uuid_representation == CSHARP_LEGACY:
591 # Microsoft GUID representation.
592 return b"\x05" + name + b'\x10\x00\x00\x00\x03' + value.bytes_le
593 # New
594 else:
595 return b"\x05" + name + b'\x10\x00\x00\x00\x04' + value.bytes
596
597
598 def _encode_objectid(name, value, dummy0, dummy1):
599 """Encode bson.objectid.ObjectId."""
600 return b"\x07" + name + value.binary
601
602
603 def _encode_bool(name, value, dummy0, dummy1):
604 """Encode a python boolean (True/False)."""
605 return b"\x08" + name + (value and b"\x01" or b"\x00")
606
607
608 def _encode_datetime(name, value, dummy0, dummy1):
609 """Encode datetime.datetime."""
610 millis = _datetime_to_millis(value)
611 return b"\x09" + name + _PACK_LONG(millis)
612
613
614 def _encode_none(name, dummy0, dummy1, dummy2):
615 """Encode python None."""
616 return b"\x0A" + name
617
618
619 def _encode_regex(name, value, dummy0, dummy1):
620 """Encode a python regex or bson.regex.Regex."""
621 flags = value.flags
622 # Python 2 common case
623 if flags == 0:
624 return b"\x0B" + name + _make_c_string_check(value.pattern) + b"\x00"
625 # Python 3 common case
626 elif flags == re.UNICODE:
627 return b"\x0B" + name + _make_c_string_check(value.pattern) + b"u\x00"
628 else:
629 sflags = b""
630 if flags & re.IGNORECASE:
631 sflags += b"i"
632 if flags & re.LOCALE:
633 sflags += b"l"
634 if flags & re.MULTILINE:
635 sflags += b"m"
636 if flags & re.DOTALL:
637 sflags += b"s"
638 if flags & re.UNICODE:
639 sflags += b"u"
640 if flags & re.VERBOSE:
641 sflags += b"x"
642 sflags += b"\x00"
643 return b"\x0B" + name + _make_c_string_check(value.pattern) + sflags
644
645
646 def _encode_code(name, value, dummy, opts):
647 """Encode bson.code.Code."""
648 cstring = _make_c_string(value)
649 cstrlen = len(cstring)
650 if value.scope is None:
651 return b"\x0D" + name + _PACK_INT(cstrlen) + cstring
652 scope = _dict_to_bson(value.scope, False, opts, False)
653 full_length = _PACK_INT(8 + cstrlen + len(scope))
654 return b"\x0F" + name + full_length + _PACK_INT(cstrlen) + cstring + scope
655
656
657 def _encode_int(name, value, dummy0, dummy1):
658 """Encode a python int."""
659 if -2147483648 <= value <= 2147483647:
660 return b"\x10" + name + _PACK_INT(value)
661 else:
662 try:
663 return b"\x12" + name + _PACK_LONG(value)
664 except struct.error:
665 raise OverflowError("BSON can only handle up to 8-byte ints")
666
667
668 def _encode_timestamp(name, value, dummy0, dummy1):
669 """Encode bson.timestamp.Timestamp."""
670 return b"\x11" + name + _PACK_TIMESTAMP(value.inc, value.time)
671
672
673 def _encode_long(name, value, dummy0, dummy1):
674 """Encode a python long (python 2.x)"""
675 try:
676 return b"\x12" + name + _PACK_LONG(value)
677 except struct.error:
678 raise OverflowError("BSON can only handle up to 8-byte ints")
679
680
681 def _encode_decimal128(name, value, dummy0, dummy1):
682 """Encode bson.decimal128.Decimal128."""
683 return b"\x13" + name + value.bid
684
685
686 def _encode_minkey(name, dummy0, dummy1, dummy2):
687 """Encode bson.min_key.MinKey."""
688 return b"\xFF" + name
689
690
691 def _encode_maxkey(name, dummy0, dummy1, dummy2):
692 """Encode bson.max_key.MaxKey."""
693 return b"\x7F" + name
694
695
696 # Each encoder function's signature is:
697 # - name: utf-8 bytes
698 # - value: a Python data type, e.g. a Python int for _encode_int
699 # - check_keys: bool, whether to check for invalid names
700 # - opts: a CodecOptions
701 _ENCODERS = {
702 bool: _encode_bool,
703 bytes: _encode_bytes,
704 datetime.datetime: _encode_datetime,
705 dict: _encode_mapping,
706 float: _encode_float,
707 int: _encode_int,
708 list: _encode_list,
709 # unicode in py2, str in py3
710 text_type: _encode_text,
711 tuple: _encode_list,
712 type(None): _encode_none,
713 uuid.UUID: _encode_uuid,
714 Binary: _encode_binary,
715 Int64: _encode_long,
716 Code: _encode_code,
717 DBRef: _encode_dbref,
718 MaxKey: _encode_maxkey,
719 MinKey: _encode_minkey,
720 ObjectId: _encode_objectid,
721 Regex: _encode_regex,
722 RE_TYPE: _encode_regex,
723 SON: _encode_mapping,
724 Timestamp: _encode_timestamp,
725 UUIDLegacy: _encode_binary,
726 Decimal128: _encode_decimal128,
727 # Special case. This will never be looked up directly.
728 abc.Mapping: _encode_mapping,
729 }
730
731
732 _MARKERS = {
733 5: _encode_binary,
734 7: _encode_objectid,
735 11: _encode_regex,
736 13: _encode_code,
737 17: _encode_timestamp,
738 18: _encode_long,
739 100: _encode_dbref,
740 127: _encode_maxkey,
741 255: _encode_minkey,
742 }
743
744 if not PY3:
745 _ENCODERS[long] = _encode_long
746
747
748 def _name_value_to_bson(name, value, check_keys, opts):
749 """Encode a single name, value pair."""
750
751 # First see if the type is already cached. KeyError will only ever
752 # happen once per subtype.
753 try:
754 return _ENCODERS[type(value)](name, value, check_keys, opts)
755 except KeyError:
756 pass
757
758 # Second, fall back to trying _type_marker. This has to be done
759 # before the loop below since users could subclass one of our
760 # custom types that subclasses a python built-in (e.g. Binary)
761 marker = getattr(value, "_type_marker", None)
762 if isinstance(marker, int) and marker in _MARKERS:
763 func = _MARKERS[marker]
764 # Cache this type for faster subsequent lookup.
765 _ENCODERS[type(value)] = func
766 return func(name, value, check_keys, opts)
767
768 # If all else fails test each base type. This will only happen once for
769 # a subtype of a supported base type.
770 for base in _ENCODERS:
771 if isinstance(value, base):
772 func = _ENCODERS[base]
773 # Cache this type for faster subsequent lookup.
774 _ENCODERS[type(value)] = func
775 return func(name, value, check_keys, opts)
776
777 raise InvalidDocument("cannot convert value of type %s to bson" %
778 type(value))
779
780
781 def _element_to_bson(key, value, check_keys, opts):
782 """Encode a single key, value pair."""
783 if not isinstance(key, string_type):
784 raise InvalidDocument("documents must have only string keys, "
785 "key was %r" % (key,))
786 if check_keys:
787 if key.startswith("$"):
788 raise InvalidDocument("key %r must not start with '$'" % (key,))
789 if "." in key:
790 raise InvalidDocument("key %r must not contain '.'" % (key,))
791
792 name = _make_name(key)
793 return _name_value_to_bson(name, value, check_keys, opts)
794
795
796 def _dict_to_bson(doc, check_keys, opts, top_level=True):
797 """Encode a document to BSON."""
798 if _raw_document_class(doc):
799 return doc.raw
800 try:
801 elements = []
802 if top_level and "_id" in doc:
803 elements.append(_name_value_to_bson(b"_id\x00", doc["_id"],
804 check_keys, opts))
805 for (key, value) in iteritems(doc):
806 if not top_level or key != "_id":
807 elements.append(_element_to_bson(key, value,
808 check_keys, opts))
809 except AttributeError:
810 raise TypeError("encoder expected a mapping type but got: %r" % (doc,))
811
812 encoded = b"".join(elements)
813 return _PACK_INT(len(encoded) + 5) + encoded + b"\x00"
814 if _USE_C:
815 _dict_to_bson = _cbson._dict_to_bson
816
817
818 def _millis_to_datetime(millis, opts):
819 """Convert milliseconds since epoch UTC to datetime."""
820 diff = ((millis % 1000) + 1000) % 1000
821 seconds = (millis - diff) / 1000
822 micros = diff * 1000
823 if opts.tz_aware:
824 dt = EPOCH_AWARE + datetime.timedelta(seconds=seconds,
825 microseconds=micros)
826 if opts.tzinfo:
827 dt = dt.astimezone(opts.tzinfo)
828 return dt
829 else:
830 return EPOCH_NAIVE + datetime.timedelta(seconds=seconds,
831 microseconds=micros)
832
833
834 def _datetime_to_millis(dtm):
835 """Convert datetime to milliseconds since epoch UTC."""
836 if dtm.utcoffset() is not None:
837 dtm = dtm - dtm.utcoffset()
838 return int(calendar.timegm(dtm.timetuple()) * 1000 +
839 dtm.microsecond / 1000)
840
841
842 _CODEC_OPTIONS_TYPE_ERROR = TypeError(
843 "codec_options must be an instance of CodecOptions")
844
845
846 def decode_all(data, codec_options=DEFAULT_CODEC_OPTIONS):
847 """Decode BSON data to multiple documents.
848
849 `data` must be a string of concatenated, valid, BSON-encoded
850 documents.
851
852 :Parameters:
853 - `data`: BSON data
854 - `codec_options` (optional): An instance of
855 :class:`~bson.codec_options.CodecOptions`.
856
857 .. versionchanged:: 3.0
858 Removed `compile_re` option: PyMongo now always represents BSON regular
859 expressions as :class:`~bson.regex.Regex` objects. Use
860 :meth:`~bson.regex.Regex.try_compile` to attempt to convert from a
861 BSON regular expression to a Python regular expression object.
862
863 Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with
864 `codec_options`.
865
866 .. versionchanged:: 2.7
867 Added `compile_re` option. If set to False, PyMongo represented BSON
868 regular expressions as :class:`~bson.regex.Regex` objects instead of
869 attempting to compile BSON regular expressions as Python native
870 regular expressions, thus preventing errors for some incompatible
871 patterns, see `PYTHON-500`_.
872
873 .. _PYTHON-500: https://jira.mongodb.org/browse/PYTHON-500
874 """
875 if not isinstance(codec_options, CodecOptions):
876 raise _CODEC_OPTIONS_TYPE_ERROR
877
878 docs = []
879 position = 0
880 end = len(data) - 1
881 use_raw = _raw_document_class(codec_options.document_class)
882 try:
883 while position < end:
884 obj_size = _UNPACK_INT(data[position:position + 4])[0]
885 if len(data) - position < obj_size:
886 raise InvalidBSON("invalid object size")
887 obj_end = position + obj_size - 1
888 if data[obj_end:position + obj_size] != b"\x00":
889 raise InvalidBSON("bad eoo")
890 if use_raw:
891 docs.append(
892 codec_options.document_class(
893 data[position:obj_end + 1], codec_options))
894 else:
895 docs.append(_elements_to_dict(data,
896 position + 4,
897 obj_end,
898 codec_options))
899 position += obj_size
900 return docs
901 except InvalidBSON:
902 raise
903 except Exception:
904 # Change exception type to InvalidBSON but preserve traceback.
905 _, exc_value, exc_tb = sys.exc_info()
906 reraise(InvalidBSON, exc_value, exc_tb)
907
908
909 if _USE_C:
910 decode_all = _cbson.decode_all
911
912
913 def decode_iter(data, codec_options=DEFAULT_CODEC_OPTIONS):
914 """Decode BSON data to multiple documents as a generator.
915
916 Works similarly to the decode_all function, but yields one document at a
917 time.
918
919 `data` must be a string of concatenated, valid, BSON-encoded
920 documents.
921
922 :Parameters:
923 - `data`: BSON data
924 - `codec_options` (optional): An instance of
925 :class:`~bson.codec_options.CodecOptions`.
926
927 .. versionchanged:: 3.0
928 Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with
929 `codec_options`.
930
931 .. versionadded:: 2.8
932 """
933 if not isinstance(codec_options, CodecOptions):
934 raise _CODEC_OPTIONS_TYPE_ERROR
935
936 position = 0
937 end = len(data) - 1
938 while position < end:
939 obj_size = _UNPACK_INT(data[position:position + 4])[0]
940 elements = data[position:position + obj_size]
941 position += obj_size
942
943 yield _bson_to_dict(elements, codec_options)
944
945
946 def decode_file_iter(file_obj, codec_options=DEFAULT_CODEC_OPTIONS):
947 """Decode bson data from a file to multiple documents as a generator.
948
949 Works similarly to the decode_all function, but reads from the file object
950 in chunks and parses bson in chunks, yielding one document at a time.
951
952 :Parameters:
953 - `file_obj`: A file object containing BSON data.
954 - `codec_options` (optional): An instance of
955 :class:`~bson.codec_options.CodecOptions`.
956
957 .. versionchanged:: 3.0
958 Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with
959 `codec_options`.
960
961 .. versionadded:: 2.8
962 """
963 while True:
964 # Read size of next object.
965 size_data = file_obj.read(4)
966 if len(size_data) == 0:
967 break # Finished with file normaly.
968 elif len(size_data) != 4:
969 raise InvalidBSON("cut off in middle of objsize")
970 obj_size = _UNPACK_INT(size_data)[0] - 4
971 elements = size_data + file_obj.read(obj_size)
972 yield _bson_to_dict(elements, codec_options)
973
974
975 def is_valid(bson):
976 """Check that the given string represents valid :class:`BSON` data.
977
978 Raises :class:`TypeError` if `bson` is not an instance of
979 :class:`str` (:class:`bytes` in python 3). Returns ``True``
980 if `bson` is valid :class:`BSON`, ``False`` otherwise.
981
982 :Parameters:
983 - `bson`: the data to be validated
984 """
985 if not isinstance(bson, bytes):
986 raise TypeError("BSON data must be an instance of a subclass of bytes")
987
988 try:
989 _bson_to_dict(bson, DEFAULT_CODEC_OPTIONS)
990 return True
991 except Exception:
992 return False
993
994
995 class BSON(bytes):
996 """BSON (Binary JSON) data.
997 """
998
999 @classmethod
1000 def encode(cls, document, check_keys=False,
1001 codec_options=DEFAULT_CODEC_OPTIONS):
1002 """Encode a document to a new :class:`BSON` instance.
1003
1004 A document can be any mapping type (like :class:`dict`).
1005
1006 Raises :class:`TypeError` if `document` is not a mapping type,
1007 or contains keys that are not instances of
1008 :class:`basestring` (:class:`str` in python 3). Raises
1009 :class:`~bson.errors.InvalidDocument` if `document` cannot be
1010 converted to :class:`BSON`.
1011
1012 :Parameters:
1013 - `document`: mapping type representing a document
1014 - `check_keys` (optional): check if keys start with '$' or
1015 contain '.', raising :class:`~bson.errors.InvalidDocument` in
1016 either case
1017 - `codec_options` (optional): An instance of
1018 :class:`~bson.codec_options.CodecOptions`.
1019
1020 .. versionchanged:: 3.0
1021 Replaced `uuid_subtype` option with `codec_options`.
1022 """
1023 if not isinstance(codec_options, CodecOptions):
1024 raise _CODEC_OPTIONS_TYPE_ERROR
1025
1026 return cls(_dict_to_bson(document, check_keys, codec_options))
1027
1028 def decode(self, codec_options=DEFAULT_CODEC_OPTIONS):
1029 """Decode this BSON data.
1030
1031 By default, returns a BSON document represented as a Python
1032 :class:`dict`. To use a different :class:`MutableMapping` class,
1033 configure a :class:`~bson.codec_options.CodecOptions`::
1034
1035 >>> import collections # From Python standard library.
1036 >>> import bson
1037 >>> from mockupdb._bson.codec_options import CodecOptions
1038 >>> data = bson.BSON.encode({'a': 1})
1039 >>> decoded_doc = bson.BSON.decode(data)
1040 <type 'dict'>
1041 >>> options = CodecOptions(document_class=collections.OrderedDict)
1042 >>> decoded_doc = bson.BSON.decode(data, codec_options=options)
1043 >>> type(decoded_doc)
1044 <class 'collections.OrderedDict'>
1045
1046 :Parameters:
1047 - `codec_options` (optional): An instance of
1048 :class:`~bson.codec_options.CodecOptions`.
1049
1050 .. versionchanged:: 3.0
1051 Removed `compile_re` option: PyMongo now always represents BSON
1052 regular expressions as :class:`~bson.regex.Regex` objects. Use
1053 :meth:`~bson.regex.Regex.try_compile` to attempt to convert from a
1054 BSON regular expression to a Python regular expression object.
1055
1056 Replaced `as_class`, `tz_aware`, and `uuid_subtype` options with
1057 `codec_options`.
1058
1059 .. versionchanged:: 2.7
1060 Added `compile_re` option. If set to False, PyMongo represented BSON
1061 regular expressions as :class:`~bson.regex.Regex` objects instead of
1062 attempting to compile BSON regular expressions as Python native
1063 regular expressions, thus preventing errors for some incompatible
1064 patterns, see `PYTHON-500`_.
1065
1066 .. _PYTHON-500: https://jira.mongodb.org/browse/PYTHON-500
1067 """
1068 if not isinstance(codec_options, CodecOptions):
1069 raise _CODEC_OPTIONS_TYPE_ERROR
1070
1071 return _bson_to_dict(self, codec_options)
1072
1073
1074 def has_c():
1075 """Is the C extension installed?
1076 """
1077 return _USE_C
+0
-243
mockupdb/_bson/binary.py less more
0 # Copyright 2009-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 from uuid import UUID
15
16 from mockupdb._bson.py3compat import PY3
17
18 """Tools for representing BSON binary data.
19 """
20
21 BINARY_SUBTYPE = 0
22 """BSON binary subtype for binary data.
23
24 This is the default subtype for binary data.
25 """
26
27 FUNCTION_SUBTYPE = 1
28 """BSON binary subtype for functions.
29 """
30
31 OLD_BINARY_SUBTYPE = 2
32 """Old BSON binary subtype for binary data.
33
34 This is the old default subtype, the current
35 default is :data:`BINARY_SUBTYPE`.
36 """
37
38 OLD_UUID_SUBTYPE = 3
39 """Old BSON binary subtype for a UUID.
40
41 :class:`uuid.UUID` instances will automatically be encoded
42 by :mod:`bson` using this subtype.
43
44 .. versionadded:: 2.1
45 """
46
47 UUID_SUBTYPE = 4
48 """BSON binary subtype for a UUID.
49
50 This is the new BSON binary subtype for UUIDs. The
51 current default is :data:`OLD_UUID_SUBTYPE` but will
52 change to this in a future release.
53
54 .. versionchanged:: 2.1
55 Changed to subtype 4.
56 """
57
58 STANDARD = UUID_SUBTYPE
59 """The standard UUID representation.
60
61 :class:`uuid.UUID` instances will automatically be encoded to
62 and decoded from mockupdb._bson binary, using RFC-4122 byte order with
63 binary subtype :data:`UUID_SUBTYPE`.
64
65 .. versionadded:: 3.0
66 """
67
68 PYTHON_LEGACY = OLD_UUID_SUBTYPE
69 """The Python legacy UUID representation.
70
71 :class:`uuid.UUID` instances will automatically be encoded to
72 and decoded from mockupdb._bson binary, using RFC-4122 byte order with
73 binary subtype :data:`OLD_UUID_SUBTYPE`.
74
75 .. versionadded:: 3.0
76 """
77
78 JAVA_LEGACY = 5
79 """The Java legacy UUID representation.
80
81 :class:`uuid.UUID` instances will automatically be encoded to
82 and decoded from mockupdb._bson binary subtype :data:`OLD_UUID_SUBTYPE`,
83 using the Java driver's legacy byte order.
84
85 .. versionchanged:: 3.6
86 BSON binary subtype 4 is decoded using RFC-4122 byte order.
87 .. versionadded:: 2.3
88 """
89
90 CSHARP_LEGACY = 6
91 """The C#/.net legacy UUID representation.
92
93 :class:`uuid.UUID` instances will automatically be encoded to
94 and decoded from mockupdb._bson binary subtype :data:`OLD_UUID_SUBTYPE`,
95 using the C# driver's legacy byte order.
96
97 .. versionchanged:: 3.6
98 BSON binary subtype 4 is decoded using RFC-4122 byte order.
99 .. versionadded:: 2.3
100 """
101
102 ALL_UUID_SUBTYPES = (OLD_UUID_SUBTYPE, UUID_SUBTYPE)
103 ALL_UUID_REPRESENTATIONS = (STANDARD, PYTHON_LEGACY, JAVA_LEGACY, CSHARP_LEGACY)
104 UUID_REPRESENTATION_NAMES = {
105 PYTHON_LEGACY: 'PYTHON_LEGACY',
106 STANDARD: 'STANDARD',
107 JAVA_LEGACY: 'JAVA_LEGACY',
108 CSHARP_LEGACY: 'CSHARP_LEGACY'}
109
110 MD5_SUBTYPE = 5
111 """BSON binary subtype for an MD5 hash.
112 """
113
114 USER_DEFINED_SUBTYPE = 128
115 """BSON binary subtype for any user defined structure.
116 """
117
118
119 class Binary(bytes):
120 """Representation of BSON binary data.
121
122 This is necessary because we want to represent Python strings as
123 the BSON string type. We need to wrap binary data so we can tell
124 the difference between what should be considered binary data and
125 what should be considered a string when we encode to BSON.
126
127 Raises TypeError if `data` is not an instance of :class:`str`
128 (:class:`bytes` in python 3) or `subtype` is not an instance of
129 :class:`int`. Raises ValueError if `subtype` is not in [0, 256).
130
131 .. note::
132 In python 3 instances of Binary with subtype 0 will be decoded
133 directly to :class:`bytes`.
134
135 :Parameters:
136 - `data`: the binary data to represent
137 - `subtype` (optional): the `binary subtype
138 <http://bsonspec.org/#/specification>`_
139 to use
140 """
141
142 _type_marker = 5
143
144 def __new__(cls, data, subtype=BINARY_SUBTYPE):
145 if not isinstance(data, bytes):
146 raise TypeError("data must be an instance of bytes")
147 if not isinstance(subtype, int):
148 raise TypeError("subtype must be an instance of int")
149 if subtype >= 256 or subtype < 0:
150 raise ValueError("subtype must be contained in [0, 256)")
151 self = bytes.__new__(cls, data)
152 self.__subtype = subtype
153 return self
154
155 @property
156 def subtype(self):
157 """Subtype of this binary data.
158 """
159 return self.__subtype
160
161 def __getnewargs__(self):
162 # Work around http://bugs.python.org/issue7382
163 data = super(Binary, self).__getnewargs__()[0]
164 if PY3 and not isinstance(data, bytes):
165 data = data.encode('latin-1')
166 return data, self.__subtype
167
168 def __eq__(self, other):
169 if isinstance(other, Binary):
170 return ((self.__subtype, bytes(self)) ==
171 (other.subtype, bytes(other)))
172 # We don't return NotImplemented here because if we did then
173 # Binary("foo") == "foo" would return True, since Binary is a
174 # subclass of str...
175 return False
176
177 def __hash__(self):
178 return super(Binary, self).__hash__() ^ hash(self.__subtype)
179
180 def __ne__(self, other):
181 return not self == other
182
183 def __repr__(self):
184 return "Binary(%s, %s)" % (bytes.__repr__(self), self.__subtype)
185
186
187 class UUIDLegacy(Binary):
188 """UUID wrapper to support working with UUIDs stored as PYTHON_LEGACY.
189
190 .. doctest::
191
192 >>> import uuid
193 >>> from mockupdb._bson.binary import Binary, UUIDLegacy, STANDARD
194 >>> from mockupdb._bson.codec_options import CodecOptions
195 >>> my_uuid = uuid.uuid4()
196 >>> coll = db.get_collection('test',
197 ... CodecOptions(uuid_representation=STANDARD))
198 >>> coll.insert_one({'uuid': Binary(my_uuid.bytes, 3)}).inserted_id
199 ObjectId('...')
200 >>> coll.find({'uuid': my_uuid}).count()
201 0
202 >>> coll.find({'uuid': UUIDLegacy(my_uuid)}).count()
203 1
204 >>> coll.find({'uuid': UUIDLegacy(my_uuid)})[0]['uuid']
205 UUID('...')
206 >>>
207 >>> # Convert from subtype 3 to subtype 4
208 >>> doc = coll.find_one({'uuid': UUIDLegacy(my_uuid)})
209 >>> coll.replace_one({"_id": doc["_id"]}, doc).matched_count
210 1
211 >>> coll.find({'uuid': UUIDLegacy(my_uuid)}).count()
212 0
213 >>> coll.find({'uuid': {'$in': [UUIDLegacy(my_uuid), my_uuid]}}).count()
214 1
215 >>> coll.find_one({'uuid': my_uuid})['uuid']
216 UUID('...')
217
218 Raises TypeError if `obj` is not an instance of :class:`~uuid.UUID`.
219
220 :Parameters:
221 - `obj`: An instance of :class:`~uuid.UUID`.
222 """
223
224 def __new__(cls, obj):
225 if not isinstance(obj, UUID):
226 raise TypeError("obj must be an instance of uuid.UUID")
227 self = Binary.__new__(cls, obj.bytes, OLD_UUID_SUBTYPE)
228 self.__uuid = obj
229 return self
230
231 def __getnewargs__(self):
232 # Support copy and deepcopy
233 return (self.__uuid,)
234
235 @property
236 def uuid(self):
237 """UUID instance wrapped by this UUIDLegacy instance.
238 """
239 return self.__uuid
240
241 def __repr__(self):
242 return "UUIDLegacy('%s')" % self.__uuid
+0
-99
mockupdb/_bson/code.py less more
0 # Copyright 2009-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for representing JavaScript code in BSON.
15 """
16
17 from mockupdb._bson.py3compat import abc, string_type, PY3, text_type
18
19
20 class Code(str):
21 """BSON's JavaScript code type.
22
23 Raises :class:`TypeError` if `code` is not an instance of
24 :class:`basestring` (:class:`str` in python 3) or `scope`
25 is not ``None`` or an instance of :class:`dict`.
26
27 Scope variables can be set by passing a dictionary as the `scope`
28 argument or by using keyword arguments. If a variable is set as a
29 keyword argument it will override any setting for that variable in
30 the `scope` dictionary.
31
32 :Parameters:
33 - `code`: A string containing JavaScript code to be evaluated or another
34 instance of Code. In the latter case, the scope of `code` becomes this
35 Code's :attr:`scope`.
36 - `scope` (optional): dictionary representing the scope in which
37 `code` should be evaluated - a mapping from identifiers (as
38 strings) to values. Defaults to ``None``. This is applied after any
39 scope associated with a given `code` above.
40 - `**kwargs` (optional): scope variables can also be passed as
41 keyword arguments. These are applied after `scope` and `code`.
42
43 .. versionchanged:: 3.4
44 The default value for :attr:`scope` is ``None`` instead of ``{}``.
45
46 """
47
48 _type_marker = 13
49
50 def __new__(cls, code, scope=None, **kwargs):
51 if not isinstance(code, string_type):
52 raise TypeError("code must be an "
53 "instance of %s" % (string_type.__name__))
54
55 if not PY3 and isinstance(code, text_type):
56 self = str.__new__(cls, code.encode('utf8'))
57 else:
58 self = str.__new__(cls, code)
59
60 try:
61 self.__scope = code.scope
62 except AttributeError:
63 self.__scope = None
64
65 if scope is not None:
66 if not isinstance(scope, abc.Mapping):
67 raise TypeError("scope must be an instance of dict")
68 if self.__scope is not None:
69 self.__scope.update(scope)
70 else:
71 self.__scope = scope
72
73 if kwargs:
74 if self.__scope is not None:
75 self.__scope.update(kwargs)
76 else:
77 self.__scope = kwargs
78
79 return self
80
81 @property
82 def scope(self):
83 """Scope dictionary for this instance or ``None``.
84 """
85 return self.__scope
86
87 def __repr__(self):
88 return "Code(%s, %r)" % (str.__repr__(self), self.__scope)
89
90 def __eq__(self, other):
91 if isinstance(other, Code):
92 return (self.__scope, str(self)) == (other.__scope, str(other))
93 return False
94
95 __hash__ = None
96
97 def __ne__(self, other):
98 return not self == other
+0
-150
mockupdb/_bson/codec_options.py less more
0 # Copyright 2014-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for specifying BSON codec options."""
15
16 import datetime
17
18 from collections import namedtuple
19
20 from mockupdb._bson.py3compat import abc, string_type
21 from mockupdb._bson.binary import (ALL_UUID_REPRESENTATIONS,
22 PYTHON_LEGACY,
23 UUID_REPRESENTATION_NAMES)
24
25 _RAW_BSON_DOCUMENT_MARKER = 101
26
27
28 def _raw_document_class(document_class):
29 """Determine if a document_class is a RawBSONDocument class."""
30 marker = getattr(document_class, '_type_marker', None)
31 return marker == _RAW_BSON_DOCUMENT_MARKER
32
33
34 _options_base = namedtuple(
35 'CodecOptions',
36 ('document_class', 'tz_aware', 'uuid_representation',
37 'unicode_decode_error_handler', 'tzinfo'))
38
39
40 class CodecOptions(_options_base):
41 """Encapsulates BSON options used in CRUD operations.
42
43 :Parameters:
44 - `document_class`: BSON documents returned in queries will be decoded
45 to an instance of this class. Must be a subclass of
46 :class:`~collections.MutableMapping`. Defaults to :class:`dict`.
47 - `tz_aware`: If ``True``, BSON datetimes will be decoded to timezone
48 aware instances of :class:`~datetime.datetime`. Otherwise they will be
49 naive. Defaults to ``False``.
50 - `uuid_representation`: The BSON representation to use when encoding
51 and decoding instances of :class:`~uuid.UUID`. Defaults to
52 :data:`~bson.binary.PYTHON_LEGACY`.
53 - `unicode_decode_error_handler`: The error handler to use when decoding
54 an invalid BSON string. Valid options include 'strict', 'replace', and
55 'ignore'. Defaults to 'strict'.
56 - `tzinfo`: A :class:`~datetime.tzinfo` subclass that specifies the
57 timezone to/from which :class:`~datetime.datetime` objects should be
58 encoded/decoded.
59
60 .. warning:: Care must be taken when changing
61 `unicode_decode_error_handler` from its default value ('strict').
62 The 'replace' and 'ignore' modes should not be used when documents
63 retrieved from the server will be modified in the client application
64 and stored back to the server.
65 """
66
67 def __new__(cls, document_class=dict,
68 tz_aware=False, uuid_representation=PYTHON_LEGACY,
69 unicode_decode_error_handler="strict",
70 tzinfo=None):
71 if not (issubclass(document_class, abc.MutableMapping) or
72 _raw_document_class(document_class)):
73 raise TypeError("document_class must be dict, bson.son.SON, "
74 "bson.raw_bson.RawBSONDocument, or a "
75 "sublass of collections.MutableMapping")
76 if not isinstance(tz_aware, bool):
77 raise TypeError("tz_aware must be True or False")
78 if uuid_representation not in ALL_UUID_REPRESENTATIONS:
79 raise ValueError("uuid_representation must be a value "
80 "from mockupdb._bson.binary.ALL_UUID_REPRESENTATIONS")
81 if not isinstance(unicode_decode_error_handler, (string_type, None)):
82 raise ValueError("unicode_decode_error_handler must be a string "
83 "or None")
84 if tzinfo is not None:
85 if not isinstance(tzinfo, datetime.tzinfo):
86 raise TypeError(
87 "tzinfo must be an instance of datetime.tzinfo")
88 if not tz_aware:
89 raise ValueError(
90 "cannot specify tzinfo without also setting tz_aware=True")
91
92 return tuple.__new__(
93 cls, (document_class, tz_aware, uuid_representation,
94 unicode_decode_error_handler, tzinfo))
95
96 def _arguments_repr(self):
97 """Representation of the arguments used to create this object."""
98 document_class_repr = (
99 'dict' if self.document_class is dict
100 else repr(self.document_class))
101
102 uuid_rep_repr = UUID_REPRESENTATION_NAMES.get(self.uuid_representation,
103 self.uuid_representation)
104
105 return ('document_class=%s, tz_aware=%r, uuid_representation='
106 '%s, unicode_decode_error_handler=%r, tzinfo=%r' %
107 (document_class_repr, self.tz_aware, uuid_rep_repr,
108 self.unicode_decode_error_handler, self.tzinfo))
109
110 def __repr__(self):
111 return '%s(%s)' % (self.__class__.__name__, self._arguments_repr())
112
113 def with_options(self, **kwargs):
114 """Make a copy of this CodecOptions, overriding some options::
115
116 >>> from mockupdb._bson.codec_options import DEFAULT_CODEC_OPTIONS
117 >>> DEFAULT_CODEC_OPTIONS.tz_aware
118 False
119 >>> options = DEFAULT_CODEC_OPTIONS.with_options(tz_aware=True)
120 >>> options.tz_aware
121 True
122
123 .. versionadded:: 3.5
124 """
125 return CodecOptions(
126 kwargs.get('document_class', self.document_class),
127 kwargs.get('tz_aware', self.tz_aware),
128 kwargs.get('uuid_representation', self.uuid_representation),
129 kwargs.get('unicode_decode_error_handler',
130 self.unicode_decode_error_handler),
131 kwargs.get('tzinfo', self.tzinfo))
132
133
134 DEFAULT_CODEC_OPTIONS = CodecOptions()
135
136
137 def _parse_codec_options(options):
138 """Parse BSON codec options."""
139 return CodecOptions(
140 document_class=options.get(
141 'document_class', DEFAULT_CODEC_OPTIONS.document_class),
142 tz_aware=options.get(
143 'tz_aware', DEFAULT_CODEC_OPTIONS.tz_aware),
144 uuid_representation=options.get(
145 'uuidrepresentation', DEFAULT_CODEC_OPTIONS.uuid_representation),
146 unicode_decode_error_handler=options.get(
147 'unicode_decode_error_handler',
148 DEFAULT_CODEC_OPTIONS.unicode_decode_error_handler),
149 tzinfo=options.get('tzinfo', DEFAULT_CODEC_OPTIONS.tzinfo))
+0
-135
mockupdb/_bson/dbref.py less more
0 # Copyright 2009-2015 MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for manipulating DBRefs (references to MongoDB documents)."""
15
16 from copy import deepcopy
17
18 from mockupdb._bson.py3compat import iteritems, string_type
19 from mockupdb._bson.son import SON
20
21
22 class DBRef(object):
23 """A reference to a document stored in MongoDB.
24 """
25
26 # DBRef isn't actually a BSON "type" so this number was arbitrarily chosen.
27 _type_marker = 100
28
29 def __init__(self, collection, id, database=None, _extra={}, **kwargs):
30 """Initialize a new :class:`DBRef`.
31
32 Raises :class:`TypeError` if `collection` or `database` is not
33 an instance of :class:`basestring` (:class:`str` in python 3).
34 `database` is optional and allows references to documents to work
35 across databases. Any additional keyword arguments will create
36 additional fields in the resultant embedded document.
37
38 :Parameters:
39 - `collection`: name of the collection the document is stored in
40 - `id`: the value of the document's ``"_id"`` field
41 - `database` (optional): name of the database to reference
42 - `**kwargs` (optional): additional keyword arguments will
43 create additional, custom fields
44
45 .. mongodoc:: dbrefs
46 """
47 if not isinstance(collection, string_type):
48 raise TypeError("collection must be an "
49 "instance of %s" % string_type.__name__)
50 if database is not None and not isinstance(database, string_type):
51 raise TypeError("database must be an "
52 "instance of %s" % string_type.__name__)
53
54 self.__collection = collection
55 self.__id = id
56 self.__database = database
57 kwargs.update(_extra)
58 self.__kwargs = kwargs
59
60 @property
61 def collection(self):
62 """Get the name of this DBRef's collection as unicode.
63 """
64 return self.__collection
65
66 @property
67 def id(self):
68 """Get this DBRef's _id.
69 """
70 return self.__id
71
72 @property
73 def database(self):
74 """Get the name of this DBRef's database.
75
76 Returns None if this DBRef doesn't specify a database.
77 """
78 return self.__database
79
80 def __getattr__(self, key):
81 try:
82 return self.__kwargs[key]
83 except KeyError:
84 raise AttributeError(key)
85
86 # Have to provide __setstate__ to avoid
87 # infinite recursion since we override
88 # __getattr__.
89 def __setstate__(self, state):
90 self.__dict__.update(state)
91
92 def as_doc(self):
93 """Get the SON document representation of this DBRef.
94
95 Generally not needed by application developers
96 """
97 doc = SON([("$ref", self.collection),
98 ("$id", self.id)])
99 if self.database is not None:
100 doc["$db"] = self.database
101 doc.update(self.__kwargs)
102 return doc
103
104 def __repr__(self):
105 extra = "".join([", %s=%r" % (k, v)
106 for k, v in iteritems(self.__kwargs)])
107 if self.database is None:
108 return "DBRef(%r, %r%s)" % (self.collection, self.id, extra)
109 return "DBRef(%r, %r, %r%s)" % (self.collection, self.id,
110 self.database, extra)
111
112 def __eq__(self, other):
113 if isinstance(other, DBRef):
114 us = (self.__database, self.__collection,
115 self.__id, self.__kwargs)
116 them = (other.__database, other.__collection,
117 other.__id, other.__kwargs)
118 return us == them
119 return NotImplemented
120
121 def __ne__(self, other):
122 return not self == other
123
124 def __hash__(self):
125 """Get a hash value for this :class:`DBRef`."""
126 return hash((self.__collection, self.__id, self.__database,
127 tuple(sorted(self.__kwargs.items()))))
128
129 def __deepcopy__(self, memo):
130 """Support function for `copy.deepcopy()`."""
131 return DBRef(deepcopy(self.__collection, memo),
132 deepcopy(self.__id, memo),
133 deepcopy(self.__database, memo),
134 deepcopy(self.__kwargs, memo))
+0
-351
mockupdb/_bson/decimal128.py less more
0 # Copyright 2016-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for working with the BSON decimal128 type.
15
16 .. versionadded:: 3.4
17
18 .. note:: The Decimal128 BSON type requires MongoDB 3.4+.
19 """
20
21 import decimal
22 import struct
23 import sys
24
25 from mockupdb._bson.py3compat import (PY3 as _PY3,
26 string_type as _string_type)
27
28
29 if _PY3:
30 _from_bytes = int.from_bytes # pylint: disable=no-member, invalid-name
31 else:
32 import binascii
33 def _from_bytes(value, dummy, _int=int, _hexlify=binascii.hexlify):
34 "An implementation of int.from_bytes for python 2.x."
35 return _int(_hexlify(value), 16)
36
37 if sys.version_info[:2] == (2, 6):
38 def _bit_length(num):
39 """bit_length for python 2.6"""
40 if num:
41 # bin() was new in 2.6. Note that this won't work
42 # for values less than 0, which we never have here.
43 return len(bin(num)) - 2
44 # bit_length(0) is 0, but len(bin(0)) - 2 is 1
45 return 0
46 else:
47 def _bit_length(num):
48 """bit_length for python >= 2.7"""
49 # num could be int or long in python 2.7
50 return num.bit_length()
51
52
53 _PACK_64 = struct.Struct("<Q").pack
54 _UNPACK_64 = struct.Struct("<Q").unpack
55
56 _EXPONENT_MASK = 3 << 61
57 _EXPONENT_BIAS = 6176
58 _EXPONENT_MAX = 6144
59 _EXPONENT_MIN = -6143
60 _MAX_DIGITS = 34
61
62 _INF = 0x7800000000000000
63 _NAN = 0x7c00000000000000
64 _SNAN = 0x7e00000000000000
65 _SIGN = 0x8000000000000000
66
67 _NINF = (_INF + _SIGN, 0)
68 _PINF = (_INF, 0)
69 _NNAN = (_NAN + _SIGN, 0)
70 _PNAN = (_NAN, 0)
71 _NSNAN = (_SNAN + _SIGN, 0)
72 _PSNAN = (_SNAN, 0)
73
74 _CTX_OPTIONS = {
75 'prec': _MAX_DIGITS,
76 'rounding': decimal.ROUND_HALF_EVEN,
77 'Emin': _EXPONENT_MIN,
78 'Emax': _EXPONENT_MAX,
79 'capitals': 1,
80 'flags': [],
81 'traps': [decimal.InvalidOperation,
82 decimal.Overflow,
83 decimal.Inexact]
84 }
85
86 try:
87 # Python >= 3.3, cdecimal
88 decimal.Context(clamp=1) # pylint: disable=unexpected-keyword-arg
89 _CTX_OPTIONS['clamp'] = 1
90 except TypeError:
91 # Python < 3.3
92 _CTX_OPTIONS['_clamp'] = 1
93
94 _DEC128_CTX = decimal.Context(**_CTX_OPTIONS.copy())
95
96
97 def create_decimal128_context():
98 """Returns an instance of :class:`decimal.Context` appropriate
99 for working with IEEE-754 128-bit decimal floating point values.
100 """
101 opts = _CTX_OPTIONS.copy()
102 opts['traps'] = []
103 return decimal.Context(**opts)
104
105
106 def _decimal_to_128(value):
107 """Converts a decimal.Decimal to BID (high bits, low bits).
108
109 :Parameters:
110 - `value`: An instance of decimal.Decimal
111 """
112 with decimal.localcontext(_DEC128_CTX) as ctx:
113 value = ctx.create_decimal(value)
114
115 if value.is_infinite():
116 return _NINF if value.is_signed() else _PINF
117
118 sign, digits, exponent = value.as_tuple()
119
120 if value.is_nan():
121 if digits:
122 raise ValueError("NaN with debug payload is not supported")
123 if value.is_snan():
124 return _NSNAN if value.is_signed() else _PSNAN
125 return _NNAN if value.is_signed() else _PNAN
126
127 significand = int("".join([str(digit) for digit in digits]))
128 bit_length = _bit_length(significand)
129
130 high = 0
131 low = 0
132 for i in range(min(64, bit_length)):
133 if significand & (1 << i):
134 low |= 1 << i
135
136 for i in range(64, bit_length):
137 if significand & (1 << i):
138 high |= 1 << (i - 64)
139
140 biased_exponent = exponent + _EXPONENT_BIAS
141
142 if high >> 49 == 1:
143 high = high & 0x7fffffffffff
144 high |= _EXPONENT_MASK
145 high |= (biased_exponent & 0x3fff) << 47
146 else:
147 high |= biased_exponent << 49
148
149 if sign:
150 high |= _SIGN
151
152 return high, low
153
154
155 class Decimal128(object):
156 """BSON Decimal128 type::
157
158 >>> Decimal128(Decimal("0.0005"))
159 Decimal128('0.0005')
160 >>> Decimal128("0.0005")
161 Decimal128('0.0005')
162 >>> Decimal128((3474527112516337664, 5))
163 Decimal128('0.0005')
164
165 :Parameters:
166 - `value`: An instance of :class:`decimal.Decimal`, string, or tuple of
167 (high bits, low bits) from Binary Integer Decimal (BID) format.
168
169 .. note:: :class:`~Decimal128` uses an instance of :class:`decimal.Context`
170 configured for IEEE-754 Decimal128 when validating parameters.
171 Signals like :class:`decimal.InvalidOperation`, :class:`decimal.Inexact`,
172 and :class:`decimal.Overflow` are trapped and raised as exceptions::
173
174 >>> Decimal128(".13.1")
175 Traceback (most recent call last):
176 File "<stdin>", line 1, in <module>
177 ...
178 decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
179 >>>
180 >>> Decimal128("1E-6177")
181 Traceback (most recent call last):
182 File "<stdin>", line 1, in <module>
183 ...
184 decimal.Inexact: [<class 'decimal.Inexact'>]
185 >>>
186 >>> Decimal128("1E6145")
187 Traceback (most recent call last):
188 File "<stdin>", line 1, in <module>
189 ...
190 decimal.Overflow: [<class 'decimal.Overflow'>, <class 'decimal.Rounded'>]
191
192 To ensure the result of a calculation can always be stored as BSON
193 Decimal128 use the context returned by
194 :func:`create_decimal128_context`::
195
196 >>> import decimal
197 >>> decimal128_ctx = create_decimal128_context()
198 >>> with decimal.localcontext(decimal128_ctx) as ctx:
199 ... Decimal128(ctx.create_decimal(".13.3"))
200 ...
201 Decimal128('NaN')
202 >>>
203 >>> with decimal.localcontext(decimal128_ctx) as ctx:
204 ... Decimal128(ctx.create_decimal("1E-6177"))
205 ...
206 Decimal128('0E-6176')
207 >>>
208 >>> with decimal.localcontext(DECIMAL128_CTX) as ctx:
209 ... Decimal128(ctx.create_decimal("1E6145"))
210 ...
211 Decimal128('Infinity')
212
213 To match the behavior of MongoDB's Decimal128 implementation
214 str(Decimal(value)) may not match str(Decimal128(value)) for NaN values::
215
216 >>> Decimal128(Decimal('NaN'))
217 Decimal128('NaN')
218 >>> Decimal128(Decimal('-NaN'))
219 Decimal128('NaN')
220 >>> Decimal128(Decimal('sNaN'))
221 Decimal128('NaN')
222 >>> Decimal128(Decimal('-sNaN'))
223 Decimal128('NaN')
224
225 However, :meth:`~Decimal128.to_decimal` will return the exact value::
226
227 >>> Decimal128(Decimal('NaN')).to_decimal()
228 Decimal('NaN')
229 >>> Decimal128(Decimal('-NaN')).to_decimal()
230 Decimal('-NaN')
231 >>> Decimal128(Decimal('sNaN')).to_decimal()
232 Decimal('sNaN')
233 >>> Decimal128(Decimal('-sNaN')).to_decimal()
234 Decimal('-sNaN')
235
236 Two instances of :class:`Decimal128` compare equal if their Binary
237 Integer Decimal encodings are equal::
238
239 >>> Decimal128('NaN') == Decimal128('NaN')
240 True
241 >>> Decimal128('NaN').bid == Decimal128('NaN').bid
242 True
243
244 This differs from :class:`decimal.Decimal` comparisons for NaN::
245
246 >>> Decimal('NaN') == Decimal('NaN')
247 False
248 """
249 __slots__ = ('__high', '__low')
250
251 _type_marker = 19
252
253 def __init__(self, value):
254 if isinstance(value, (_string_type, decimal.Decimal)):
255 self.__high, self.__low = _decimal_to_128(value)
256 elif isinstance(value, (list, tuple)):
257 if len(value) != 2:
258 raise ValueError('Invalid size for creation of Decimal128 '
259 'from list or tuple. Must have exactly 2 '
260 'elements.')
261 self.__high, self.__low = value
262 else:
263 raise TypeError("Cannot convert %r to Decimal128" % (value,))
264
265 def to_decimal(self):
266 """Returns an instance of :class:`decimal.Decimal` for this
267 :class:`Decimal128`.
268 """
269 high = self.__high
270 low = self.__low
271 sign = 1 if (high & _SIGN) else 0
272
273 if (high & _SNAN) == _SNAN:
274 return decimal.Decimal((sign, (), 'N'))
275 elif (high & _NAN) == _NAN:
276 return decimal.Decimal((sign, (), 'n'))
277 elif (high & _INF) == _INF:
278 return decimal.Decimal((sign, (), 'F'))
279
280 if (high & _EXPONENT_MASK) == _EXPONENT_MASK:
281 exponent = ((high & 0x1fffe00000000000) >> 47) - _EXPONENT_BIAS
282 return decimal.Decimal((sign, (0,), exponent))
283 else:
284 exponent = ((high & 0x7fff800000000000) >> 49) - _EXPONENT_BIAS
285
286 arr = bytearray(15)
287 mask = 0x00000000000000ff
288 for i in range(14, 6, -1):
289 arr[i] = (low & mask) >> ((14 - i) << 3)
290 mask = mask << 8
291
292 mask = 0x00000000000000ff
293 for i in range(6, 0, -1):
294 arr[i] = (high & mask) >> ((6 - i) << 3)
295 mask = mask << 8
296
297 mask = 0x0001000000000000
298 arr[0] = (high & mask) >> 48
299
300 # Have to convert bytearray to bytes for python 2.6.
301 # cdecimal only accepts a tuple for digits.
302 digits = tuple(
303 int(digit) for digit in str(_from_bytes(bytes(arr), 'big')))
304
305 with decimal.localcontext(_DEC128_CTX) as ctx:
306 return ctx.create_decimal((sign, digits, exponent))
307
308 @classmethod
309 def from_bid(cls, value):
310 """Create an instance of :class:`Decimal128` from Binary Integer
311 Decimal string.
312
313 :Parameters:
314 - `value`: 16 byte string (128-bit IEEE 754-2008 decimal floating
315 point in Binary Integer Decimal (BID) format).
316 """
317 if not isinstance(value, bytes):
318 raise TypeError("value must be an instance of bytes")
319 if len(value) != 16:
320 raise ValueError("value must be exactly 16 bytes")
321 return cls((_UNPACK_64(value[8:])[0], _UNPACK_64(value[:8])[0]))
322
323 @property
324 def bid(self):
325 """The Binary Integer Decimal (BID) encoding of this instance."""
326 return _PACK_64(self.__low) + _PACK_64(self.__high)
327
328 def __str__(self):
329 dec = self.to_decimal()
330 if dec.is_nan():
331 # Required by the drivers spec to match MongoDB behavior.
332 return "NaN"
333 return str(dec)
334
335 def __repr__(self):
336 return "Decimal128('%s')" % (str(self),)
337
338 def __setstate__(self, value):
339 self.__high, self.__low = value
340
341 def __getstate__(self):
342 return self.__high, self.__low
343
344 def __eq__(self, other):
345 if isinstance(other, Decimal128):
346 return self.bid == other.bid
347 return NotImplemented
348
349 def __ne__(self, other):
350 return not self == other
+0
-40
mockupdb/_bson/errors.py less more
0 # Copyright 2009-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Exceptions raised by the BSON package."""
15
16
17 class BSONError(Exception):
18 """Base class for all BSON exceptions.
19 """
20
21
22 class InvalidBSON(BSONError):
23 """Raised when trying to create a BSON object from invalid data.
24 """
25
26
27 class InvalidStringData(BSONError):
28 """Raised when trying to encode a string containing non-UTF8 data.
29 """
30
31
32 class InvalidDocument(BSONError):
33 """Raised when trying to create a BSON object from an invalid document.
34 """
35
36
37 class InvalidId(BSONError):
38 """Raised when trying to create an ObjectId from invalid data.
39 """
+0
-34
mockupdb/_bson/int64.py less more
0 # Copyright 2014-2015 MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """A BSON wrapper for long (int in python3)"""
15
16 from mockupdb._bson.py3compat import PY3
17
18 if PY3:
19 long = int
20
21
22 class Int64(long):
23 """Representation of the BSON int64 type.
24
25 This is necessary because every integral number is an :class:`int` in
26 Python 3. Small integral numbers are encoded to BSON int32 by default,
27 but Int64 numbers will always be encoded to BSON int64.
28
29 :Parameters:
30 - `value`: the numeric value to represent
31 """
32
33 _type_marker = 18
+0
-860
mockupdb/_bson/json_util.py less more
0 # Copyright 2009-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for using Python's :mod:`json` module with BSON documents.
15
16 This module provides two helper methods `dumps` and `loads` that wrap the
17 native :mod:`json` methods and provide explicit BSON conversion to and from
18 JSON. :class:`~bson.json_util.JSONOptions` provides a way to control how JSON
19 is emitted and parsed, with the default being the legacy PyMongo format.
20 :mod:`~bson.json_util` can also generate Canonical or Relaxed `Extended JSON`_
21 when :const:`CANONICAL_JSON_OPTIONS` or :const:`RELAXED_JSON_OPTIONS` is
22 provided, respectively.
23
24 .. _Extended JSON: https://github.com/mongodb/specifications/blob/master/source/extended-json.rst
25
26 Example usage (deserialization):
27
28 .. doctest::
29
30 >>> from mockupdb._bson.json_util import loads
31 >>> loads('[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": "80", "$binary": "AQIDBA=="}}]')
32 [{u'foo': [1, 2]}, {u'bar': {u'hello': u'world'}}, {u'code': Code('function x() { return 1; }', {})}, {u'bin': Binary('...', 128)}]
33
34 Example usage (serialization):
35
36 .. doctest::
37
38 >>> from mockupdb._bson import Binary, Code
39 >>> from mockupdb._bson.json_util import dumps
40 >>> dumps([{'foo': [1, 2]},
41 ... {'bar': {'hello': 'world'}},
42 ... {'code': Code("function x() { return 1; }", {})},
43 ... {'bin': Binary(b"\x01\x02\x03\x04")}])
44 '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]'
45
46 Example usage (with :const:`CANONICAL_JSON_OPTIONS`):
47
48 .. doctest::
49
50 >>> from mockupdb._bson import Binary, Code
51 >>> from mockupdb._bson.json_util import dumps, CANONICAL_JSON_OPTIONS
52 >>> dumps([{'foo': [1, 2]},
53 ... {'bar': {'hello': 'world'}},
54 ... {'code': Code("function x() { return 1; }")},
55 ... {'bin': Binary(b"\x01\x02\x03\x04")}],
56 ... json_options=CANONICAL_JSON_OPTIONS)
57 '[{"foo": [{"$numberInt": "1"}, {"$numberInt": "2"}]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]'
58
59 Example usage (with :const:`RELAXED_JSON_OPTIONS`):
60
61 .. doctest::
62
63 >>> from mockupdb._bson import Binary, Code
64 >>> from mockupdb._bson.json_util import dumps, RELAXED_JSON_OPTIONS
65 >>> dumps([{'foo': [1, 2]},
66 ... {'bar': {'hello': 'world'}},
67 ... {'code': Code("function x() { return 1; }")},
68 ... {'bin': Binary(b"\x01\x02\x03\x04")}],
69 ... json_options=RELAXED_JSON_OPTIONS)
70 '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]'
71
72 Alternatively, you can manually pass the `default` to :func:`json.dumps`.
73 It won't handle :class:`~bson.binary.Binary` and :class:`~bson.code.Code`
74 instances (as they are extended strings you can't provide custom defaults),
75 but it will be faster as there is less recursion.
76
77 .. note::
78 If your application does not need the flexibility offered by
79 :class:`JSONOptions` and spends a large amount of time in the `json_util`
80 module, look to
81 `python-bsonjs <https://pypi.python.org/pypi/python-bsonjs>`_ for a nice
82 performance improvement. `python-bsonjs` is a fast BSON to MongoDB
83 Extended JSON converter for Python built on top of
84 `libbson <https://github.com/mongodb/libbson>`_. `python-bsonjs` works best
85 with PyMongo when using :class:`~bson.raw_bson.RawBSONDocument`.
86
87 .. versionchanged:: 2.8
88 The output format for :class:`~bson.timestamp.Timestamp` has changed from
89 '{"t": <int>, "i": <int>}' to '{"$timestamp": {"t": <int>, "i": <int>}}'.
90 This new format will be decoded to an instance of
91 :class:`~bson.timestamp.Timestamp`. The old format will continue to be
92 decoded to a python dict as before. Encoding to the old format is no longer
93 supported as it was never correct and loses type information.
94 Added support for $numberLong and $undefined - new in MongoDB 2.6 - and
95 parsing $date in ISO-8601 format.
96
97 .. versionchanged:: 2.7
98 Preserves order when rendering SON, Timestamp, Code, Binary, and DBRef
99 instances.
100
101 .. versionchanged:: 2.3
102 Added dumps and loads helpers to automatically handle conversion to and
103 from json and supports :class:`~bson.binary.Binary` and
104 :class:`~bson.code.Code`
105 """
106
107 import base64
108 import datetime
109 import math
110 import re
111 import sys
112 import uuid
113
114 if sys.version_info[:2] == (2, 6):
115 # In Python 2.6, json does not include object_pairs_hook. Use simplejson
116 # instead.
117 try:
118 import simplejson as json
119 except ImportError:
120 import json
121 else:
122 import json
123
124 class ConfigurationError(Exception):
125 pass
126
127 import mockupdb._bson as bson
128 from mockupdb._bson import EPOCH_AWARE, EPOCH_NAIVE, RE_TYPE, SON
129 from mockupdb._bson.binary import (Binary, JAVA_LEGACY, CSHARP_LEGACY,
130 OLD_UUID_SUBTYPE,
131 UUID_SUBTYPE)
132 from mockupdb._bson.code import Code
133 from mockupdb._bson.codec_options import CodecOptions
134 from mockupdb._bson.dbref import DBRef
135 from mockupdb._bson.decimal128 import Decimal128
136 from mockupdb._bson.int64 import Int64
137 from mockupdb._bson.max_key import MaxKey
138 from mockupdb._bson.min_key import MinKey
139 from mockupdb._bson.objectid import ObjectId
140 from mockupdb._bson.py3compat import (PY3, iteritems, integer_types,
141 string_type,
142 text_type)
143 from mockupdb._bson.regex import Regex
144 from mockupdb._bson.timestamp import Timestamp
145 from mockupdb._bson.tz_util import utc
146
147
148 try:
149 json.loads("{}", object_pairs_hook=dict)
150 _HAS_OBJECT_PAIRS_HOOK = True
151 except TypeError:
152 _HAS_OBJECT_PAIRS_HOOK = False
153
154
155 _RE_OPT_TABLE = {
156 "i": re.I,
157 "l": re.L,
158 "m": re.M,
159 "s": re.S,
160 "u": re.U,
161 "x": re.X,
162 }
163
164 # Dollar-prefixed keys which may appear in DBRefs.
165 _DBREF_KEYS = frozenset(['$id', '$ref', '$db'])
166
167
168 class DatetimeRepresentation:
169 LEGACY = 0
170 """Legacy MongoDB Extended JSON datetime representation.
171
172 :class:`datetime.datetime` instances will be encoded to JSON in the
173 format `{"$date": <dateAsMilliseconds>}`, where `dateAsMilliseconds` is
174 a 64-bit signed integer giving the number of milliseconds since the Unix
175 epoch UTC. This was the default encoding before PyMongo version 3.4.
176
177 .. versionadded:: 3.4
178 """
179
180 NUMBERLONG = 1
181 """NumberLong datetime representation.
182
183 :class:`datetime.datetime` instances will be encoded to JSON in the
184 format `{"$date": {"$numberLong": "<dateAsMilliseconds>"}}`,
185 where `dateAsMilliseconds` is the string representation of a 64-bit signed
186 integer giving the number of milliseconds since the Unix epoch UTC.
187
188 .. versionadded:: 3.4
189 """
190
191 ISO8601 = 2
192 """ISO-8601 datetime representation.
193
194 :class:`datetime.datetime` instances greater than or equal to the Unix
195 epoch UTC will be encoded to JSON in the format `{"$date": "<ISO-8601>"}`.
196 :class:`datetime.datetime` instances before the Unix epoch UTC will be
197 encoded as if the datetime representation is
198 :const:`~DatetimeRepresentation.NUMBERLONG`.
199
200 .. versionadded:: 3.4
201 """
202
203
204 class JSONMode:
205 LEGACY = 0
206 """Legacy Extended JSON representation.
207
208 In this mode, :func:`~bson.json_util.dumps` produces PyMongo's legacy
209 non-standard JSON output. Consider using
210 :const:`~bson.json_util.JSONMode.RELAXED` or
211 :const:`~bson.json_util.JSONMode.CANONICAL` instead.
212
213 .. versionadded:: 3.5
214 """
215
216 RELAXED = 1
217 """Relaxed Extended JSON representation.
218
219 In this mode, :func:`~bson.json_util.dumps` produces Relaxed Extended JSON,
220 a mostly JSON-like format. Consider using this for things like a web API,
221 where one is sending a document (or a projection of a document) that only
222 uses ordinary JSON type primitives. In particular, the ``int``,
223 :class:`~bson.int64.Int64`, and ``float`` numeric types are represented in
224 the native JSON number format. This output is also the most human readable
225 and is useful for debugging and documentation.
226
227 .. seealso:: The specification for Relaxed `Extended JSON`_.
228
229 .. versionadded:: 3.5
230 """
231
232 CANONICAL = 2
233 """Canonical Extended JSON representation.
234
235 In this mode, :func:`~bson.json_util.dumps` produces Canonical Extended
236 JSON, a type preserving format. Consider using this for things like
237 testing, where one has to precisely specify expected types in JSON. In
238 particular, the ``int``, :class:`~bson.int64.Int64`, and ``float`` numeric
239 types are encoded with type wrappers.
240
241 .. seealso:: The specification for Canonical `Extended JSON`_.
242
243 .. versionadded:: 3.5
244 """
245
246
247 class JSONOptions(CodecOptions):
248 """Encapsulates JSON options for :func:`dumps` and :func:`loads`.
249
250 Raises :exc:`~pymongo.errors.ConfigurationError` on Python 2.6 if
251 `simplejson >= 2.1.0 <https://pypi.python.org/pypi/simplejson>`_ is not
252 installed and document_class is not the default (:class:`dict`).
253
254 :Parameters:
255 - `strict_number_long`: If ``True``, :class:`~bson.int64.Int64` objects
256 are encoded to MongoDB Extended JSON's *Strict mode* type
257 `NumberLong`, ie ``'{"$numberLong": "<number>" }'``. Otherwise they
258 will be encoded as an `int`. Defaults to ``False``.
259 - `datetime_representation`: The representation to use when encoding
260 instances of :class:`datetime.datetime`. Defaults to
261 :const:`~DatetimeRepresentation.LEGACY`.
262 - `strict_uuid`: If ``True``, :class:`uuid.UUID` object are encoded to
263 MongoDB Extended JSON's *Strict mode* type `Binary`. Otherwise it
264 will be encoded as ``'{"$uuid": "<hex>" }'``. Defaults to ``False``.
265 - `json_mode`: The :class:`JSONMode` to use when encoding BSON types to
266 Extended JSON. Defaults to :const:`~JSONMode.LEGACY`.
267 - `document_class`: BSON documents returned by :func:`loads` will be
268 decoded to an instance of this class. Must be a subclass of
269 :class:`collections.MutableMapping`. Defaults to :class:`dict`.
270 - `uuid_representation`: The BSON representation to use when encoding
271 and decoding instances of :class:`uuid.UUID`. Defaults to
272 :const:`~bson.binary.PYTHON_LEGACY`.
273 - `tz_aware`: If ``True``, MongoDB Extended JSON's *Strict mode* type
274 `Date` will be decoded to timezone aware instances of
275 :class:`datetime.datetime`. Otherwise they will be naive. Defaults
276 to ``True``.
277 - `tzinfo`: A :class:`datetime.tzinfo` subclass that specifies the
278 timezone from which :class:`~datetime.datetime` objects should be
279 decoded. Defaults to :const:`~bson.tz_util.utc`.
280 - `args`: arguments to :class:`~bson.codec_options.CodecOptions`
281 - `kwargs`: arguments to :class:`~bson.codec_options.CodecOptions`
282
283 .. seealso:: The specification for Relaxed and Canonical `Extended JSON`_.
284
285 .. versionadded:: 3.4
286
287 .. versionchanged:: 3.5
288 Accepts the optional parameter `json_mode`.
289
290 """
291
292 def __new__(cls, strict_number_long=False,
293 datetime_representation=DatetimeRepresentation.LEGACY,
294 strict_uuid=False, json_mode=JSONMode.LEGACY,
295 *args, **kwargs):
296 kwargs["tz_aware"] = kwargs.get("tz_aware", True)
297 if kwargs["tz_aware"]:
298 kwargs["tzinfo"] = kwargs.get("tzinfo", utc)
299 if datetime_representation not in (DatetimeRepresentation.LEGACY,
300 DatetimeRepresentation.NUMBERLONG,
301 DatetimeRepresentation.ISO8601):
302 raise ConfigurationError(
303 "JSONOptions.datetime_representation must be one of LEGACY, "
304 "NUMBERLONG, or ISO8601 from DatetimeRepresentation.")
305 self = super(JSONOptions, cls).__new__(cls, *args, **kwargs)
306 if not _HAS_OBJECT_PAIRS_HOOK and self.document_class != dict:
307 raise ConfigurationError(
308 "Support for JSONOptions.document_class on Python 2.6 "
309 "requires simplejson >= 2.1.0"
310 "(https://pypi.python.org/pypi/simplejson) to be installed.")
311 if json_mode not in (JSONMode.LEGACY,
312 JSONMode.RELAXED,
313 JSONMode.CANONICAL):
314 raise ConfigurationError(
315 "JSONOptions.json_mode must be one of LEGACY, RELAXED, "
316 "or CANONICAL from JSONMode.")
317 self.json_mode = json_mode
318 if self.json_mode == JSONMode.RELAXED:
319 self.strict_number_long = False
320 self.datetime_representation = DatetimeRepresentation.ISO8601
321 self.strict_uuid = True
322 elif self.json_mode == JSONMode.CANONICAL:
323 self.strict_number_long = True
324 self.datetime_representation = DatetimeRepresentation.NUMBERLONG
325 self.strict_uuid = True
326 else:
327 self.strict_number_long = strict_number_long
328 self.datetime_representation = datetime_representation
329 self.strict_uuid = strict_uuid
330 return self
331
332 def _arguments_repr(self):
333 return ('strict_number_long=%r, '
334 'datetime_representation=%r, '
335 'strict_uuid=%r, json_mode=%r, %s' % (
336 self.strict_number_long,
337 self.datetime_representation,
338 self.strict_uuid,
339 self.json_mode,
340 super(JSONOptions, self)._arguments_repr()))
341
342
343 LEGACY_JSON_OPTIONS = JSONOptions(json_mode=JSONMode.LEGACY)
344 """:class:`JSONOptions` for encoding to PyMongo's legacy JSON format.
345
346 .. seealso:: The documentation for :const:`bson.json_util.JSONMode.LEGACY`.
347
348 .. versionadded:: 3.5
349 """
350
351 DEFAULT_JSON_OPTIONS = LEGACY_JSON_OPTIONS
352 """The default :class:`JSONOptions` for JSON encoding/decoding.
353
354 The same as :const:`LEGACY_JSON_OPTIONS`. This will change to
355 :const:`RELAXED_JSON_OPTIONS` in a future release.
356
357 .. versionadded:: 3.4
358 """
359
360 CANONICAL_JSON_OPTIONS = JSONOptions(json_mode=JSONMode.CANONICAL)
361 """:class:`JSONOptions` for Canonical Extended JSON.
362
363 .. seealso:: The documentation for :const:`bson.json_util.JSONMode.CANONICAL`.
364
365 .. versionadded:: 3.5
366 """
367
368 RELAXED_JSON_OPTIONS = JSONOptions(json_mode=JSONMode.RELAXED)
369 """:class:`JSONOptions` for Relaxed Extended JSON.
370
371 .. seealso:: The documentation for :const:`bson.json_util.JSONMode.RELAXED`.
372
373 .. versionadded:: 3.5
374 """
375
376 STRICT_JSON_OPTIONS = JSONOptions(
377 strict_number_long=True,
378 datetime_representation=DatetimeRepresentation.ISO8601,
379 strict_uuid=True)
380 """**DEPRECATED** - :class:`JSONOptions` for MongoDB Extended JSON's *Strict
381 mode* encoding.
382
383 .. versionadded:: 3.4
384
385 .. versionchanged:: 3.5
386 Deprecated. Use :const:`RELAXED_JSON_OPTIONS` or
387 :const:`CANONICAL_JSON_OPTIONS` instead.
388 """
389
390
391 def dumps(obj, *args, **kwargs):
392 """Helper function that wraps :func:`json.dumps`.
393
394 Recursive function that handles all BSON types including
395 :class:`~bson.binary.Binary` and :class:`~bson.code.Code`.
396
397 :Parameters:
398 - `json_options`: A :class:`JSONOptions` instance used to modify the
399 encoding of MongoDB Extended JSON types. Defaults to
400 :const:`DEFAULT_JSON_OPTIONS`.
401
402 .. versionchanged:: 3.4
403 Accepts optional parameter `json_options`. See :class:`JSONOptions`.
404
405 .. versionchanged:: 2.7
406 Preserves order when rendering SON, Timestamp, Code, Binary, and DBRef
407 instances.
408 """
409 json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS)
410 return json.dumps(_json_convert(obj, json_options), *args, **kwargs)
411
412
413 def loads(s, *args, **kwargs):
414 """Helper function that wraps :func:`json.loads`.
415
416 Automatically passes the object_hook for BSON type conversion.
417
418 Raises ``TypeError``, ``ValueError``, ``KeyError``, or
419 :exc:`~bson.errors.InvalidId` on invalid MongoDB Extended JSON.
420
421 :Parameters:
422 - `json_options`: A :class:`JSONOptions` instance used to modify the
423 decoding of MongoDB Extended JSON types. Defaults to
424 :const:`DEFAULT_JSON_OPTIONS`.
425
426 .. versionchanged:: 3.5
427 Parses Relaxed and Canonical Extended JSON as well as PyMongo's legacy
428 format. Now raises ``TypeError`` or ``ValueError`` when parsing JSON
429 type wrappers with values of the wrong type or any extra keys.
430
431 .. versionchanged:: 3.4
432 Accepts optional parameter `json_options`. See :class:`JSONOptions`.
433 """
434 json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS)
435 if _HAS_OBJECT_PAIRS_HOOK:
436 kwargs["object_pairs_hook"] = lambda pairs: object_pairs_hook(
437 pairs, json_options)
438 else:
439 kwargs["object_hook"] = lambda obj: object_hook(obj, json_options)
440 return json.loads(s, *args, **kwargs)
441
442
443 def _json_convert(obj, json_options=DEFAULT_JSON_OPTIONS):
444 """Recursive helper method that converts BSON types so they can be
445 converted into json.
446 """
447 if hasattr(obj, 'iteritems') or hasattr(obj, 'items'): # PY3 support
448 return SON(((k, _json_convert(v, json_options))
449 for k, v in iteritems(obj)))
450 elif hasattr(obj, '__iter__') and not isinstance(obj, (text_type, bytes)):
451 return list((_json_convert(v, json_options) for v in obj))
452 try:
453 return default(obj, json_options)
454 except TypeError:
455 return obj
456
457
458 def object_pairs_hook(pairs, json_options=DEFAULT_JSON_OPTIONS):
459 return object_hook(json_options.document_class(pairs), json_options)
460
461
462 def object_hook(dct, json_options=DEFAULT_JSON_OPTIONS):
463 if "$oid" in dct:
464 return _parse_canonical_oid(dct)
465 if "$ref" in dct:
466 return _parse_canonical_dbref(dct)
467 if "$date" in dct:
468 return _parse_canonical_datetime(dct, json_options)
469 if "$regex" in dct:
470 return _parse_legacy_regex(dct)
471 if "$minKey" in dct:
472 return _parse_canonical_minkey(dct)
473 if "$maxKey" in dct:
474 return _parse_canonical_maxkey(dct)
475 if "$binary" in dct:
476 if "$type" in dct:
477 return _parse_legacy_binary(dct, json_options)
478 else:
479 return _parse_canonical_binary(dct, json_options)
480 if "$code" in dct:
481 return _parse_canonical_code(dct)
482 if "$uuid" in dct:
483 return _parse_legacy_uuid(dct)
484 if "$undefined" in dct:
485 return None
486 if "$numberLong" in dct:
487 return _parse_canonical_int64(dct)
488 if "$timestamp" in dct:
489 tsp = dct["$timestamp"]
490 return Timestamp(tsp["t"], tsp["i"])
491 if "$numberDecimal" in dct:
492 return _parse_canonical_decimal128(dct)
493 if "$dbPointer" in dct:
494 return _parse_canonical_dbpointer(dct)
495 if "$regularExpression" in dct:
496 return _parse_canonical_regex(dct)
497 if "$symbol" in dct:
498 return _parse_canonical_symbol(dct)
499 if "$numberInt" in dct:
500 return _parse_canonical_int32(dct)
501 if "$numberDouble" in dct:
502 return _parse_canonical_double(dct)
503 return dct
504
505
506 def _parse_legacy_regex(doc):
507 pattern = doc["$regex"]
508 # Check if this is the $regex query operator.
509 if isinstance(pattern, Regex):
510 return doc
511 flags = 0
512 # PyMongo always adds $options but some other tools may not.
513 for opt in doc.get("$options", ""):
514 flags |= _RE_OPT_TABLE.get(opt, 0)
515 return Regex(pattern, flags)
516
517
518 def _parse_legacy_uuid(doc):
519 """Decode a JSON legacy $uuid to Python UUID."""
520 if len(doc) != 1:
521 raise TypeError('Bad $uuid, extra field(s): %s' % (doc,))
522 return uuid.UUID(doc["$uuid"])
523
524
525 def _binary_or_uuid(data, subtype, json_options):
526 # special handling for UUID
527 if subtype == OLD_UUID_SUBTYPE:
528 if json_options.uuid_representation == CSHARP_LEGACY:
529 return uuid.UUID(bytes_le=data)
530 if json_options.uuid_representation == JAVA_LEGACY:
531 data = data[7::-1] + data[:7:-1]
532 return uuid.UUID(bytes=data)
533 if subtype == UUID_SUBTYPE:
534 return uuid.UUID(bytes=data)
535 if PY3 and subtype == 0:
536 return data
537 return Binary(data, subtype)
538
539
540 def _parse_legacy_binary(doc, json_options):
541 if isinstance(doc["$type"], int):
542 doc["$type"] = "%02x" % doc["$type"]
543 subtype = int(doc["$type"], 16)
544 if subtype >= 0xffffff80: # Handle mongoexport values
545 subtype = int(doc["$type"][6:], 16)
546 data = base64.b64decode(doc["$binary"].encode())
547 return _binary_or_uuid(data, subtype, json_options)
548
549
550 def _parse_canonical_binary(doc, json_options):
551 binary = doc["$binary"]
552 b64 = binary["base64"]
553 subtype = binary["subType"]
554 if not isinstance(b64, string_type):
555 raise TypeError('$binary base64 must be a string: %s' % (doc,))
556 if not isinstance(subtype, string_type) or len(subtype) > 2:
557 raise TypeError('$binary subType must be a string at most 2 '
558 'characters: %s' % (doc,))
559 if len(binary) != 2:
560 raise TypeError('$binary must include only "base64" and "subType" '
561 'components: %s' % (doc,))
562
563 data = base64.b64decode(b64.encode())
564 return _binary_or_uuid(data, int(subtype, 16), json_options)
565
566
567 def _parse_canonical_datetime(doc, json_options):
568 """Decode a JSON datetime to python datetime.datetime."""
569 dtm = doc["$date"]
570 if len(doc) != 1:
571 raise TypeError('Bad $date, extra field(s): %s' % (doc,))
572 # mongoexport 2.6 and newer
573 if isinstance(dtm, string_type):
574 # Parse offset
575 if dtm[-1] == 'Z':
576 dt = dtm[:-1]
577 offset = 'Z'
578 elif dtm[-3] == ':':
579 # (+|-)HH:MM
580 dt = dtm[:-6]
581 offset = dtm[-6:]
582 elif dtm[-5] in ('+', '-'):
583 # (+|-)HHMM
584 dt = dtm[:-5]
585 offset = dtm[-5:]
586 elif dtm[-3] in ('+', '-'):
587 # (+|-)HH
588 dt = dtm[:-3]
589 offset = dtm[-3:]
590 else:
591 dt = dtm
592 offset = ''
593
594 # Parse the optional factional seconds portion.
595 dot_index = dt.rfind('.')
596 microsecond = 0
597 if dot_index != -1:
598 microsecond = int(float(dt[dot_index:]) * 1000000)
599 dt = dt[:dot_index]
600
601 aware = datetime.datetime.strptime(
602 dt, "%Y-%m-%dT%H:%M:%S").replace(microsecond=microsecond,
603 tzinfo=utc)
604
605 if offset and offset != 'Z':
606 if len(offset) == 6:
607 hours, minutes = offset[1:].split(':')
608 secs = (int(hours) * 3600 + int(minutes) * 60)
609 elif len(offset) == 5:
610 secs = (int(offset[1:3]) * 3600 + int(offset[3:]) * 60)
611 elif len(offset) == 3:
612 secs = int(offset[1:3]) * 3600
613 if offset[0] == "-":
614 secs *= -1
615 aware = aware - datetime.timedelta(seconds=secs)
616
617 if json_options.tz_aware:
618 if json_options.tzinfo:
619 aware = aware.astimezone(json_options.tzinfo)
620 return aware
621 else:
622 return aware.replace(tzinfo=None)
623 return bson._millis_to_datetime(int(dtm), json_options)
624
625
626 def _parse_canonical_oid(doc):
627 """Decode a JSON ObjectId to bson.objectid.ObjectId."""
628 if len(doc) != 1:
629 raise TypeError('Bad $oid, extra field(s): %s' % (doc,))
630 return ObjectId(doc['$oid'])
631
632
633 def _parse_canonical_symbol(doc):
634 """Decode a JSON symbol to Python string."""
635 symbol = doc['$symbol']
636 if len(doc) != 1:
637 raise TypeError('Bad $symbol, extra field(s): %s' % (doc,))
638 return text_type(symbol)
639
640
641 def _parse_canonical_code(doc):
642 """Decode a JSON code to bson.code.Code."""
643 for key in doc:
644 if key not in ('$code', '$scope'):
645 raise TypeError('Bad $code, extra field(s): %s' % (doc,))
646 return Code(doc['$code'], scope=doc.get('$scope'))
647
648
649 def _parse_canonical_regex(doc):
650 """Decode a JSON regex to bson.regex.Regex."""
651 regex = doc['$regularExpression']
652 if len(doc) != 1:
653 raise TypeError('Bad $regularExpression, extra field(s): %s' % (doc,))
654 if len(regex) != 2:
655 raise TypeError('Bad $regularExpression must include only "pattern"'
656 'and "options" components: %s' % (doc,))
657 return Regex(regex['pattern'], regex['options'])
658
659
660 def _parse_canonical_dbref(doc):
661 """Decode a JSON DBRef to bson.dbref.DBRef."""
662 for key in doc:
663 if key.startswith('$') and key not in _DBREF_KEYS:
664 # Other keys start with $, so dct cannot be parsed as a DBRef.
665 return doc
666 return DBRef(doc.pop('$ref'), doc.pop('$id'),
667 database=doc.pop('$db', None), **doc)
668
669
670 def _parse_canonical_dbpointer(doc):
671 """Decode a JSON (deprecated) DBPointer to bson.dbref.DBRef."""
672 dbref = doc['$dbPointer']
673 if len(doc) != 1:
674 raise TypeError('Bad $dbPointer, extra field(s): %s' % (doc,))
675 if isinstance(dbref, DBRef):
676 dbref_doc = dbref.as_doc()
677 # DBPointer must not contain $db in its value.
678 if dbref.database is not None:
679 raise TypeError(
680 'Bad $dbPointer, extra field $db: %s' % (dbref_doc,))
681 if not isinstance(dbref.id, ObjectId):
682 raise TypeError(
683 'Bad $dbPointer, $id must be an ObjectId: %s' % (dbref_doc,))
684 if len(dbref_doc) != 2:
685 raise TypeError(
686 'Bad $dbPointer, extra field(s) in DBRef: %s' % (dbref_doc,))
687 return dbref
688 else:
689 raise TypeError('Bad $dbPointer, expected a DBRef: %s' % (doc,))
690
691
692 def _parse_canonical_int32(doc):
693 """Decode a JSON int32 to python int."""
694 i_str = doc['$numberInt']
695 if len(doc) != 1:
696 raise TypeError('Bad $numberInt, extra field(s): %s' % (doc,))
697 if not isinstance(i_str, string_type):
698 raise TypeError('$numberInt must be string: %s' % (doc,))
699 return int(i_str)
700
701
702 def _parse_canonical_int64(doc):
703 """Decode a JSON int64 to bson.int64.Int64."""
704 l_str = doc['$numberLong']
705 if len(doc) != 1:
706 raise TypeError('Bad $numberLong, extra field(s): %s' % (doc,))
707 return Int64(l_str)
708
709
710 def _parse_canonical_double(doc):
711 """Decode a JSON double to python float."""
712 d_str = doc['$numberDouble']
713 if len(doc) != 1:
714 raise TypeError('Bad $numberDouble, extra field(s): %s' % (doc,))
715 if not isinstance(d_str, string_type):
716 raise TypeError('$numberDouble must be string: %s' % (doc,))
717 return float(d_str)
718
719
720 def _parse_canonical_decimal128(doc):
721 """Decode a JSON decimal128 to bson.decimal128.Decimal128."""
722 d_str = doc['$numberDecimal']
723 if len(doc) != 1:
724 raise TypeError('Bad $numberDecimal, extra field(s): %s' % (doc,))
725 if not isinstance(d_str, string_type):
726 raise TypeError('$numberDecimal must be string: %s' % (doc,))
727 return Decimal128(d_str)
728
729
730 def _parse_canonical_minkey(doc):
731 """Decode a JSON MinKey to bson.min_key.MinKey."""
732 if doc['$minKey'] is not 1:
733 raise TypeError('$minKey value must be 1: %s' % (doc,))
734 if len(doc) != 1:
735 raise TypeError('Bad $minKey, extra field(s): %s' % (doc,))
736 return MinKey()
737
738
739 def _parse_canonical_maxkey(doc):
740 """Decode a JSON MaxKey to bson.max_key.MaxKey."""
741 if doc['$maxKey'] is not 1:
742 raise TypeError('$maxKey value must be 1: %s', (doc,))
743 if len(doc) != 1:
744 raise TypeError('Bad $minKey, extra field(s): %s' % (doc,))
745 return MaxKey()
746
747
748 def _encode_binary(data, subtype, json_options):
749 if json_options.json_mode == JSONMode.LEGACY:
750 return SON([
751 ('$binary', base64.b64encode(data).decode()),
752 ('$type', "%02x" % subtype)])
753 return {'$binary': SON([
754 ('base64', base64.b64encode(data).decode()),
755 ('subType', "%02x" % subtype)])}
756
757
758 def default(obj, json_options=DEFAULT_JSON_OPTIONS):
759 # We preserve key order when rendering SON, DBRef, etc. as JSON by
760 # returning a SON for those types instead of a dict.
761 if isinstance(obj, ObjectId):
762 return {"$oid": str(obj)}
763 if isinstance(obj, DBRef):
764 return _json_convert(obj.as_doc(), json_options=json_options)
765 if isinstance(obj, datetime.datetime):
766 if (json_options.datetime_representation ==
767 DatetimeRepresentation.ISO8601):
768 if not obj.tzinfo:
769 obj = obj.replace(tzinfo=utc)
770 if obj >= EPOCH_AWARE:
771 off = obj.tzinfo.utcoffset(obj)
772 if (off.days, off.seconds, off.microseconds) == (0, 0, 0):
773 tz_string = 'Z'
774 else:
775 tz_string = obj.strftime('%z')
776 millis = int(obj.microsecond / 1000)
777 fracsecs = ".%03d" % (millis,) if millis else ""
778 return {"$date": "%s%s%s" % (
779 obj.strftime("%Y-%m-%dT%H:%M:%S"), fracsecs, tz_string)}
780
781 millis = bson._datetime_to_millis(obj)
782 if (json_options.datetime_representation ==
783 DatetimeRepresentation.LEGACY):
784 return {"$date": millis}
785 return {"$date": {"$numberLong": str(millis)}}
786 if json_options.strict_number_long and isinstance(obj, Int64):
787 return {"$numberLong": str(obj)}
788 if isinstance(obj, (RE_TYPE, Regex)):
789 flags = ""
790 if obj.flags & re.IGNORECASE:
791 flags += "i"
792 if obj.flags & re.LOCALE:
793 flags += "l"
794 if obj.flags & re.MULTILINE:
795 flags += "m"
796 if obj.flags & re.DOTALL:
797 flags += "s"
798 if obj.flags & re.UNICODE:
799 flags += "u"
800 if obj.flags & re.VERBOSE:
801 flags += "x"
802 if isinstance(obj.pattern, text_type):
803 pattern = obj.pattern
804 else:
805 pattern = obj.pattern.decode('utf-8')
806 if json_options.json_mode == JSONMode.LEGACY:
807 return SON([("$regex", pattern), ("$options", flags)])
808 return {'$regularExpression': SON([("pattern", pattern),
809 ("options", flags)])}
810 if isinstance(obj, MinKey):
811 return {"$minKey": 1}
812 if isinstance(obj, MaxKey):
813 return {"$maxKey": 1}
814 if isinstance(obj, Timestamp):
815 return {"$timestamp": SON([("t", obj.time), ("i", obj.inc)])}
816 if isinstance(obj, Code):
817 if obj.scope is None:
818 return {'$code': str(obj)}
819 return SON([
820 ('$code', str(obj)),
821 ('$scope', _json_convert(obj.scope, json_options))])
822 if isinstance(obj, Binary):
823 return _encode_binary(obj, obj.subtype, json_options)
824 if PY3 and isinstance(obj, bytes):
825 return _encode_binary(obj, 0, json_options)
826 if isinstance(obj, uuid.UUID):
827 if json_options.strict_uuid:
828 data = obj.bytes
829 subtype = OLD_UUID_SUBTYPE
830 if json_options.uuid_representation == CSHARP_LEGACY:
831 data = obj.bytes_le
832 elif json_options.uuid_representation == JAVA_LEGACY:
833 data = data[7::-1] + data[:7:-1]
834 elif json_options.uuid_representation == UUID_SUBTYPE:
835 subtype = UUID_SUBTYPE
836 return _encode_binary(data, subtype, json_options)
837 else:
838 return {"$uuid": obj.hex}
839 if isinstance(obj, Decimal128):
840 return {"$numberDecimal": str(obj)}
841 if isinstance(obj, bool):
842 return obj
843 if (json_options.json_mode == JSONMode.CANONICAL and
844 isinstance(obj, integer_types)):
845 if -2 ** 31 <= obj < 2 ** 31:
846 return {'$numberInt': text_type(obj)}
847 return {'$numberLong': text_type(obj)}
848 if json_options.json_mode != JSONMode.LEGACY and isinstance(obj, float):
849 if math.isnan(obj):
850 return {'$numberDouble': 'NaN'}
851 elif math.isinf(obj):
852 representation = 'Infinity' if obj > 0 else '-Infinity'
853 return {'$numberDouble': representation}
854 elif json_options.json_mode == JSONMode.CANONICAL:
855 # repr() will return the shortest string guaranteed to produce the
856 # original value, when float() is called on it. str produces a
857 # shorter string in Python 2.
858 return {'$numberDouble': text_type(repr(obj))}
859 raise TypeError("%r is not JSON serializable" % obj)
+0
-50
mockupdb/_bson/max_key.py less more
0 # Copyright 2010-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Representation for the MongoDB internal MaxKey type.
15 """
16
17
18 class MaxKey(object):
19 """MongoDB internal MaxKey type.
20
21 .. versionchanged:: 2.7
22 ``MaxKey`` now implements comparison operators.
23 """
24
25 _type_marker = 127
26
27 def __eq__(self, other):
28 return isinstance(other, MaxKey)
29
30 def __hash__(self):
31 return hash(self._type_marker)
32
33 def __ne__(self, other):
34 return not self == other
35
36 def __le__(self, other):
37 return isinstance(other, MaxKey)
38
39 def __lt__(self, dummy):
40 return False
41
42 def __ge__(self, dummy):
43 return True
44
45 def __gt__(self, other):
46 return not isinstance(other, MaxKey)
47
48 def __repr__(self):
49 return "MaxKey()"
+0
-50
mockupdb/_bson/min_key.py less more
0 # Copyright 2010-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Representation for the MongoDB internal MinKey type.
15 """
16
17
18 class MinKey(object):
19 """MongoDB internal MinKey type.
20
21 .. versionchanged:: 2.7
22 ``MinKey`` now implements comparison operators.
23 """
24
25 _type_marker = 255
26
27 def __eq__(self, other):
28 return isinstance(other, MinKey)
29
30 def __hash__(self):
31 return hash(self._type_marker)
32
33 def __ne__(self, other):
34 return not self == other
35
36 def __le__(self, dummy):
37 return True
38
39 def __lt__(self, other):
40 return not isinstance(other, MinKey)
41
42 def __ge__(self, other):
43 return isinstance(other, MinKey)
44
45 def __gt__(self, dummy):
46 return False
47
48 def __repr__(self):
49 return "MinKey()"
+0
-292
mockupdb/_bson/objectid.py less more
0 # Copyright 2009-2015 MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for working with MongoDB `ObjectIds
15 <http://dochub.mongodb.org/core/objectids>`_.
16 """
17
18 import binascii
19 import calendar
20 import datetime
21 import hashlib
22 import os
23 import random
24 import socket
25 import struct
26 import threading
27 import time
28
29 from mockupdb._bson.errors import InvalidId
30 from mockupdb._bson.py3compat import PY3, bytes_from_hex, string_type, text_type
31 from mockupdb._bson.tz_util import utc
32
33
34 def _machine_bytes():
35 """Get the machine portion of an ObjectId.
36 """
37 machine_hash = hashlib.md5()
38 if PY3:
39 # gethostname() returns a unicode string in python 3.x
40 # while update() requires a byte string.
41 machine_hash.update(socket.gethostname().encode())
42 else:
43 # Calling encode() here will fail with non-ascii hostnames
44 machine_hash.update(socket.gethostname())
45 return machine_hash.digest()[0:3]
46
47
48 def _raise_invalid_id(oid):
49 raise InvalidId(
50 "%r is not a valid ObjectId, it must be a 12-byte input"
51 " or a 24-character hex string" % oid)
52
53
54 class ObjectId(object):
55 """A MongoDB ObjectId.
56 """
57
58 _inc = random.randint(0, 0xFFFFFF)
59 _inc_lock = threading.Lock()
60
61 _machine_bytes = _machine_bytes()
62
63 __slots__ = ('__id')
64
65 _type_marker = 7
66
67 def __init__(self, oid=None):
68 """Initialize a new ObjectId.
69
70 An ObjectId is a 12-byte unique identifier consisting of:
71
72 - a 4-byte value representing the seconds since the Unix epoch,
73 - a 3-byte machine identifier,
74 - a 2-byte process id, and
75 - a 3-byte counter, starting with a random value.
76
77 By default, ``ObjectId()`` creates a new unique identifier. The
78 optional parameter `oid` can be an :class:`ObjectId`, or any 12
79 :class:`bytes` or, in Python 2, any 12-character :class:`str`.
80
81 For example, the 12 bytes b'foo-bar-quux' do not follow the ObjectId
82 specification but they are acceptable input::
83
84 >>> ObjectId(b'foo-bar-quux')
85 ObjectId('666f6f2d6261722d71757578')
86
87 `oid` can also be a :class:`unicode` or :class:`str` of 24 hex digits::
88
89 >>> ObjectId('0123456789ab0123456789ab')
90 ObjectId('0123456789ab0123456789ab')
91 >>>
92 >>> # A u-prefixed unicode literal:
93 >>> ObjectId(u'0123456789ab0123456789ab')
94 ObjectId('0123456789ab0123456789ab')
95
96 Raises :class:`~bson.errors.InvalidId` if `oid` is not 12 bytes nor
97 24 hex digits, or :class:`TypeError` if `oid` is not an accepted type.
98
99 :Parameters:
100 - `oid` (optional): a valid ObjectId.
101
102 .. mongodoc:: objectids
103 """
104 if oid is None:
105 self.__generate()
106 elif isinstance(oid, bytes) and len(oid) == 12:
107 self.__id = oid
108 else:
109 self.__validate(oid)
110
111 @classmethod
112 def from_datetime(cls, generation_time):
113 """Create a dummy ObjectId instance with a specific generation time.
114
115 This method is useful for doing range queries on a field
116 containing :class:`ObjectId` instances.
117
118 .. warning::
119 It is not safe to insert a document containing an ObjectId
120 generated using this method. This method deliberately
121 eliminates the uniqueness guarantee that ObjectIds
122 generally provide. ObjectIds generated with this method
123 should be used exclusively in queries.
124
125 `generation_time` will be converted to UTC. Naive datetime
126 instances will be treated as though they already contain UTC.
127
128 An example using this helper to get documents where ``"_id"``
129 was generated before January 1, 2010 would be:
130
131 >>> gen_time = datetime.datetime(2010, 1, 1)
132 >>> dummy_id = ObjectId.from_datetime(gen_time)
133 >>> result = collection.find({"_id": {"$lt": dummy_id}})
134
135 :Parameters:
136 - `generation_time`: :class:`~datetime.datetime` to be used
137 as the generation time for the resulting ObjectId.
138 """
139 if generation_time.utcoffset() is not None:
140 generation_time = generation_time - generation_time.utcoffset()
141 timestamp = calendar.timegm(generation_time.timetuple())
142 oid = struct.pack(
143 ">i", int(timestamp)) + b"\x00\x00\x00\x00\x00\x00\x00\x00"
144 return cls(oid)
145
146 @classmethod
147 def is_valid(cls, oid):
148 """Checks if a `oid` string is valid or not.
149
150 :Parameters:
151 - `oid`: the object id to validate
152
153 .. versionadded:: 2.3
154 """
155 if not oid:
156 return False
157
158 try:
159 ObjectId(oid)
160 return True
161 except (InvalidId, TypeError):
162 return False
163
164 def __generate(self):
165 """Generate a new value for this ObjectId.
166 """
167
168 # 4 bytes current time
169 oid = struct.pack(">i", int(time.time()))
170
171 # 3 bytes machine
172 oid += ObjectId._machine_bytes
173
174 # 2 bytes pid
175 oid += struct.pack(">H", os.getpid() % 0xFFFF)
176
177 # 3 bytes inc
178 with ObjectId._inc_lock:
179 oid += struct.pack(">i", ObjectId._inc)[1:4]
180 ObjectId._inc = (ObjectId._inc + 1) % 0xFFFFFF
181
182 self.__id = oid
183
184 def __validate(self, oid):
185 """Validate and use the given id for this ObjectId.
186
187 Raises TypeError if id is not an instance of
188 (:class:`basestring` (:class:`str` or :class:`bytes`
189 in python 3), ObjectId) and InvalidId if it is not a
190 valid ObjectId.
191
192 :Parameters:
193 - `oid`: a valid ObjectId
194 """
195 if isinstance(oid, ObjectId):
196 self.__id = oid.binary
197 # bytes or unicode in python 2, str in python 3
198 elif isinstance(oid, string_type):
199 if len(oid) == 24:
200 try:
201 self.__id = bytes_from_hex(oid)
202 except (TypeError, ValueError):
203 _raise_invalid_id(oid)
204 else:
205 _raise_invalid_id(oid)
206 else:
207 raise TypeError("id must be an instance of (bytes, %s, ObjectId), "
208 "not %s" % (text_type.__name__, type(oid)))
209
210 @property
211 def binary(self):
212 """12-byte binary representation of this ObjectId.
213 """
214 return self.__id
215
216 @property
217 def generation_time(self):
218 """A :class:`datetime.datetime` instance representing the time of
219 generation for this :class:`ObjectId`.
220
221 The :class:`datetime.datetime` is timezone aware, and
222 represents the generation time in UTC. It is precise to the
223 second.
224 """
225 timestamp = struct.unpack(">i", self.__id[0:4])[0]
226 return datetime.datetime.fromtimestamp(timestamp, utc)
227
228 def __getstate__(self):
229 """return value of object for pickling.
230 needed explicitly because __slots__() defined.
231 """
232 return self.__id
233
234 def __setstate__(self, value):
235 """explicit state set from pickling
236 """
237 # Provide backwards compatability with OIDs
238 # pickled with pymongo-1.9 or older.
239 if isinstance(value, dict):
240 oid = value["_ObjectId__id"]
241 else:
242 oid = value
243 # ObjectIds pickled in python 2.x used `str` for __id.
244 # In python 3.x this has to be converted to `bytes`
245 # by encoding latin-1.
246 if PY3 and isinstance(oid, text_type):
247 self.__id = oid.encode('latin-1')
248 else:
249 self.__id = oid
250
251 def __str__(self):
252 if PY3:
253 return binascii.hexlify(self.__id).decode()
254 return binascii.hexlify(self.__id)
255
256 def __repr__(self):
257 return "ObjectId('%s')" % (str(self),)
258
259 def __eq__(self, other):
260 if isinstance(other, ObjectId):
261 return self.__id == other.binary
262 return NotImplemented
263
264 def __ne__(self, other):
265 if isinstance(other, ObjectId):
266 return self.__id != other.binary
267 return NotImplemented
268
269 def __lt__(self, other):
270 if isinstance(other, ObjectId):
271 return self.__id < other.binary
272 return NotImplemented
273
274 def __le__(self, other):
275 if isinstance(other, ObjectId):
276 return self.__id <= other.binary
277 return NotImplemented
278
279 def __gt__(self, other):
280 if isinstance(other, ObjectId):
281 return self.__id > other.binary
282 return NotImplemented
283
284 def __ge__(self, other):
285 if isinstance(other, ObjectId):
286 return self.__id >= other.binary
287 return NotImplemented
288
289 def __hash__(self):
290 """Get a hash value for this :class:`ObjectId`."""
291 return hash(self.__id)
+0
-96
mockupdb/_bson/py3compat.py less more
0 # Copyright 2009-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License"); you
3 # may not use this file except in compliance with the License. You
4 # may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
11 # implied. See the License for the specific language governing
12 # permissions and limitations under the License.
13
14 """Utility functions and definitions for python3 compatibility."""
15
16 import sys
17
18 PY3 = sys.version_info[0] == 3
19
20 if PY3:
21 import codecs
22 import _thread as thread
23 from io import BytesIO as StringIO
24
25 try:
26 import collections.abc as abc
27 except ImportError:
28 # PyPy3 (based on CPython 3.2)
29 import collections as abc
30
31 MAXSIZE = sys.maxsize
32
33 imap = map
34
35 def b(s):
36 # BSON and socket operations deal in binary data. In
37 # python 3 that means instances of `bytes`. In python
38 # 2.6 and 2.7 you can create an alias for `bytes` using
39 # the b prefix (e.g. b'foo').
40 # See http://python3porting.com/problems.html#nicer-solutions
41 return codecs.latin_1_encode(s)[0]
42
43 def bytes_from_hex(h):
44 return bytes.fromhex(h)
45
46 def iteritems(d):
47 return iter(d.items())
48
49 def itervalues(d):
50 return iter(d.values())
51
52 def reraise(exctype, value, trace=None):
53 raise exctype(str(value)).with_traceback(trace)
54
55 def _unicode(s):
56 return s
57
58 text_type = str
59 string_type = str
60 integer_types = int
61 else:
62 import collections as abc
63 import thread
64
65 from itertools import imap
66 try:
67 from cStringIO import StringIO
68 except ImportError:
69 from StringIO import StringIO
70
71 MAXSIZE = sys.maxint
72
73 def b(s):
74 # See comments above. In python 2.x b('foo') is just 'foo'.
75 return s
76
77 def bytes_from_hex(h):
78 return h.decode('hex')
79
80 def iteritems(d):
81 return d.iteritems()
82
83 def itervalues(d):
84 return d.itervalues()
85
86 # "raise x, y, z" raises SyntaxError in Python 3
87 exec("""def reraise(exctype, value, trace=None):
88 raise exctype, str(value), trace
89 """)
90
91 _unicode = unicode
92
93 string_type = basestring
94 text_type = unicode
95 integer_types = (int, long)
+0
-104
mockupdb/_bson/raw_bson.py less more
0 # Copyright 2015-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for representing raw BSON documents.
15 """
16
17 from mockupdb._bson import _UNPACK_INT, _iterate_elements
18 from mockupdb._bson.py3compat import abc, iteritems
19 from mockupdb._bson.codec_options import (
20 DEFAULT_CODEC_OPTIONS as DEFAULT, _RAW_BSON_DOCUMENT_MARKER)
21 from mockupdb._bson.errors import InvalidBSON
22
23
24 class RawBSONDocument(abc.Mapping):
25 """Representation for a MongoDB document that provides access to the raw
26 BSON bytes that compose it.
27
28 Only when a field is accessed or modified within the document does
29 RawBSONDocument decode its bytes.
30 """
31
32 __slots__ = ('__raw', '__inflated_doc', '__codec_options')
33 _type_marker = _RAW_BSON_DOCUMENT_MARKER
34
35 def __init__(self, bson_bytes, codec_options=None):
36 """Create a new :class:`RawBSONDocument`.
37
38 :Parameters:
39 - `bson_bytes`: the BSON bytes that compose this document
40 - `codec_options` (optional): An instance of
41 :class:`~bson.codec_options.CodecOptions`.
42
43 .. versionchanged:: 3.5
44 If a :class:`~bson.codec_options.CodecOptions` is passed in, its
45 `document_class` must be :class:`RawBSONDocument`.
46 """
47 self.__raw = bson_bytes
48 self.__inflated_doc = None
49 # Can't default codec_options to DEFAULT_RAW_BSON_OPTIONS in signature,
50 # it refers to this class RawBSONDocument.
51 if codec_options is None:
52 codec_options = DEFAULT_RAW_BSON_OPTIONS
53 elif codec_options.document_class is not RawBSONDocument:
54 raise TypeError(
55 "RawBSONDocument cannot use CodecOptions with document "
56 "class %s" % (codec_options.document_class, ))
57 self.__codec_options = codec_options
58
59 @property
60 def raw(self):
61 """The raw BSON bytes composing this document."""
62 return self.__raw
63
64 def items(self):
65 """Lazily decode and iterate elements in this document."""
66 return iteritems(self.__inflated)
67
68 @property
69 def __inflated(self):
70 if self.__inflated_doc is None:
71 # We already validated the object's size when this document was
72 # created, so no need to do that again. We still need to check the
73 # size of all the elements and compare to the document size.
74 object_size = _UNPACK_INT(self.__raw[:4])[0] - 1
75 position = 0
76 self.__inflated_doc = {}
77 for key, value, position in _iterate_elements(
78 self.__raw, 4, object_size, self.__codec_options):
79 self.__inflated_doc[key] = value
80 if position != object_size:
81 raise InvalidBSON('bad object or element length')
82 return self.__inflated_doc
83
84 def __getitem__(self, item):
85 return self.__inflated[item]
86
87 def __iter__(self):
88 return iter(self.__inflated)
89
90 def __len__(self):
91 return len(self.__inflated)
92
93 def __eq__(self, other):
94 if isinstance(other, RawBSONDocument):
95 return self.__raw == other.raw
96 return NotImplemented
97
98 def __repr__(self):
99 return ("RawBSONDocument(%r, codec_options=%r)"
100 % (self.raw, self.__codec_options))
101
102
103 DEFAULT_RAW_BSON_OPTIONS = DEFAULT.with_options(document_class=RawBSONDocument)
+0
-128
mockupdb/_bson/regex.py less more
0 # Copyright 2013-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for representing MongoDB regular expressions.
15 """
16
17 import re
18
19 from mockupdb._bson.son import RE_TYPE
20 from mockupdb._bson.py3compat import string_type, text_type
21
22
23 def str_flags_to_int(str_flags):
24 flags = 0
25 if "i" in str_flags:
26 flags |= re.IGNORECASE
27 if "l" in str_flags:
28 flags |= re.LOCALE
29 if "m" in str_flags:
30 flags |= re.MULTILINE
31 if "s" in str_flags:
32 flags |= re.DOTALL
33 if "u" in str_flags:
34 flags |= re.UNICODE
35 if "x" in str_flags:
36 flags |= re.VERBOSE
37
38 return flags
39
40
41 class Regex(object):
42 """BSON regular expression data."""
43 _type_marker = 11
44
45 @classmethod
46 def from_native(cls, regex):
47 """Convert a Python regular expression into a ``Regex`` instance.
48
49 Note that in Python 3, a regular expression compiled from a
50 :class:`str` has the ``re.UNICODE`` flag set. If it is undesirable
51 to store this flag in a BSON regular expression, unset it first::
52
53 >>> pattern = re.compile('.*')
54 >>> regex = Regex.from_native(pattern)
55 >>> regex.flags ^= re.UNICODE
56 >>> db.collection.insert({'pattern': regex})
57
58 :Parameters:
59 - `regex`: A regular expression object from ``re.compile()``.
60
61 .. warning::
62 Python regular expressions use a different syntax and different
63 set of flags than MongoDB, which uses `PCRE`_. A regular
64 expression retrieved from the server may not compile in
65 Python, or may match a different set of strings in Python than
66 when used in a MongoDB query.
67
68 .. _PCRE: http://www.pcre.org/
69 """
70 if not isinstance(regex, RE_TYPE):
71 raise TypeError(
72 "regex must be a compiled regular expression, not %s"
73 % type(regex))
74
75 return Regex(regex.pattern, regex.flags)
76
77 def __init__(self, pattern, flags=0):
78 """BSON regular expression data.
79
80 This class is useful to store and retrieve regular expressions that are
81 incompatible with Python's regular expression dialect.
82
83 :Parameters:
84 - `pattern`: string
85 - `flags`: (optional) an integer bitmask, or a string of flag
86 characters like "im" for IGNORECASE and MULTILINE
87 """
88 if not isinstance(pattern, (text_type, bytes)):
89 raise TypeError("pattern must be a string, not %s" % type(pattern))
90 self.pattern = pattern
91
92 if isinstance(flags, string_type):
93 self.flags = str_flags_to_int(flags)
94 elif isinstance(flags, int):
95 self.flags = flags
96 else:
97 raise TypeError(
98 "flags must be a string or int, not %s" % type(flags))
99
100 def __eq__(self, other):
101 if isinstance(other, Regex):
102 return self.pattern == other.pattern and self.flags == other.flags
103 else:
104 return NotImplemented
105
106 __hash__ = None
107
108 def __ne__(self, other):
109 return not self == other
110
111 def __repr__(self):
112 return "Regex(%r, %r)" % (self.pattern, self.flags)
113
114 def try_compile(self):
115 """Compile this :class:`Regex` as a Python regular expression.
116
117 .. warning::
118 Python regular expressions use a different syntax and different
119 set of flags than MongoDB, which uses `PCRE`_. A regular
120 expression retrieved from the server may not compile in
121 Python, or may match a different set of strings in Python than
122 when used in a MongoDB query. :meth:`try_compile()` may raise
123 :exc:`re.error`.
124
125 .. _PCRE: http://www.pcre.org/
126 """
127 return re.compile(self.pattern, self.flags)
+0
-200
mockupdb/_bson/son.py less more
0 # Copyright 2009-present MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for creating and manipulating SON, the Serialized Ocument Notation.
15
16 Regular dictionaries can be used instead of SON objects, but not when the order
17 of keys is important. A SON object can be used just like a normal Python
18 dictionary."""
19
20 import copy
21 import re
22
23 from mockupdb._bson.py3compat import abc, iteritems
24
25
26 # This sort of sucks, but seems to be as good as it gets...
27 # This is essentially the same as re._pattern_type
28 RE_TYPE = type(re.compile(""))
29
30
31 class SON(dict):
32 """SON data.
33
34 A subclass of dict that maintains ordering of keys and provides a
35 few extra niceties for dealing with SON. SON provides an API
36 similar to collections.OrderedDict from Python 2.7+.
37 """
38
39 def __init__(self, data=None, **kwargs):
40 self.__keys = []
41 dict.__init__(self)
42 self.update(data)
43 self.update(kwargs)
44
45 def __new__(cls, *args, **kwargs):
46 instance = super(SON, cls).__new__(cls, *args, **kwargs)
47 instance.__keys = []
48 return instance
49
50 def __repr__(self):
51 result = []
52 for key in self.__keys:
53 result.append("(%r, %r)" % (key, self[key]))
54 return "SON([%s])" % ", ".join(result)
55
56 def __setitem__(self, key, value):
57 if key not in self.__keys:
58 self.__keys.append(key)
59 dict.__setitem__(self, key, value)
60
61 def __delitem__(self, key):
62 self.__keys.remove(key)
63 dict.__delitem__(self, key)
64
65 def keys(self):
66 return list(self.__keys)
67
68 def copy(self):
69 other = SON()
70 other.update(self)
71 return other
72
73 # TODO this is all from UserDict.DictMixin. it could probably be made more
74 # efficient.
75 # second level definitions support higher levels
76 def __iter__(self):
77 for k in self.__keys:
78 yield k
79
80 def has_key(self, key):
81 return key in self.__keys
82
83 # third level takes advantage of second level definitions
84 def iteritems(self):
85 for k in self:
86 yield (k, self[k])
87
88 def iterkeys(self):
89 return self.__iter__()
90
91 # fourth level uses definitions from lower levels
92 def itervalues(self):
93 for _, v in self.iteritems():
94 yield v
95
96 def values(self):
97 return [v for _, v in self.iteritems()]
98
99 def items(self):
100 return [(key, self[key]) for key in self]
101
102 def clear(self):
103 self.__keys = []
104 super(SON, self).clear()
105
106 def setdefault(self, key, default=None):
107 try:
108 return self[key]
109 except KeyError:
110 self[key] = default
111 return default
112
113 def pop(self, key, *args):
114 if len(args) > 1:
115 raise TypeError("pop expected at most 2 arguments, got "\
116 + repr(1 + len(args)))
117 try:
118 value = self[key]
119 except KeyError:
120 if args:
121 return args[0]
122 raise
123 del self[key]
124 return value
125
126 def popitem(self):
127 try:
128 k, v = next(self.iteritems())
129 except StopIteration:
130 raise KeyError('container is empty')
131 del self[k]
132 return (k, v)
133
134 def update(self, other=None, **kwargs):
135 # Make progressively weaker assumptions about "other"
136 if other is None:
137 pass
138 elif hasattr(other, 'iteritems'): # iteritems saves memory and lookups
139 for k, v in other.iteritems():
140 self[k] = v
141 elif hasattr(other, 'keys'):
142 for k in other.keys():
143 self[k] = other[k]
144 else:
145 for k, v in other:
146 self[k] = v
147 if kwargs:
148 self.update(kwargs)
149
150 def get(self, key, default=None):
151 try:
152 return self[key]
153 except KeyError:
154 return default
155
156 def __eq__(self, other):
157 """Comparison to another SON is order-sensitive while comparison to a
158 regular dictionary is order-insensitive.
159 """
160 if isinstance(other, SON):
161 return len(self) == len(other) and self.items() == other.items()
162 return self.to_dict() == other
163
164 def __ne__(self, other):
165 return not self == other
166
167 def __len__(self):
168 return len(self.__keys)
169
170 def to_dict(self):
171 """Convert a SON document to a normal Python dictionary instance.
172
173 This is trickier than just *dict(...)* because it needs to be
174 recursive.
175 """
176
177 def transform_value(value):
178 if isinstance(value, list):
179 return [transform_value(v) for v in value]
180 elif isinstance(value, abc.Mapping):
181 return dict([
182 (k, transform_value(v))
183 for k, v in iteritems(value)])
184 else:
185 return value
186
187 return transform_value(dict(self))
188
189 def __deepcopy__(self, memo):
190 out = SON()
191 val_id = id(self)
192 if val_id in memo:
193 return memo.get(val_id)
194 memo[val_id] = out
195 for k, v in self.iteritems():
196 if not isinstance(v, RE_TYPE):
197 v = copy.deepcopy(v, memo)
198 out[k] = v
199 return out
+0
-120
mockupdb/_bson/timestamp.py less more
0 # Copyright 2010-2015 MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Tools for representing MongoDB internal Timestamps.
15 """
16
17 import calendar
18 import datetime
19
20 from mockupdb._bson.py3compat import integer_types
21 from mockupdb._bson.tz_util import utc
22
23 UPPERBOUND = 4294967296
24
25
26 class Timestamp(object):
27 """MongoDB internal timestamps used in the opLog.
28 """
29
30 _type_marker = 17
31
32 def __init__(self, time, inc):
33 """Create a new :class:`Timestamp`.
34
35 This class is only for use with the MongoDB opLog. If you need
36 to store a regular timestamp, please use a
37 :class:`~datetime.datetime`.
38
39 Raises :class:`TypeError` if `time` is not an instance of
40 :class: `int` or :class:`~datetime.datetime`, or `inc` is not
41 an instance of :class:`int`. Raises :class:`ValueError` if
42 `time` or `inc` is not in [0, 2**32).
43
44 :Parameters:
45 - `time`: time in seconds since epoch UTC, or a naive UTC
46 :class:`~datetime.datetime`, or an aware
47 :class:`~datetime.datetime`
48 - `inc`: the incrementing counter
49 """
50 if isinstance(time, datetime.datetime):
51 if time.utcoffset() is not None:
52 time = time - time.utcoffset()
53 time = int(calendar.timegm(time.timetuple()))
54 if not isinstance(time, integer_types):
55 raise TypeError("time must be an instance of int")
56 if not isinstance(inc, integer_types):
57 raise TypeError("inc must be an instance of int")
58 if not 0 <= time < UPPERBOUND:
59 raise ValueError("time must be contained in [0, 2**32)")
60 if not 0 <= inc < UPPERBOUND:
61 raise ValueError("inc must be contained in [0, 2**32)")
62
63 self.__time = time
64 self.__inc = inc
65
66 @property
67 def time(self):
68 """Get the time portion of this :class:`Timestamp`.
69 """
70 return self.__time
71
72 @property
73 def inc(self):
74 """Get the inc portion of this :class:`Timestamp`.
75 """
76 return self.__inc
77
78 def __eq__(self, other):
79 if isinstance(other, Timestamp):
80 return (self.__time == other.time and self.__inc == other.inc)
81 else:
82 return NotImplemented
83
84 def __hash__(self):
85 return hash(self.time) ^ hash(self.inc)
86
87 def __ne__(self, other):
88 return not self == other
89
90 def __lt__(self, other):
91 if isinstance(other, Timestamp):
92 return (self.time, self.inc) < (other.time, other.inc)
93 return NotImplemented
94
95 def __le__(self, other):
96 if isinstance(other, Timestamp):
97 return (self.time, self.inc) <= (other.time, other.inc)
98 return NotImplemented
99
100 def __gt__(self, other):
101 if isinstance(other, Timestamp):
102 return (self.time, self.inc) > (other.time, other.inc)
103 return NotImplemented
104
105 def __ge__(self, other):
106 if isinstance(other, Timestamp):
107 return (self.time, self.inc) >= (other.time, other.inc)
108 return NotImplemented
109
110 def __repr__(self):
111 return "Timestamp(%s, %s)" % (self.__time, self.__inc)
112
113 def as_datetime(self):
114 """Return a :class:`~datetime.datetime` instance corresponding
115 to the time portion of this :class:`Timestamp`.
116
117 The returned datetime's timezone is UTC.
118 """
119 return datetime.datetime.fromtimestamp(self.__time, utc)
+0
-52
mockupdb/_bson/tz_util.py less more
0 # Copyright 2010-2015 MongoDB, Inc.
1 #
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Timezone related utilities for BSON."""
15
16 from datetime import (timedelta,
17 tzinfo)
18
19 ZERO = timedelta(0)
20
21
22 class FixedOffset(tzinfo):
23 """Fixed offset timezone, in minutes east from UTC.
24
25 Implementation based from the Python `standard library documentation
26 <http://docs.python.org/library/datetime.html#tzinfo-objects>`_.
27 Defining __getinitargs__ enables pickling / copying.
28 """
29
30 def __init__(self, offset, name):
31 if isinstance(offset, timedelta):
32 self.__offset = offset
33 else:
34 self.__offset = timedelta(minutes=offset)
35 self.__name = name
36
37 def __getinitargs__(self):
38 return self.__offset, self.__name
39
40 def utcoffset(self, dt):
41 return self.__offset
42
43 def tzname(self, dt):
44 return self.__name
45
46 def dst(self, dt):
47 return ZERO
48
49
50 utc = FixedOffset(0, "UTC")
51 """Fixed offset timezone representing UTC."""
00 Metadata-Version: 1.1
11 Name: mockupdb
2 Version: 1.4.1
2 Version: 1.7.0
33 Summary: MongoDB Wire Protocol server library
44 Home-page: https://github.com/ajdavis/mongo-mockup-db
55 Author: A. Jesse Jiryu Davis
2020
2121 Changelog
2222 =========
23
24 1.7.0 (2018-12-02)
25 ------------------
26
27 Improve datetime support in match expressions. Python datetimes have microsecond
28 precision but BSON only has milliseconds, so expressions like this always
29 failed::
30
31 server.receives(Command('foo', when=datetime(2018, 12, 1, 6, 6, 6, 12345)))
32
33 Now, the matching logic has been rewritten to recurse through arrays and
34 subdocuments, comparing them value by value. It compares datetime values with
35 only millisecond precision.
36
37 1.6.0 (2018-11-16)
38 ------------------
39
40 Remove vendored BSON library. Instead, require PyMongo and use its BSON library.
41 This avoids surprising problems where a BSON type created with PyMongo does not
42 appear equal to one created with MockupDB, and it avoids the occasional need to
43 update the vendored code to support new BSON features.
44
45 1.5.0 (2018-11-02)
46 ------------------
47
48 Support for Unix domain paths with ``uds_path`` parameter.
49
50 The ``interactive_server()`` function now prepares the server to autorespond to
51 the ``getFreeMonitoringStatus`` command from the mongo shell.
2352
2453 1.4.1 (2018-06-30)
2554 ------------------
107136
108137 Keywords: mongo,mongodb,wire protocol,mockupdb,mock
109138 Platform: UNKNOWN
110 Classifier: Development Status :: 2 - Pre-Alpha
139 Classifier: Development Status :: 5 - Production/Stable
111140 Classifier: Intended Audience :: Developers
112141 Classifier: License :: OSI Approved :: Apache Software License
113142 Classifier: Natural Language :: English
2222 mockupdb.egg-info/SOURCES.txt
2323 mockupdb.egg-info/dependency_links.txt
2424 mockupdb.egg-info/not-zip-safe
25 mockupdb.egg-info/requires.txt
2526 mockupdb.egg-info/top_level.txt
26 mockupdb/_bson/__init__.py
27 mockupdb/_bson/binary.py
28 mockupdb/_bson/code.py
29 mockupdb/_bson/codec_options.py
30 mockupdb/_bson/dbref.py
31 mockupdb/_bson/decimal128.py
32 mockupdb/_bson/errors.py
33 mockupdb/_bson/int64.py
34 mockupdb/_bson/json_util.py
35 mockupdb/_bson/max_key.py
36 mockupdb/_bson/min_key.py
37 mockupdb/_bson/objectid.py
38 mockupdb/_bson/py3compat.py
39 mockupdb/_bson/raw_bson.py
40 mockupdb/_bson/regex.py
41 mockupdb/_bson/son.py
42 mockupdb/_bson/timestamp.py
43 mockupdb/_bson/tz_util.py
4427 tests/__init__.py
4528 tests/test_mockupdb.py
1414 with open('CHANGELOG.rst') as changelog_file:
1515 changelog = changelog_file.read().replace('.. :changelog:', '')
1616
17 requirements = []
18 test_requirements = ['pymongo>=3']
17 requirements = ['pymongo>=3']
18 test_requirements = []
1919
2020 if sys.version_info[:2] == (2, 6):
2121 requirements.append('ordereddict')
2323
2424 setup(
2525 name='mockupdb',
26 version='1.4.1',
26 version='1.7.0',
2727 description="MongoDB Wire Protocol server library",
2828 long_description=readme + '\n\n' + changelog,
2929 author="A. Jesse Jiryu Davis",
3737 zip_safe=False,
3838 keywords=["mongo", "mongodb", "wire protocol", "mockupdb", "mock"],
3939 classifiers=[
40 'Development Status :: 2 - Pre-Alpha',
40 'Development Status :: 5 - Production/Stable',
4141 'Intended Audience :: Developers',
4242 "License :: OSI Approved :: Apache Software License",
4343 'Natural Language :: English',
33 """Test MockupDB."""
44
55 import contextlib
6 import datetime
7 import os
68 import ssl
79 import sys
10 import tempfile
811
912 if sys.version_info[0] < 3:
1013 from io import BytesIO as StringIO
1619 except ImportError:
1720 from Queue import Queue
1821
19 # Tests depend on PyMongo's BSON implementation, but MockupDB itself does not.
2022 from bson import (Binary, Code, DBRef, Decimal128, MaxKey, MinKey, ObjectId,
2123 Regex, SON, Timestamp)
2224 from bson.codec_options import CodecOptions
2325 from pymongo import MongoClient, message, WriteConcern
2426
25 from mockupdb import (_bson as mockup_bson, go, going,
26 Command, CommandBase, Matcher, MockupDB, Request,
27 OpInsert, OpMsg, OpQuery, QUERY_FLAGS)
27 from mockupdb import (go, going, Command, CommandBase, Matcher, MockupDB,
28 Request, OpInsert, OpMsg, OpQuery, QUERY_FLAGS)
2829 from tests import unittest # unittest2 on Python 2.6.
2930
3031
208209 _id = '5a918f9fa08bff9c7688d3e1'
209210
210211 for a, b in [
211 (Binary(b'foo'), mockup_bson.Binary(b'foo')),
212 (Code('foo'), mockup_bson.Code('foo')),
213 (Code('foo', {'x': 1}), mockup_bson.Code('foo', {'x': 1})),
214 (DBRef('coll', 1), mockup_bson.DBRef('coll', 1)),
215 (DBRef('coll', 1, 'db'), mockup_bson.DBRef('coll', 1, 'db')),
216 (Decimal128('1'), mockup_bson.Decimal128('1')),
217 (MaxKey(), mockup_bson.MaxKey()),
218 (MinKey(), mockup_bson.MinKey()),
219 (ObjectId(_id), mockup_bson.ObjectId(_id)),
220 (Regex('foo', 'i'), mockup_bson.Regex('foo', 'i')),
221 (Timestamp(1, 2), mockup_bson.Timestamp(1, 2)),
212 (Binary(b'foo'), Binary(b'foo')),
213 (Code('foo'), Code('foo')),
214 (Code('foo', {'x': 1}), Code('foo', {'x': 1})),
215 (DBRef('coll', 1), DBRef('coll', 1)),
216 (DBRef('coll', 1, 'db'), DBRef('coll', 1, 'db')),
217 (Decimal128('1'), Decimal128('1')),
218 (MaxKey(), MaxKey()),
219 (MinKey(), MinKey()),
220 (ObjectId(_id), ObjectId(_id)),
221 (Regex('foo', 'i'), Regex('foo', 'i')),
222 (Timestamp(1, 2), Timestamp(1, 2)),
222223 ]:
223224 # Basic case.
224225 self.assertTrue(
239240 Matcher(Command('x', y=b)).matches(Command('x', y=a)),
240241 "PyMongo %r != MockupDB %r" % (a, b))
241242
243 def test_datetime(self):
244 server = MockupDB(auto_ismaster=True)
245 server.run()
246 client = MongoClient(server.uri)
247 # Python datetimes have microsecond precision, BSON only millisecond.
248 # Ensure this datetime matches itself despite the truncation.
249 dt = datetime.datetime(2018, 12, 1, 6, 6, 6, 12345)
250 doc = SON([('_id', 1), ('dt', dt)])
251 with going(client.db.collection.insert_one, doc):
252 server.receives(
253 OpMsg('insert', 'collection', documents=[doc])).ok()
254
242255
243256 class TestAutoresponds(unittest.TestCase):
244257 def test_auto_dequeue(self):
317330 self.assertEqual(ismaster['minWireVersion'], 1)
318331 self.assertEqual(ismaster['maxWireVersion'], 42)
319332
333 @unittest.skipIf(sys.platform == 'win32', 'Windows')
334 def test_unix_domain_socket(self):
335 tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.sock')
336 tmp.close()
337 server = MockupDB(auto_ismaster={'maxWireVersion': 3},
338 uds_path=tmp.name)
339 server.run()
340 self.assertTrue(server.uri.endswith('.sock'),
341 'Expected URI "%s" to end with ".sock"' % (server.uri,))
342 self.assertEqual(server.host, tmp.name)
343 self.assertEqual(server.port, 0)
344 self.assertEqual(server.address, (tmp.name, 0))
345 self.assertEqual(server.address_string, tmp.name)
346 client = MongoClient(server.uri)
347 with going(client.test.command, {'foo': 1}) as future:
348 server.receives().ok()
349
350 response = future()
351 self.assertEqual(1, response['ok'])
352 server.stop()
353 self.assertFalse(os.path.exists(tmp.name))
354
320355
321356 class TestResponse(unittest.TestCase):
322357 def test_ok(self):