Codebase list python-mockupdb / d5e20b0
record new upstream branch created by importing python-mockupdb_1.4.1.orig.tar.gz and merge it Ondřej Nový 5 years ago
23 changed file(s) with 1907 addition(s) and 436 deletion(s). Raw diff Collapse all Expand all
11
22 Changelog
33 =========
4
5 1.4.1 (2018-06-30)
6 ------------------
7
8 Fix an inadvertent dependency on PyMongo, which broke the docs build.
9
10 1.4.0 (2018-06-29)
11 ------------------
12
13 Support, and expect, OP_MSG requests from clients. Thanks to Shane Harvey for
14 the contribution.
15
16 Update vendored bson library from PyMongo. Support the Decimal128 BSON type. Fix
17 Matcher so it equates BSON objects from PyMongo like ``ObjectId(...)`` with
18 equivalent objects created from MockupDB's vendored bson library.
419
520 1.3.0 (2018-02-19)
621 ------------------
00 Metadata-Version: 1.1
11 Name: mockupdb
2 Version: 1.3.0
2 Version: 1.4.1
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.4.1 (2018-06-30)
25 ------------------
26
27 Fix an inadvertent dependency on PyMongo, which broke the docs build.
28
29 1.4.0 (2018-06-29)
30 ------------------
31
32 Support, and expect, OP_MSG requests from clients. Thanks to Shane Harvey for
33 the contribution.
34
35 Update vendored bson library from PyMongo. Support the Decimal128 BSON type. Fix
36 Matcher so it equates BSON objects from PyMongo like ``ObjectId(...)`` with
37 equivalent objects created from MockupDB's vendored bson library.
2338
2439 1.3.0 (2018-02-19)
2540 ------------------
00 # see git-dpm(1) from git-dpm package
1 e7cbf57d95e3b940244cb64d1cab877837774749
2 e7cbf57d95e3b940244cb64d1cab877837774749
3 e7cbf57d95e3b940244cb64d1cab877837774749
4 e7cbf57d95e3b940244cb64d1cab877837774749
5 python-mockupdb_1.3.0.orig.tar.gz
6 77e66429104bcac82e6b83f5d53736315c2c398e
7 61001
1 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc
2 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc
3 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc
4 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc
5 python-mockupdb_1.4.1.orig.tar.gz
6 cca1a4cfe81f6cd78aebcf89c2c0b4b7566fba43
7 75003
88 debianTag="debian/%e%v"
99 patchedTag="patched/%e%v"
1010 upstreamTag="upstream/%e%u"
4848 (Notice that `~Request.replies` returns True. This makes more advanced uses of
4949 `~MockupDB.autoresponds` easier, see the reference document.)
5050
51 Reply To Legacy Writes
52 ----------------------
53
54 Send an unacknowledged OP_INSERT:
55
56 >>> from pymongo.write_concern import WriteConcern
57 >>> w0 = WriteConcern(w=0)
58 >>> collection = client.db.coll.with_options(write_concern=w0)
59 >>> collection.insert_one({'_id': 1}) # doctest: +ELLIPSIS
60 <pymongo.results.InsertOneResult object at ...>
61 >>> server.receives()
62 OpInsert({"_id": 1}, namespace="db.coll")
6351
6452 Reply To Write Commands
6553 -----------------------
6654
6755 If PyMongo sends an unacknowledged OP_INSERT it does not block
68 waiting for you to call `~Request.replies`. However, for acknowledge operations
56 waiting for you to call `~Request.replies`. However, for acknowledged operations
6957 it does block. Use `~test.utils.go` to defer PyMongo to a background thread so
7058 you can respond from the main thread:
7159
8169
8270 >>> cmd = server.receives()
8371 >>> cmd
84 Command({"insert": "coll", "ordered": true, "documents": [{"_id": 1}]}, namespace="db")
72 OpMsg({"insert": "coll", "ordered": true, "$db": "db", "$readPreference": {"mode": "primary"}, "documents": [{"_id": 1}]}, namespace="db")
8573
8674 (Note how MockupDB renders requests and replies as JSON, not Python.
8775 The chief differences are that "true" and "false" are lower-case, and the order
157145 ... try:
158146 ... while server.running:
159147 ... # Match queries most restrictive first.
160 ... if server.got(Command('find', 'coll', filter={'a': {'$gt': 1}})):
148 ... if server.got(OpMsg('find', 'coll', filter={'a': {'$gt': 1}})):
161149 ... server.reply(cursor={'id': 0, 'firstBatch':[{'a': 2}]})
162150 ... elif server.got('break'):
163151 ... server.ok()
164152 ... break
165 ... elif server.got(Command('find', 'coll')):
153 ... elif server.got(OpMsg('find', 'coll')):
166154 ... server.reply(
167155 ... cursor={'id': 0, 'firstBatch':[{'a': 1}, {'a': 2}]})
168156 ... else:
246234 >>> client = MongoClient(server.uri)
247235 >>> future = go(client.db.collection.insert, {'_id': 1})
248236 >>> # Assert the command name is "insert" and its parameter is "collection".
249 >>> request = server.receives(Command('insert', 'collection'))
237 >>> request = server.receives(OpMsg('insert', 'collection'))
250238 >>> request.ok()
251239 True
252240 >>> assert future()
253241
254242 If the request did not match, MockupDB would raise an `AssertionError`.
255243
256 The arguments to `Command` above are an example of a message spec. The
244 The arguments to `OpMsg` above are an example of a message spec. The
257245 pattern-matching rules are implemented in `Matcher`.
258246 Here are
259247 some more examples.
320308 >>> m.matches(OpQuery, {'_id': 1})
321309 True
322310
323 Commands are queries on some database's "database.$cmd" namespace.
324 They are specially prohibited from matching regular queries:
325
326 >>> Matcher(OpQuery).matches(Command)
327 False
328 >>> Matcher(Command).matches(Command)
329 True
330 >>> Matcher(OpQuery).matches(OpQuery)
331 True
332 >>> Matcher(Command).matches(OpQuery)
333 False
334
311 Commands in MongoDB 3.6 and later use the OP_MSG wire protocol message.
335312 The command name is matched case-insensitively:
336313
337 >>> Matcher(Command('ismaster')).matches(Command('IsMaster'))
314 >>> Matcher(OpMsg('ismaster')).matches(OpMsg('IsMaster'))
338315 True
339316
340317 You can match properties specific to certain opcodes:
404381 document and assumes the value is 1:
405382
406383 >>> import mockupdb
407 >>> mockupdb.make_reply()
408 OpReply()
409 >>> mockupdb.make_reply(0)
410 OpReply({"ok": 0})
411 >>> mockupdb.make_reply("foo")
412 OpReply({"foo": 1})
384 >>> mockupdb.make_op_msg_reply()
385 OpMsgReply()
386 >>> mockupdb.make_op_msg_reply(0)
387 OpMsgReply({"ok": 0})
388 >>> mockupdb.make_op_msg_reply("foo")
389 OpMsgReply({"foo": 1})
413390
414391 You can pass a dict or OrderedDict of fields instead of using keyword arguments.
415392 This is best for fieldnames that are not valid Python identifiers:
416393
417 >>> mockupdb.make_reply(OrderedDict([('ok', 0), ('$err', 'bad')]))
418 OpReply({"ok": 0, "$err": "bad"})
394 >>> mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')]))
395 OpMsgReply({"ok": 0, "$err": "bad"})
419396
420397 You can customize the OP_REPLY header flags with the "flags" keyword argument:
421398
422 >>> r = mockupdb.make_reply(OrderedDict([('ok', 0), ('$err', 'bad')]),
423 ... flags=REPLY_FLAGS['QueryFailure'])
399 >>> r = mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')]),
400 ... flags=OP_MSG_FLAGS['checksumPresent'])
424401 >>> repr(r)
425 'OpReply({"ok": 0, "$err": "bad"}, flags=QueryFailure)'
426
427 The above logic, which simulates a query error in MongoDB before 3.2, is
428 provided conveniently in `~Request.fail()`. This protocol is obsolete in MongoDB
429 3.2+, which uses commands for all operations.
430
431 Although these examples call `make_reply` explicitly, this is only to
402 'OpMsgReply({"ok": 0, "$err": "bad"}, flags=checksumPresent)'
403
404 Although these examples call `make_op_msg_reply` explicitly, this is only to
432405 illustrate how replies are specified. Your code will pass these arguments to a
433406 `Request` method like `~Request.replies`.
434407
454427
455428 >>> cursor = collection.find().batch_size(1)
456429 >>> future = go(next, cursor)
457 >>> server.receives(Command('find', 'coll')).fail()
430 >>> server.receives(OpMsg('find', 'coll')).command_err()
458431 True
459432 >>> future()
460433 Traceback (most recent call last):
461434 ...
462 OperationFailure: database error: MockupDB query failure
435 OperationFailure: database error: MockupDB command failure
463436
464437 You can simulate normal querying, too:
465438
466439 >>> cursor = collection.find().batch_size(2)
467440 >>> future = go(list, cursor)
468441 >>> documents = [{'_id': 1}, {'x': 2}, {'foo': 'bar'}, {'beauty': True}]
469 >>> request = server.receives(Command('find', 'coll'))
442 >>> request = server.receives(OpMsg('find', 'coll'))
470443 >>> n = request['batchSize']
471444 >>> request.replies(cursor={'id': 123, 'firstBatch': documents[:n]})
472445 True
473446 >>> while True:
474 ... getmore = server.receives(Command('getMore', 123))
447 ... getmore = server.receives(OpMsg('getMore', 123))
475448 ... n = getmore['batchSize']
476449 ... if documents:
477450 ... cursor_id = 123
1818
1919 __author__ = 'A. Jesse Jiryu Davis'
2020 __email__ = 'jesse@mongodb.com'
21 __version__ = '1.3.0'
21 __version__ = '1.4.1'
2222
2323 import atexit
24 import collections
2524 import contextlib
2625 import errno
2726 import functools
4544 from Queue import Queue, Empty
4645
4746 try:
47 from collections.abc import Mapping
48 except:
49 from collections import Mapping
50
51 try:
4852 from collections import OrderedDict
4953 except:
5054 from ordereddict import OrderedDict # Python 2.6, "pip install ordereddict"
8286 'MockupDB', 'go', 'going', 'Future', 'wait_until', 'interactive_server',
8387
8488 'OP_REPLY', 'OP_UPDATE', 'OP_INSERT', 'OP_QUERY', 'OP_GET_MORE',
85 'OP_DELETE', 'OP_KILL_CURSORS',
89 'OP_DELETE', 'OP_KILL_CURSORS', 'OP_MSG',
8690
8791 'QUERY_FLAGS', 'UPDATE_FLAGS', 'INSERT_FLAGS', 'DELETE_FLAGS',
88 'REPLY_FLAGS',
92 'REPLY_FLAGS', 'OP_MSG_FLAGS',
8993
9094 'Request', 'Command', 'OpQuery', 'OpGetMore', 'OpKillCursors', 'OpInsert',
91 'OpUpdate', 'OpDelete', 'OpReply',
95 'OpUpdate', 'OpDelete', 'OpReply', 'OpMsg',
9296
9397 'Matcher', 'absent',
9498 ]
238242 OP_GET_MORE = 2005
239243 OP_DELETE = 2006
240244 OP_KILL_CURSORS = 2007
245 OP_MSG = 2013
241246
242247 QUERY_FLAGS = OrderedDict([
243248 ('TailableCursor', 2),
262267 ('CursorNotFound', 1),
263268 ('QueryFailure', 2)])
264269
270 OP_MSG_FLAGS = OrderedDict([
271 ('checksumPresent', 1),
272 ('moreToCome', 2)])
273
274 _UNPACK_BYTE = struct.Struct("<b").unpack
265275 _UNPACK_INT = struct.Struct("<i").unpack
276 _UNPACK_UINT = struct.Struct("<I").unpack
266277 _UNPACK_LONG = struct.Struct("<q").unpack
267278
268279
270281 """Decode a BSON 'C' string to python unicode string."""
271282 end = data.index(b"\x00", position)
272283 return _utf_8_decode(data[position:end], None, True)[0], end + 1
284
285
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
273327
274328
275329 class _PeekableQueue(Queue):
297351
298352
299353 class Request(object):
300 """Base class for `Command`, `OpInsert`, and so on.
354 """Base class for `Command`, `OpMsg`, and so on.
301355
302356 Some useful asserts you can do in tests:
303357
309363 True
310364 >>> {'_id': 1} == OpInsert([{'_id': 0}, {'_id': 1}])[1]
311365 True
312 >>> 'field' in Command(field=1)
366 >>> 'field' in OpMsg(field=1)
313367 True
314 >>> 'field' in Command()
368 >>> 'field' in OpMsg()
315369 False
316 >>> 'field' in Command('ismaster')
370 >>> 'field' in OpMsg('ismaster')
317371 False
318 >>> Command(ismaster=False)['ismaster'] is False
372 >>> OpMsg(ismaster=False)['ismaster'] is False
319373 True
320374 """
321375 opcode = None
332386 self._verbose = self._server and self._server.verbose
333387 self._server_port = kwargs.pop('server_port', None)
334388 self._docs = make_docs(*args, **kwargs)
335 if not all(isinstance(doc, collections.Mapping) for doc in self._docs):
389 if not all(isinstance(doc, Mapping) for doc in self._docs):
336390 raise_args_err()
337391
338392 @property
456510 if value is absent:
457511 if key in other_doc:
458512 return False
459 elif other_doc.get(key, None) != value:
513 elif not _bson_values_equal(value, other_doc.get(key, None)):
460514 return False
461515 if isinstance(doc, (OrderedDict, _bson.SON)):
462516 if not isinstance(other_doc, (OrderedDict, _bson.SON)):
508562 parts.append('namespace="%s"' % self._namespace)
509563
510564 return '%s(%s)' % (name, ', '.join(str(part) for part in parts))
565
566
567 class CommandBase(Request):
568 """A command the client executes on the server."""
569 is_command = True
570
571 # Check command name case-insensitively.
572 _non_matched_attrs = Request._non_matched_attrs + ('command_name', )
573
574 @property
575 def command_name(self):
576 """The command name or None.
577
578 >>> OpMsg({'count': 'collection'}).command_name
579 'count'
580 >>> OpMsg('aggregate', 'collection', cursor=absent).command_name
581 'aggregate'
582 """
583 if self.docs and self.docs[0]:
584 return list(self.docs[0])[0]
585
586 def _matches_docs(self, docs, other_docs):
587 assert len(docs) == len(other_docs) == 1
588 doc, = docs
589 other_doc, = other_docs
590 items = list(doc.items())
591 other_items = list(other_doc.items())
592
593 # Compare command name case-insensitively.
594 if items and other_items:
595 if items[0][0].lower() != other_items[0][0].lower():
596 return False
597 if not _bson_values_equal(items[0][1], other_items[0][1]):
598 return False
599 return super(CommandBase, self)._matches_docs(
600 [OrderedDict(items[1:])],
601 [OrderedDict(other_items[1:])])
602
603
604 class OpMsg(CommandBase):
605 """An OP_MSG request the client executes on the server."""
606 opcode = OP_MSG
607 is_command = True
608 _flags_map = OP_MSG_FLAGS
609
610 @classmethod
611 def unpack(cls, msg, client, server, request_id):
612 """Parse message and return an `OpMsg`.
613
614 Takes the client message as bytes, the client and server socket objects,
615 and the client request id.
616 """
617 flags, = _UNPACK_UINT(msg[:4])
618 pos = 4
619 first_payload_type, = _UNPACK_BYTE(msg[pos:pos+1])
620 pos += 1
621 first_payload_size, = _UNPACK_INT(msg[pos:pos+4])
622 if flags != 0 and flags != 2:
623 raise ValueError('OP_MSG flag must be 0 or 2 not %r' % (flags,))
624 if first_payload_type != 0:
625 raise ValueError('First OP_MSG payload type must be 0 not %r' % (
626 first_payload_type,))
627
628 # 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]
631 pos += first_payload_size
632 if len(msg) != pos:
633 payload_type, = _UNPACK_BYTE(msg[pos:pos+1])
634 pos += 1
635 if payload_type != 1:
636 raise ValueError('Second OP_MSG payload type must be 1 not %r'
637 % (payload_type,))
638 section_size, = _UNPACK_INT(msg[pos:pos+4])
639 if len(msg) != pos + section_size:
640 raise ValueError('More than two OP_MSG sections unsupported')
641 pos += 4
642 identifier, pos = _get_c_string(msg, pos)
643 documents = _bson.decode_all(msg[pos:], CODEC_OPTIONS)
644 payload_document[identifier] = documents
645
646 database = payload_document['$db']
647 return OpMsg(payload_document, namespace=database, flags=flags,
648 _client=client, request_id=request_id,
649 _server=server)
650
651 def __init__(self, *args, **kwargs):
652 super(OpMsg, self).__init__(*args, **kwargs)
653 if len(self._docs) > 1:
654 raise_args_err('OpMsg too many documents', ValueError)
655
656 @property
657 def slave_ok(self):
658 """True if this OpMsg can read from a secondary."""
659 read_preference = self.doc.get('$readPreference')
660 return read_preference and read_preference.get('mode') != 'primary'
661
662 slave_okay = slave_ok
663 """Synonym for `.slave_ok`."""
664
665 @property
666 def command_name(self):
667 """The command name or None.
668
669 >>> OpMsg({'count': 'collection'}).command_name
670 'count'
671 >>> OpMsg('aggregate', 'collection', cursor=absent).command_name
672 'aggregate'
673 """
674 if self.docs and self.docs[0]:
675 return list(self.docs[0])[0]
676
677 def _replies(self, *args, **kwargs):
678 if self.flags & OP_MSG_FLAGS['moreToCome']:
679 assert False, "Cannot reply to OpMsg with moreToCome: %r" % (self,)
680 reply = make_op_msg_reply(*args, **kwargs)
681 if not reply.docs:
682 reply.docs = [{'ok': 1}]
683 else:
684 if len(reply.docs) > 1:
685 raise ValueError('OP_MSG reply with multiple documents: %s'
686 % (reply.docs, ))
687 reply.doc.setdefault('ok', 1)
688 super(OpMsg, self)._replies(reply)
511689
512690
513691 class OpQuery(Request):
554732
555733 def __init__(self, *args, **kwargs):
556734 fields = kwargs.pop('fields', None)
557 if fields is not None and not isinstance(fields, collections.Mapping):
735 if fields is not None and not isinstance(fields, Mapping):
558736 raise_args_err()
559737 self._fields = fields
560738 self._num_to_skip = kwargs.pop('num_to_skip', None)
591769 return rep + ')'
592770
593771
594 class Command(OpQuery):
772 class Command(CommandBase, OpQuery):
595773 """A command the client executes on the server."""
596 is_command = True
597
598 # Check command name case-insensitively.
599 _non_matched_attrs = OpQuery._non_matched_attrs + ('command_name', )
600
601 @property
602 def command_name(self):
603 """The command name or None.
604
605 >>> Command({'count': 'collection'}).command_name
606 'count'
607 >>> Command('aggregate', 'collection', cursor=absent).command_name
608 'aggregate'
609 """
610 if self.docs and self.docs[0]:
611 return list(self.docs[0])[0]
612
613 def _matches_docs(self, docs, other_docs):
614 assert len(docs) == len(other_docs) == 1
615 doc, = docs
616 other_doc, = other_docs
617 items = list(doc.items())
618 other_items = list(other_doc.items())
619
620 # Compare command name case-insensitively.
621 if items and other_items:
622 if items[0][0].lower() != other_items[0][0].lower():
623 return False
624 if items[0][1] != other_items[0][1]:
625 return False
626 return super(Command, self)._matches_docs(
627 [OrderedDict(items[1:])],
628 [OrderedDict(other_items[1:])])
629774
630775 def _replies(self, *args, **kwargs):
631776 reply = make_reply(*args, **kwargs)
779924 request_id=request_id, _server=server)
780925
781926
782 class OpReply(object):
927 class Reply(object):
783928 """A reply from `MockupDB` to the client."""
784929 def __init__(self, *args, **kwargs):
785930 self._flags = kwargs.pop('flags', 0)
931 self._docs = make_docs(*args, **kwargs)
932
933 @property
934 def doc(self):
935 """Contents of reply.
936
937 Useful for replies to commands; replies to other messages may have no
938 documents or multiple documents.
939 """
940 assert len(self._docs) == 1, '%s has more than one document' % self
941 return self._docs[0]
942
943 def __str__(self):
944 return docs_repr(*self._docs)
945
946 def __repr__(self):
947 rep = '%s(%s' % (self.__class__.__name__, self)
948 if self._flags:
949 rep += ', flags=' + '|'.join(
950 name for name, value in REPLY_FLAGS.items()
951 if self._flags & value)
952
953 return rep + ')'
954
955
956 class OpReply(Reply):
957 """An OP_REPLY reply from `MockupDB` to the client."""
958 def __init__(self, *args, **kwargs):
786959 self._cursor_id = kwargs.pop('cursor_id', 0)
787960 self._starting_from = kwargs.pop('starting_from', 0)
788 self._docs = make_docs(*args, **kwargs)
961 super(OpReply, self).__init__(*args, **kwargs)
789962
790963 @property
791964 def docs(self):
795968 @docs.setter
796969 def docs(self, docs):
797970 self._docs = make_docs(docs)
798
799 @property
800 def doc(self):
801 """Contents of reply.
802
803 Useful for replies to commands; replies to other messages may have no
804 documents or multiple documents.
805 """
806 assert len(self._docs) == 1, '%s has more than one document' % self
807 return self._docs[0]
808971
809972 def update(self, *args, **kwargs):
810973 """Update the document. Same as ``dict().update()``.
835998 message += struct.pack("<i", OP_REPLY)
836999 return message + data
8371000
838 def __str__(self):
839 return docs_repr(*self._docs)
1001
1002 class OpMsgReply(Reply):
1003 """A OP_MSG reply from `MockupDB` to the client."""
1004 def __init__(self, *args, **kwargs):
1005 super(OpMsgReply, self).__init__(*args, **kwargs)
1006 assert len(self._docs) <= 1, 'OpMsgReply can only have one document'
1007
1008 @property
1009 def docs(self):
1010 """The reply documents, if any."""
1011 return self._docs
1012
1013 @docs.setter
1014 def docs(self, docs):
1015 self._docs = make_docs(docs)
1016 assert len(self._docs) == 1, 'OpMsgReply must have one document'
1017
1018 def update(self, *args, **kwargs):
1019 """Update the document. Same as ``dict().update()``.
1020
1021 >>> reply = OpMsgReply({'ismaster': True})
1022 >>> reply.update(maxWireVersion=3)
1023 >>> reply.doc['maxWireVersion']
1024 3
1025 >>> reply.update({'maxWriteBatchSize': 10, 'msg': 'isdbgrid'})
1026 """
1027 self.doc.update(*args, **kwargs)
1028
1029 def reply_bytes(self, request):
1030 """Take a `Request` and return an OP_MSG message as bytes."""
1031 flags = struct.pack("<I", self._flags)
1032 payload_type = struct.pack("<b", 0)
1033 payload_data = _bson.BSON.encode(self.doc)
1034 data = b''.join([flags, payload_type, payload_data])
1035
1036 reply_id = random.randint(0, 1000000)
1037 response_to = request.request_id
1038
1039 header = struct.pack(
1040 "<iiii", 16 + len(data), reply_id, response_to, OP_MSG)
1041 return header + data
8401042
8411043 def __repr__(self):
8421044 rep = '%s(%s' % (self.__class__.__name__, self)
843 if self._starting_from:
844 rep += ', starting_from=%d' % self._starting_from
845
8461045 if self._flags:
8471046 rep += ', flags=' + '|'.join(
848 name for name, value in REPLY_FLAGS.items()
1047 name for name, value in OP_MSG_FLAGS.items()
8491048 if self._flags & value)
8501049
8511050 return rep + ')'
10221221 self._autoresponders = []
10231222
10241223 if auto_ismaster is True:
1025 self.autoresponds(Command('ismaster'),
1224 self.autoresponds(CommandBase('ismaster'),
10261225 {'ismaster': True,
10271226 'minWireVersion': min_wire_version,
10281227 'maxWireVersion': max_wire_version})
10291228 elif auto_ismaster:
1030 self.autoresponds(Command('ismaster'), auto_ismaster)
1229 self.autoresponds(CommandBase('ismaster'), auto_ismaster)
10311230
10321231 @_synchronized
10331232 def run(self):
11081307 >>> future = go(client.db.command, 'foo')
11091308 >>> s.got('foo')
11101309 True
1111 >>> s.got(Command('foo', namespace='db'))
1310 >>> s.got(OpMsg('foo', namespace='db'))
11121311 True
1113 >>> s.got(Command('foo', key='value'))
1312 >>> s.got(OpMsg('foo', key='value'))
11141313 False
11151314 >>> s.ok()
11161315 >>> future() == {'ok': 1}
11741373
11751374 The remaining arguments are a :ref:`message spec <message spec>`:
11761375
1376 >>> # ok
11771377 >>> responder = s.autoresponds('bar', ok=0, errmsg='err')
11781378 >>> client.db.command('bar')
11791379 Traceback (most recent call last):
11801380 ...
11811381 OperationFailure: command SON([('bar', 1)]) on namespace db.$cmd failed: err
1182 >>> responder = s.autoresponds(Command('find', 'collection'),
1382 >>> responder = s.autoresponds(OpMsg('find', 'collection'),
11831383 ... {'cursor': {'id': 0, 'firstBatch': [{'_id': 1}, {'_id': 2}]}})
1384 >>> # ok
11841385 >>> list(client.db.collection.find()) == [{'_id': 1}, {'_id': 2}]
11851386 True
1186 >>> responder = s.autoresponds(Command('find', 'collection'),
1387 >>> responder = s.autoresponds(OpMsg('find', 'collection'),
11871388 ... {'cursor': {'id': 0, 'firstBatch': [{'a': 1}, {'a': 2}]}})
1389 >>> # bad
11881390 >>> list(client.db.collection.find()) == [{'a': 1}, {'a': 2}]
11891391 True
11901392
11961398 and replied to. Future matching requests skip the queue.
11971399
11981400 >>> future = go(client.db.command, 'baz')
1401 >>> # bad
11991402 >>> responder = s.autoresponds('baz', {'key': 'value'})
12001403 >>> future() == {'ok': 1, 'key': 'value'}
12011404 True
12351438 ... print('logging: %r' % request)
12361439 >>> responder = s.autoresponds(logger)
12371440 >>> client.db.command('baz') == {'ok': 1, 'a': 2}
1238 logging: Command({"baz": 1}, flags=SlaveOkay, namespace="db")
1441 logging: OpMsg({"baz": 1, "$db": "db", "$readPreference": {"mode": "primaryPreferred"}}, namespace="db")
12391442 True
12401443
12411444 The synonym `subscribe` better expresses your intent if your handler
14651668 raise socket.error('could not bind socket')
14661669
14671670
1468 OPCODES = {OP_QUERY: OpQuery,
1671 OPCODES = {OP_MSG: OpMsg,
1672 OP_QUERY: OpQuery,
14691673 OP_INSERT: OpInsert,
14701674 OP_UPDATE: OpUpdate,
14711675 OP_DELETE: OpDelete,
15101714
15111715
15121716 def make_docs(*args, **kwargs):
1513 """Make the documents for a `Request` or `OpReply`.
1717 """Make the documents for a `Request` or `Reply`.
15141718
15151719 Takes a variety of argument styles, returns a list of dicts.
15161720
15361740
15371741 if isinstance(args[0], (list, tuple)):
15381742 # Send a batch: OpReply([{'a': 1}, {'a': 2}]).
1539 if not all(isinstance(doc, (OpReply, collections.Mapping))
1743 if not all(isinstance(doc, (OpReply, Mapping))
15401744 for doc in args[0]):
15411745 raise_args_err('each doc must be a dict:')
15421746 if kwargs:
15601764 raise_args_err(err_msg, ValueError)
15611765
15621766 # Send a batch as varargs: OpReply({'a': 1}, {'a': 2}).
1563 if not all(isinstance(doc, (OpReply, collections.Mapping)) for doc in args):
1767 if not all(isinstance(doc, (OpReply, Mapping)) for doc in args):
15641768 raise_args_err('each doc must be a dict')
15651769
15661770 return args
16021806
16031807 def make_reply(*args, **kwargs):
16041808 # Error we might raise.
1605 if args and isinstance(args[0], OpReply):
1809 if args and isinstance(args[0], (OpReply, OpMsgReply)):
16061810 if args[1:] or kwargs:
16071811 raise_args_err("can't interpret args")
16081812 return args[0]
16091813
16101814 return OpReply(*args, **kwargs)
1815
1816
1817 def make_op_msg_reply(*args, **kwargs):
1818 # Error we might raise.
1819 if args and isinstance(args[0], (OpReply, OpMsgReply)):
1820 if args[1:] or kwargs:
1821 raise_args_err("can't interpret args")
1822 return args[0]
1823
1824 return OpMsgReply(*args, **kwargs)
16111825
16121826
16131827 def unprefixed(bson_str):
17271941 server.autoresponds('whatsmyuri', you='localhost:12345')
17281942 server.autoresponds({'getLog': 'startupWarnings'},
17291943 log=['hello from %s!' % name])
1730 server.autoresponds(Command('buildInfo'), version='MockupDB ' + __version__)
1731 server.autoresponds(Command('listCollections'))
1944 server.autoresponds(OpMsg('buildInfo'), version='MockupDB ' + __version__)
1945 server.autoresponds(OpMsg('listCollections'))
17321946 server.autoresponds('replSetGetStatus', ok=0)
17331947 return server
0 # Copyright 2009-2015 MongoDB, Inc.
0 # Copyright 2009-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
1212 # limitations under the License.
1313
1414 """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.
1564 """
1665
1766 import calendar
18 import collections
1967 import datetime
2068 import itertools
2169 import re
3078 JAVA_LEGACY, CSHARP_LEGACY,
3179 UUIDLegacy)
3280 from mockupdb._bson.code import Code
33 from mockupdb._bson.codec_options import CodecOptions, DEFAULT_CODEC_OPTIONS
81 from mockupdb._bson.codec_options import (
82 CodecOptions, DEFAULT_CODEC_OPTIONS, _raw_document_class)
3483 from mockupdb._bson.dbref import DBRef
84 from mockupdb._bson.decimal128 import Decimal128
3585 from mockupdb._bson.errors import (InvalidBSON,
3686 InvalidDocument,
3787 InvalidStringData)
3989 from mockupdb._bson.max_key import MaxKey
4090 from mockupdb._bson.min_key import MinKey
4191 from mockupdb._bson.objectid import ObjectId
42 from mockupdb._bson.py3compat import (b,
92 from mockupdb._bson.py3compat import (abc,
93 b,
4394 PY3,
4495 iteritems,
4596 text_type,
80131 BSONINT = b"\x10" # 32bit int
81132 BSONTIM = b"\x11" # Timestamp
82133 BSONLON = b"\x12" # 64bit int
134 BSONDEC = b"\x13" # Decimal128
83135 BSONMIN = b"\xFF" # Min key
84136 BSONMAX = b"\x7F" # Max key
85137
91143 _UNPACK_TIMESTAMP = struct.Struct("<II").unpack
92144
93145
94 def _get_int(data, position, dummy0, dummy1):
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):
95154 """Decode a BSON int32 to python int."""
96155 end = position + 4
97156 return _UNPACK_INT(data[position:end])[0], end
104163 opts.unicode_decode_error_handler, True)[0], end + 1
105164
106165
107 def _get_float(data, position, dummy0, dummy1):
166 def _get_float(data, position, dummy0, dummy1, dummy2):
108167 """Decode a BSON double to python float."""
109168 end = position + 8
110169 return _UNPACK_FLOAT(data[position:end])[0], end
111170
112171
113 def _get_string(data, position, obj_end, opts):
172 def _get_string(data, position, obj_end, opts, dummy):
114173 """Decode a BSON string to python unicode string."""
115174 length = _UNPACK_INT(data[position:position + 4])[0]
116175 position += 4
123182 opts.unicode_decode_error_handler, True)[0], end + 1
124183
125184
126 def _get_object(data, position, obj_end, opts):
185 def _get_object(data, position, obj_end, opts, dummy):
127186 """Decode a BSON subdocument to opts.document_class or bson.dbref.DBRef."""
128187 obj_size = _UNPACK_INT(data[position:position + 4])[0]
129188 end = position + obj_size - 1
131190 raise InvalidBSON("bad eoo")
132191 if end >= obj_end:
133192 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
134197 obj = _elements_to_dict(data, position + 4, end, opts)
135198
136199 position += obj_size
140203 return obj, position
141204
142205
143 def _get_array(data, position, obj_end, opts):
206 def _get_array(data, position, obj_end, opts, element_name):
144207 """Decode a BSON array to python list."""
145208 size = _UNPACK_INT(data[position:position + 4])[0]
146209 end = position + size - 1
147210 if data[end:end + 1] != b"\x00":
148211 raise InvalidBSON("bad eoo")
212
149213 position += 4
150214 end -= 1
151215 result = []
159223 element_type = data[position:position + 1]
160224 # Just skip the keys.
161225 position = index(b'\x00', position) + 1
162 value, position = getter[element_type](data, position, obj_end, opts)
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)
163231 append(value)
232
233 if position != end + 1:
234 raise InvalidBSON('bad array length')
164235 return result, position + 1
165236
166237
167 def _get_binary(data, position, dummy, opts):
238 def _get_binary(data, position, obj_end, opts, dummy1):
168239 """Decode a BSON binary to bson.binary.Binary or python UUID."""
169240 length, subtype = _UNPACK_LENGTH_SUBTYPE(data[position:position + 5])
170241 position += 5
175246 raise InvalidBSON("invalid binary (st 2) - lengths don't match!")
176247 length = length2
177248 end = position + length
178 if subtype in (3, 4):
249 if length < 0 or end > obj_end:
250 raise InvalidBSON('bad binary object length')
251 if subtype == 3:
179252 # Java Legacy
180253 uuid_representation = opts.uuid_representation
181254 if uuid_representation == JAVA_LEGACY:
188261 else:
189262 value = uuid.UUID(bytes=data[position:end])
190263 return value, end
264 if subtype == 4:
265 return uuid.UUID(bytes=data[position:end]), end
191266 # Python3 special case. Decode subtype 0 to 'bytes'.
192267 if PY3 and subtype == 0:
193268 value = data[position:end]
196271 return value, end
197272
198273
199 def _get_oid(data, position, dummy0, dummy1):
274 def _get_oid(data, position, dummy0, dummy1, dummy2):
200275 """Decode a BSON ObjectId to bson.objectid.ObjectId."""
201276 end = position + 12
202277 return ObjectId(data[position:end]), end
203278
204279
205 def _get_boolean(data, position, dummy0, dummy1):
280 def _get_boolean(data, position, dummy0, dummy1, dummy2):
206281 """Decode a BSON true/false to python True/False."""
207282 end = position + 1
208 return data[position:end] == b"\x01", end
209
210
211 def _get_date(data, position, dummy, opts):
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):
212292 """Decode a BSON datetime to python datetime.datetime."""
213293 end = position + 8
214294 millis = _UNPACK_LONG(data[position:end])[0]
215 diff = ((millis % 1000) + 1000) % 1000
216 seconds = (millis - diff) / 1000
217 micros = diff * 1000
218 if opts.tz_aware:
219 dt = EPOCH_AWARE + datetime.timedelta(
220 seconds=seconds, microseconds=micros)
221 if opts.tzinfo:
222 dt = dt.astimezone(opts.tzinfo)
223 else:
224 dt = EPOCH_NAIVE + datetime.timedelta(
225 seconds=seconds, microseconds=micros)
226 return dt, end
227
228
229 def _get_code(data, position, obj_end, opts):
295 return _millis_to_datetime(millis, opts), end
296
297
298 def _get_code(data, position, obj_end, opts, element_name):
230299 """Decode a BSON code to bson.code.Code."""
231 code, position = _get_string(data, position, obj_end, opts)
300 code, position = _get_string(data, position, obj_end, opts, element_name)
232301 return Code(code), position
233302
234303
235 def _get_code_w_scope(data, position, obj_end, opts):
304 def _get_code_w_scope(data, position, obj_end, opts, element_name):
236305 """Decode a BSON code_w_scope to bson.code.Code."""
237 code, position = _get_string(data, position + 4, obj_end, opts)
238 scope, position = _get_object(data, position, obj_end, opts)
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')
239312 return Code(code, scope), position
240313
241314
242 def _get_regex(data, position, dummy0, opts):
315 def _get_regex(data, position, dummy0, opts, dummy1):
243316 """Decode a BSON regex to bson.regex.Regex or a python pattern object."""
244317 pattern, position = _get_c_string(data, position, opts)
245318 bson_flags, position = _get_c_string(data, position, opts)
247320 return bson_re, position
248321
249322
250 def _get_ref(data, position, obj_end, opts):
323 def _get_ref(data, position, obj_end, opts, element_name):
251324 """Decode (deprecated) BSON DBPointer to bson.dbref.DBRef."""
252 collection, position = _get_string(data, position, obj_end, opts)
253 oid, position = _get_oid(data, position, obj_end, opts)
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)
254328 return DBRef(collection, oid), position
255329
256330
257 def _get_timestamp(data, position, dummy0, dummy1):
331 def _get_timestamp(data, position, dummy0, dummy1, dummy2):
258332 """Decode a BSON timestamp to bson.timestamp.Timestamp."""
259333 end = position + 8
260334 inc, timestamp = _UNPACK_TIMESTAMP(data[position:end])
261335 return Timestamp(timestamp, inc), end
262336
263337
264 def _get_int64(data, position, dummy0, dummy1):
338 def _get_int64(data, position, dummy0, dummy1, dummy2):
265339 """Decode a BSON int64 to bson.int64.Int64."""
266340 end = position + 8
267341 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
268348
269349
270350 # Each decoder function's signature is:
278358 BSONOBJ: _get_object,
279359 BSONARR: _get_array,
280360 BSONBIN: _get_binary,
281 BSONUND: lambda w, x, y, z: (None, x), # Deprecated undefined
361 BSONUND: lambda v, w, x, y, z: (None, w), # Deprecated undefined
282362 BSONOID: _get_oid,
283363 BSONBOO: _get_boolean,
284364 BSONDAT: _get_date,
285 BSONNUL: lambda w, x, y, z: (None, x),
365 BSONNUL: lambda v, w, x, y, z: (None, w),
286366 BSONRGX: _get_regex,
287367 BSONREF: _get_ref, # Deprecated DBPointer
288368 BSONCOD: _get_code,
291371 BSONINT: _get_int,
292372 BSONTIM: _get_timestamp,
293373 BSONLON: _get_int64,
294 BSONMIN: lambda w, x, y, z: (MinKey(), x),
295 BSONMAX: lambda w, x, y, z: (MaxKey(), x)}
374 BSONDEC: _get_decimal128,
375 BSONMIN: lambda v, w, x, y, z: (MinKey(), w),
376 BSONMAX: lambda v, w, x, y, z: (MaxKey(), w)}
296377
297378
298379 def _element_to_dict(data, position, obj_end, opts):
300381 element_type = data[position:position + 1]
301382 position += 1
302383 element_name, position = _get_c_string(data, position, opts)
303 value, position = _ELEMENT_GETTER[element_type](data,
304 position, obj_end, 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)
305390 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
306400
307401
308402 def _elements_to_dict(data, position, obj_end, opts):
309403 """Decode a BSON document."""
310404 result = opts.document_class()
311 end = obj_end - 1
312 while position < end:
313 (key, value, position) = _element_to_dict(data, position, obj_end, opts)
405 pos = position
406 for key, value, pos in _iterate_elements(data, position, obj_end, opts):
314407 result[key] = value
408 if pos != obj_end:
409 raise InvalidBSON('bad object or element length')
315410 return result
316411
317412
326421 if data[obj_size - 1:obj_size] != b"\x00":
327422 raise InvalidBSON("bad eoo")
328423 try:
424 if _raw_document_class(opts.document_class):
425 return opts.document_class(data, opts)
329426 return _elements_to_dict(data, 4, obj_size - 1, opts)
330427 except InvalidBSON:
331428 raise
428525
429526 def _encode_mapping(name, value, check_keys, opts):
430527 """Encode a mapping type."""
528 if _raw_document_class(value):
529 return b'\x03' + name + value.raw
431530 data = b"".join([_element_to_bson(key, val, check_keys, opts)
432531 for key, val in iteritems(value)])
433532 return b"\x03" + name + _PACK_INT(len(data) + 5) + data + b"\x00"
508607
509608 def _encode_datetime(name, value, dummy0, dummy1):
510609 """Encode datetime.datetime."""
511 if value.utcoffset() is not None:
512 value = value - value.utcoffset()
513 millis = int(calendar.timegm(value.timetuple()) * 1000 +
514 value.microsecond / 1000)
610 millis = _datetime_to_millis(value)
515611 return b"\x09" + name + _PACK_LONG(millis)
516612
517613
551647 """Encode bson.code.Code."""
552648 cstring = _make_c_string(value)
553649 cstrlen = len(cstring)
554 if not value.scope:
650 if value.scope is None:
555651 return b"\x0D" + name + _PACK_INT(cstrlen) + cstring
556652 scope = _dict_to_bson(value.scope, False, opts, False)
557653 full_length = _PACK_INT(8 + cstrlen + len(scope))
580676 return b"\x12" + name + _PACK_LONG(value)
581677 except struct.error:
582678 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
583684
584685
585686 def _encode_minkey(name, dummy0, dummy1, dummy2):
622723 SON: _encode_mapping,
623724 Timestamp: _encode_timestamp,
624725 UUIDLegacy: _encode_binary,
726 Decimal128: _encode_decimal128,
625727 # Special case. This will never be looked up directly.
626 collections.Mapping: _encode_mapping,
728 abc.Mapping: _encode_mapping,
627729 }
628730
629731
693795
694796 def _dict_to_bson(doc, check_keys, opts, top_level=True):
695797 """Encode a document to BSON."""
798 if _raw_document_class(doc):
799 return doc.raw
696800 try:
697801 elements = []
698802 if top_level and "_id" in doc:
711815 _dict_to_bson = _cbson._dict_to_bson
712816
713817
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
714842 _CODEC_OPTIONS_TYPE_ERROR = TypeError(
715843 "codec_options must be an instance of CodecOptions")
716844
750878 docs = []
751879 position = 0
752880 end = len(data) - 1
881 use_raw = _raw_document_class(codec_options.document_class)
753882 try:
754883 while position < end:
755884 obj_size = _UNPACK_INT(data[position:position + 4])[0]
758887 obj_end = position + obj_size - 1
759888 if data[obj_end:position + obj_size] != b"\x00":
760889 raise InvalidBSON("bad eoo")
761 docs.append(_elements_to_dict(data,
762 position + 4,
763 obj_end,
764 codec_options))
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))
765899 position += obj_size
766900 return docs
767901 except InvalidBSON:
0 # Copyright 2009-2015 MongoDB, Inc.
0 # Copyright 2009-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
7979 """The Java legacy UUID representation.
8080
8181 :class:`uuid.UUID` instances will automatically be encoded to
82 and decoded from mockupdb._bson binary, using the Java driver's legacy
83 byte order with binary subtype :data:`OLD_UUID_SUBTYPE`.
84
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.
8587 .. versionadded:: 2.3
8688 """
8789
8991 """The C#/.net legacy UUID representation.
9092
9193 :class:`uuid.UUID` instances will automatically be encoded to
92 and decoded from mockupdb._bson binary, using the C# driver's legacy
93 byte order and binary subtype :data:`OLD_UUID_SUBTYPE`.
94
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.
9599 .. versionadded:: 2.3
96100 """
97101
0 # Copyright 2009-2015 MongoDB, Inc.
0 # Copyright 2009-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
1313
1414 """Tools for representing JavaScript code in BSON.
1515 """
16 import collections
1716
18 from mockupdb._bson.py3compat import string_type
17 from mockupdb._bson.py3compat import abc, string_type, PY3, text_type
1918
2019
2120 class Code(str):
3130 the `scope` dictionary.
3231
3332 :Parameters:
34 - `code`: string containing JavaScript code to be evaluated
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`.
3536 - `scope` (optional): dictionary representing the scope in which
3637 `code` should be evaluated - a mapping from identifiers (as
37 strings) to values
38 strings) to values. Defaults to ``None``. This is applied after any
39 scope associated with a given `code` above.
3840 - `**kwargs` (optional): scope variables can also be passed as
39 keyword arguments
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
4046 """
4147
4248 _type_marker = 13
4652 raise TypeError("code must be an "
4753 "instance of %s" % (string_type.__name__))
4854
49 self = str.__new__(cls, code)
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)
5059
5160 try:
5261 self.__scope = code.scope
5362 except AttributeError:
54 self.__scope = {}
63 self.__scope = None
5564
5665 if scope is not None:
57 if not isinstance(scope, collections.Mapping):
66 if not isinstance(scope, abc.Mapping):
5867 raise TypeError("scope must be an instance of dict")
59 self.__scope.update(scope)
68 if self.__scope is not None:
69 self.__scope.update(scope)
70 else:
71 self.__scope = scope
6072
61 self.__scope.update(kwargs)
73 if kwargs:
74 if self.__scope is not None:
75 self.__scope.update(kwargs)
76 else:
77 self.__scope = kwargs
6278
6379 return self
6480
6581 @property
6682 def scope(self):
67 """Scope dictionary for this instance.
83 """Scope dictionary for this instance or ``None``.
6884 """
6985 return self.__scope
7086
0 # Copyright 2014-2015 MongoDB, Inc.
0 # Copyright 2014-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
1515
1616 import datetime
1717
18 from collections import MutableMapping, namedtuple
18 from collections import namedtuple
1919
20 from mockupdb._bson.py3compat import string_type
20 from mockupdb._bson.py3compat import abc, string_type
2121 from mockupdb._bson.binary import (ALL_UUID_REPRESENTATIONS,
22 PYTHON_LEGACY,
23 UUID_REPRESENTATION_NAMES)
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
2432
2533
2634 _options_base = namedtuple(
4351 and decoding instances of :class:`~uuid.UUID`. Defaults to
4452 :data:`~bson.binary.PYTHON_LEGACY`.
4553 - `unicode_decode_error_handler`: The error handler to use when decoding
46 an invalid BSON string. Valid options include 'strict', 'replace', and
47 'ignore'. Defaults to 'strict'.
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.
4859
4960 .. warning:: Care must be taken when changing
5061 `unicode_decode_error_handler` from its default value ('strict').
5162 The 'replace' and 'ignore' modes should not be used when documents
5263 retrieved from the server will be modified in the client application
5364 and stored back to the server.
54
55 - `tzinfo`: A :class:`~datetime.tzinfo` subclass that specifies the
56 timezone to/from which :class:`~datetime.datetime` objects should be
57 encoded/decoded.
58
5965 """
6066
6167 def __new__(cls, document_class=dict,
6268 tz_aware=False, uuid_representation=PYTHON_LEGACY,
6369 unicode_decode_error_handler="strict",
6470 tzinfo=None):
65 if not issubclass(document_class, MutableMapping):
66 raise TypeError("document_class must be dict, bson.son.SON, or "
67 "another subclass of collections.MutableMapping")
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")
6876 if not isinstance(tz_aware, bool):
6977 raise TypeError("tz_aware must be True or False")
7078 if uuid_representation not in ALL_UUID_REPRESENTATIONS:
8593 cls, (document_class, tz_aware, uuid_representation,
8694 unicode_decode_error_handler, tzinfo))
8795
88 def __repr__(self):
96 def _arguments_repr(self):
97 """Representation of the arguments used to create this object."""
8998 document_class_repr = (
9099 'dict' if self.document_class is dict
91100 else repr(self.document_class))
93102 uuid_rep_repr = UUID_REPRESENTATION_NAMES.get(self.uuid_representation,
94103 self.uuid_representation)
95104
96 return (
97 'CodecOptions(document_class=%s, tz_aware=%r, uuid_representation='
98 '%s, unicode_decode_error_handler=%r, tzinfo=%r)' %
99 (document_class_repr, self.tz_aware, uuid_rep_repr,
100 self.unicode_decode_error_handler,
101 self.tzinfo))
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))
102132
103133
104134 DEFAULT_CODEC_OPTIONS = CodecOptions()
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 # Copyright 2009-2015 MongoDB, Inc.
0 # Copyright 2009-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
0 # Copyright 2009-2015 MongoDB, Inc.
0 # Copyright 2009-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
1515
1616 This module provides two helper methods `dumps` and `loads` that wrap the
1717 native :mod:`json` methods and provide explicit BSON conversion to and from
18 json. This allows for specialized encoding and decoding of BSON documents
19 into `Mongo Extended JSON
20 <http://www.mongodb.org/display/DOCS/Mongo+Extended+JSON>`_'s *Strict*
21 mode. This lets you encode / decode BSON documents to JSON even when
22 they use special BSON types.
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)}]
2333
2434 Example usage (serialization):
2535
2939 >>> from mockupdb._bson.json_util import dumps
3040 >>> dumps([{'foo': [1, 2]},
3141 ... {'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'}},
3254 ... {'code': Code("function x() { return 1; }")},
33 ... {'bin': Binary("\x01\x02\x03\x04")}])
34 '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]'
35
36 Example usage (deserialization):
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`):
3760
3861 .. doctest::
3962
40 >>> from mockupdb._bson.json_util import loads
41 >>> loads('[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": "00", "$binary": "AQIDBA=="}}]')
42 [{u'foo': [1, 2]}, {u'bar': {u'hello': u'world'}}, {u'code': Code('function x() { return 1; }', {})}, {u'bin': Binary('...', 0)}]
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"}}}]'
4371
4472 Alternatively, you can manually pass the `default` to :func:`json.dumps`.
4573 It won't handle :class:`~bson.binary.Binary` and :class:`~bson.code.Code`
4674 instances (as they are extended strings you can't provide custom defaults),
4775 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`.
4886
4987 .. versionchanged:: 2.8
5088 The output format for :class:`~bson.timestamp.Timestamp` has changed from
67105 """
68106
69107 import base64
70 import calendar
71 import collections
72108 import datetime
73 import json
109 import math
74110 import re
111 import sys
75112 import uuid
76113
77 from mockupdb._bson import EPOCH_AWARE, RE_TYPE, SON
78 from mockupdb._bson.binary import Binary
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)
79132 from mockupdb._bson.code import Code
133 from mockupdb._bson.codec_options import CodecOptions
80134 from mockupdb._bson.dbref import DBRef
135 from mockupdb._bson.decimal128 import Decimal128
81136 from mockupdb._bson.int64 import Int64
82137 from mockupdb._bson.max_key import MaxKey
83138 from mockupdb._bson.min_key import MinKey
84139 from mockupdb._bson.objectid import ObjectId
140 from mockupdb._bson.py3compat import (PY3, iteritems, integer_types,
141 string_type,
142 text_type)
85143 from mockupdb._bson.regex import Regex
86144 from mockupdb._bson.timestamp import Timestamp
87145 from mockupdb._bson.tz_util import utc
88146
89 from mockupdb._bson.py3compat import PY3, iteritems, string_type, text_type
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
90153
91154
92155 _RE_OPT_TABLE = {
98161 "x": re.X,
99162 }
100163
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
101390
102391 def dumps(obj, *args, **kwargs):
103 """Helper function that wraps :class:`json.dumps`.
392 """Helper function that wraps :func:`json.dumps`.
104393
105394 Recursive function that handles all BSON types including
106395 :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`.
107404
108405 .. versionchanged:: 2.7
109406 Preserves order when rendering SON, Timestamp, Code, Binary, and DBRef
110407 instances.
111408 """
112 return json.dumps(_json_convert(obj), *args, **kwargs)
409 json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS)
410 return json.dumps(_json_convert(obj, json_options), *args, **kwargs)
113411
114412
115413 def loads(s, *args, **kwargs):
116 """Helper function that wraps :class:`json.loads`.
414 """Helper function that wraps :func:`json.loads`.
117415
118416 Automatically passes the object_hook for BSON type conversion.
119 """
120 kwargs['object_hook'] = lambda dct: object_hook(dct)
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)
121440 return json.loads(s, *args, **kwargs)
122441
123442
124 def _json_convert(obj):
443 def _json_convert(obj, json_options=DEFAULT_JSON_OPTIONS):
125444 """Recursive helper method that converts BSON types so they can be
126445 converted into json.
127446 """
128447 if hasattr(obj, 'iteritems') or hasattr(obj, 'items'): # PY3 support
129 return SON(((k, _json_convert(v)) for k, v in iteritems(obj)))
448 return SON(((k, _json_convert(v, json_options))
449 for k, v in iteritems(obj)))
130450 elif hasattr(obj, '__iter__') and not isinstance(obj, (text_type, bytes)):
131 return list((_json_convert(v) for v in obj))
451 return list((_json_convert(v, json_options) for v in obj))
132452 try:
133 return default(obj)
453 return default(obj, json_options)
134454 except TypeError:
135455 return obj
136456
137457
138 def object_hook(dct):
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):
139463 if "$oid" in dct:
140 return ObjectId(str(dct["$oid"]))
464 return _parse_canonical_oid(dct)
141465 if "$ref" in dct:
142 return DBRef(dct["$ref"], dct["$id"], dct.get("$db", None))
466 return _parse_canonical_dbref(dct)
143467 if "$date" in dct:
144 dtm = dct["$date"]
145 # mongoexport 2.6 and newer
146 if isinstance(dtm, string_type):
147 aware = datetime.datetime.strptime(
148 dtm[:23], "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=utc)
149 offset = dtm[23:]
150 if not offset or offset == 'Z':
151 # UTC
152 return aware
153 else:
154 if len(offset) == 5:
155 # Offset from mongoexport is in format (+|-)HHMM
156 secs = (int(offset[1:3]) * 3600 + int(offset[3:]) * 60)
157 elif ':' in offset and len(offset) == 6:
158 # RFC-3339 format (+|-)HH:MM
159 hours, minutes = offset[1:].split(':')
160 secs = (int(hours) * 3600 + int(minutes) * 60)
161 else:
162 # Not RFC-3339 compliant or mongoexport output.
163 raise ValueError("invalid format for offset")
164 if offset[0] == "-":
165 secs *= -1
166 return aware - datetime.timedelta(seconds=secs)
167 # mongoexport 2.6 and newer, time before the epoch (SERVER-15275)
168 elif isinstance(dtm, collections.Mapping):
169 secs = float(dtm["$numberLong"]) / 1000.0
170 # mongoexport before 2.6
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)
171478 else:
172 secs = float(dtm) / 1000.0
173 return EPOCH_AWARE + datetime.timedelta(seconds=secs)
174 if "$regex" in dct:
175 flags = 0
176 # PyMongo always adds $options but some other tools may not.
177 for opt in dct.get("$options", ""):
178 flags |= _RE_OPT_TABLE.get(opt, 0)
179 return Regex(dct["$regex"], flags)
180 if "$minKey" in dct:
181 return MinKey()
182 if "$maxKey" in dct:
183 return MaxKey()
184 if "$binary" in dct:
185 if isinstance(dct["$type"], int):
186 dct["$type"] = "%02x" % dct["$type"]
187 subtype = int(dct["$type"], 16)
188 if subtype >= 0xffffff80: # Handle mongoexport values
189 subtype = int(dct["$type"][6:], 16)
190 return Binary(base64.b64decode(dct["$binary"].encode()), subtype)
479 return _parse_canonical_binary(dct, json_options)
191480 if "$code" in dct:
192 return Code(dct["$code"], dct.get("$scope"))
481 return _parse_canonical_code(dct)
193482 if "$uuid" in dct:
194 return uuid.UUID(dct["$uuid"])
483 return _parse_legacy_uuid(dct)
195484 if "$undefined" in dct:
196485 return None
197486 if "$numberLong" in dct:
198 return Int64(dct["$numberLong"])
487 return _parse_canonical_int64(dct)
199488 if "$timestamp" in dct:
200489 tsp = dct["$timestamp"]
201490 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)
202503 return dct
203504
204505
205 def default(obj):
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):
206759 # We preserve key order when rendering SON, DBRef, etc. as JSON by
207760 # returning a SON for those types instead of a dict.
208761 if isinstance(obj, ObjectId):
209762 return {"$oid": str(obj)}
210763 if isinstance(obj, DBRef):
211 return _json_convert(obj.as_doc())
764 return _json_convert(obj.as_doc(), json_options=json_options)
212765 if isinstance(obj, datetime.datetime):
213 # TODO share this code w/ bson.py?
214 if obj.utcoffset() is not None:
215 obj = obj - obj.utcoffset()
216 millis = int(calendar.timegm(obj.timetuple()) * 1000 +
217 obj.microsecond / 1000)
218 return {"$date": millis}
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)}
219788 if isinstance(obj, (RE_TYPE, Regex)):
220789 flags = ""
221790 if obj.flags & re.IGNORECASE:
234803 pattern = obj.pattern
235804 else:
236805 pattern = obj.pattern.decode('utf-8')
237 return SON([("$regex", pattern), ("$options", flags)])
806 if json_options.json_mode == JSONMode.LEGACY:
807 return SON([("$regex", pattern), ("$options", flags)])
808 return {'$regularExpression': SON([("pattern", pattern),
809 ("options", flags)])}
238810 if isinstance(obj, MinKey):
239811 return {"$minKey": 1}
240812 if isinstance(obj, MaxKey):
242814 if isinstance(obj, Timestamp):
243815 return {"$timestamp": SON([("t", obj.time), ("i", obj.inc)])}
244816 if isinstance(obj, Code):
245 return SON([('$code', str(obj)), ('$scope', obj.scope)])
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))])
246822 if isinstance(obj, Binary):
247 return SON([
248 ('$binary', base64.b64encode(obj).decode()),
249 ('$type', "%02x" % obj.subtype)])
823 return _encode_binary(obj, obj.subtype, json_options)
250824 if PY3 and isinstance(obj, bytes):
251 return SON([
252 ('$binary', base64.b64encode(obj).decode()),
253 ('$type', "00")])
825 return _encode_binary(obj, 0, json_options)
254826 if isinstance(obj, uuid.UUID):
255 return {"$uuid": obj.hex}
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))}
256859 raise TypeError("%r is not JSON serializable" % obj)
0 # Copyright 2010-2015 MongoDB, Inc.
0 # Copyright 2010-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
3535
3636 def __le__(self, other):
3737 return isinstance(other, MaxKey)
38
38
3939 def __lt__(self, dummy):
4040 return False
4141
4242 def __ge__(self, dummy):
4343 return True
44
44
4545 def __gt__(self, other):
4646 return not isinstance(other, MaxKey)
4747
0 # Copyright 2010-2015 MongoDB, Inc.
0 # Copyright 2010-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
3232
3333 def __ne__(self, other):
3434 return not self == other
35
35
3636 def __le__(self, dummy):
3737 return True
38
38
3939 def __lt__(self, other):
4040 return not isinstance(other, MinKey)
4141
4242 def __ge__(self, other):
4343 return isinstance(other, MinKey)
44
44
4545 def __gt__(self, dummy):
4646 return False
4747
234234 def __setstate__(self, value):
235235 """explicit state set from pickling
236236 """
237 # Provide backwards compatibility with OIDs
237 # Provide backwards compatability with OIDs
238238 # pickled with pymongo-1.9 or older.
239239 if isinstance(value, dict):
240240 oid = value["_ObjectId__id"]
0 # Copyright 2009-2015 MongoDB, Inc.
0 # Copyright 2009-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License"); you
33 # may not use this file except in compliance with the License. You
2121 import codecs
2222 import _thread as thread
2323 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
2431 MAXSIZE = sys.maxsize
2532
2633 imap = map
3239 # the b prefix (e.g. b'foo').
3340 # See http://python3porting.com/problems.html#nicer-solutions
3441 return codecs.latin_1_encode(s)[0]
35
36 def u(s):
37 # PY3 strings may already be treated as unicode literals
38 return s
3942
4043 def bytes_from_hex(h):
4144 return bytes.fromhex(h)
5659 string_type = str
5760 integer_types = int
5861 else:
62 import collections as abc
5963 import thread
6064
6165 from itertools import imap
6973 def b(s):
7074 # See comments above. In python 2.x b('foo') is just 'foo'.
7175 return s
72
73 def u(s):
74 """Replacement for unicode literal prefix."""
75 return unicode(s.replace('\\', '\\\\'), 'unicode_escape')
7676
7777 def bytes_from_hex(h):
7878 return h.decode('hex')
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 # Copyright 2013-2015 MongoDB, Inc.
0 # Copyright 2013-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
9999
100100 def __eq__(self, other):
101101 if isinstance(other, Regex):
102 return self.pattern == self.pattern and self.flags == other.flags
102 return self.pattern == other.pattern and self.flags == other.flags
103103 else:
104104 return NotImplemented
105105
0 # Copyright 2009-2015 MongoDB, Inc.
0 # Copyright 2009-present MongoDB, Inc.
11 #
22 # Licensed under the Apache License, Version 2.0 (the "License");
33 # you may not use this file except in compliance with the License.
1717 of keys is important. A SON object can be used just like a normal Python
1818 dictionary."""
1919
20 import collections
2120 import copy
2221 import re
2322
24 from mockupdb._bson.py3compat import iteritems
23 from mockupdb._bson.py3compat import abc, iteritems
2524
2625
2726 # This sort of sucks, but seems to be as good as it gets...
3332 """SON data.
3433
3534 A subclass of dict that maintains ordering of keys and provides a
36 few extra niceties for dealing with SON. SON objects can be
37 converted to and from mockupdb._bson.
38
39 The mapping from Python types to BSON types is as follows:
40
41 ======================================= ============= ===================
42 Python Type BSON Type Supported Direction
43 ======================================= ============= ===================
44 None null both
45 bool boolean both
46 int [#int]_ int32 / int64 py -> bson
47 long int64 py -> bson
48 `bson.int64.Int64` int64 both
49 float number (real) both
50 string string py -> bson
51 unicode string both
52 list array both
53 dict / `SON` object both
54 datetime.datetime [#dt]_ [#dt2]_ date both
55 `bson.regex.Regex` regex both
56 compiled re [#re]_ regex py -> bson
57 `bson.binary.Binary` binary both
58 `bson.objectid.ObjectId` oid both
59 `bson.dbref.DBRef` dbref both
60 None undefined bson -> py
61 unicode code bson -> py
62 `bson.code.Code` code py -> bson
63 unicode symbol bson -> py
64 bytes (Python 3) [#bytes]_ binary both
65 ======================================= ============= ===================
66
67 Note that to save binary data it must be wrapped as an instance of
68 `bson.binary.Binary`. Otherwise it will be saved as a BSON string
69 and retrieved as unicode.
70
71 .. [#int] A Python int will be saved as a BSON int32 or BSON int64 depending
72 on its size. A BSON int32 will always decode to a Python int. A BSON
73 int64 will always decode to a :class:`~bson.int64.Int64`.
74 .. [#dt] datetime.datetime instances will be rounded to the nearest
75 millisecond when saved
76 .. [#dt2] all datetime.datetime instances are treated as *naive*. clients
77 should always use UTC.
78 .. [#re] :class:`~bson.regex.Regex` instances and regular expression
79 objects from ``re.compile()`` are both saved as BSON regular expressions.
80 BSON regular expressions are decoded as :class:`~bson.regex.Regex`
81 instances.
82 .. [#bytes] The bytes type from Python 3.x is encoded as BSON binary with
83 subtype 0. In Python 3.x it will be decoded back to bytes. In Python 2.x
84 it will be decoded to an instance of :class:`~bson.binary.Binary` with
85 subtype 0.
35 few extra niceties for dealing with SON. SON provides an API
36 similar to collections.OrderedDict from Python 2.7+.
8637 """
8738
8839 def __init__(self, data=None, **kwargs):
226177 def transform_value(value):
227178 if isinstance(value, list):
228179 return [transform_value(v) for v in value]
229 elif isinstance(value, collections.Mapping):
180 elif isinstance(value, abc.Mapping):
230181 return dict([
231182 (k, transform_value(v))
232183 for k, v in iteritems(value)])
00 Metadata-Version: 1.1
11 Name: mockupdb
2 Version: 1.3.0
2 Version: 1.4.1
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.4.1 (2018-06-30)
25 ------------------
26
27 Fix an inadvertent dependency on PyMongo, which broke the docs build.
28
29 1.4.0 (2018-06-29)
30 ------------------
31
32 Support, and expect, OP_MSG requests from clients. Thanks to Shane Harvey for
33 the contribution.
34
35 Update vendored bson library from PyMongo. Support the Decimal128 BSON type. Fix
36 Matcher so it equates BSON objects from PyMongo like ``ObjectId(...)`` with
37 equivalent objects created from MockupDB's vendored bson library.
2338
2439 1.3.0 (2018-02-19)
2540 ------------------
2828 mockupdb/_bson/code.py
2929 mockupdb/_bson/codec_options.py
3030 mockupdb/_bson/dbref.py
31 mockupdb/_bson/decimal128.py
3132 mockupdb/_bson/errors.py
3233 mockupdb/_bson/int64.py
3334 mockupdb/_bson/json_util.py
3536 mockupdb/_bson/min_key.py
3637 mockupdb/_bson/objectid.py
3738 mockupdb/_bson/py3compat.py
39 mockupdb/_bson/raw_bson.py
3840 mockupdb/_bson/regex.py
3941 mockupdb/_bson/son.py
4042 mockupdb/_bson/timestamp.py
2323
2424 setup(
2525 name='mockupdb',
26 version='1.3.0',
26 version='1.4.1',
2727 description="MongoDB Wire Protocol server library",
2828 long_description=readme + '\n\n' + changelog,
2929 author="A. Jesse Jiryu Davis",
33 """Test MockupDB."""
44
55 import contextlib
6 import os
76 import ssl
87 import sys
98
1817 from Queue import Queue
1918
2019 # Tests depend on PyMongo's BSON implementation, but MockupDB itself does not.
21 from bson import SON
20 from bson import (Binary, Code, DBRef, Decimal128, MaxKey, MinKey, ObjectId,
21 Regex, SON, Timestamp)
2222 from bson.codec_options import CodecOptions
2323 from pymongo import MongoClient, message, WriteConcern
2424
25 from mockupdb import (go, going,
26 Command, Matcher, MockupDB, Request,
27 OpDelete, OpInsert, OpQuery, OpUpdate,
28 DELETE_FLAGS, INSERT_FLAGS, UPDATE_FLAGS, QUERY_FLAGS)
29
25 from mockupdb import (_bson as mockup_bson, go, going,
26 Command, CommandBase, Matcher, MockupDB, Request,
27 OpInsert, OpMsg, OpQuery, QUERY_FLAGS)
3028 from tests import unittest # unittest2 on Python 2.6.
3129
3230
135133 request.assert_matches(Command('foo'))
136134
137135
138 class TestLegacyWrites(unittest.TestCase):
136 class TestUnacknowledgedWrites(unittest.TestCase):
139137 def setUp(self):
140138 self.server = MockupDB(auto_ismaster=True)
141139 self.server.run()
146144
147145 def test_insert_one(self):
148146 with going(self.collection.insert_one, {'_id': 1}):
149 self.server.receives(OpInsert({'_id': 1}, flags=0))
147 # The moreToCome flag = 2.
148 self.server.receives(
149 OpMsg('insert', 'collection', writeConcern={'w': 0}, flags=2))
150150
151151 def test_insert_many(self):
152152 collection = self.collection.with_options(
153153 write_concern=WriteConcern(0))
154154
155 flags = INSERT_FLAGS['ContinueOnError']
156155 docs = [{'_id': 1}, {'_id': 2}]
157156 with going(collection.insert_many, docs, ordered=False):
158 self.server.receives(OpInsert(docs, flags=flags))
157 self.server.receives(OpMsg(SON([
158 ('insert', 'collection'),
159 ('ordered', False),
160 ('writeConcern', {'w': 0})]), flags=2))
159161
160162 def test_replace_one(self):
161163 with going(self.collection.replace_one, {}, {}):
162 self.server.receives(OpUpdate({}, {}, flags=0))
164 self.server.receives(OpMsg(SON([
165 ('update', 'collection'),
166 ('writeConcern', {'w': 0})
167 ]), flags=2))
163168
164169 def test_update_many(self):
165 flags = UPDATE_FLAGS['MultiUpdate']
166170 with going(self.collection.update_many, {}, {'$unset': 'a'}):
167 update = self.server.receives(OpUpdate({}, {}, flags=flags))
168 self.assertEqual(2, update.flags)
171 self.server.receives(OpMsg(SON([
172 ('update', 'collection'),
173 ('ordered', True),
174 ('writeConcern', {'w': 0})
175 ]), flags=2))
169176
170177 def test_delete_one(self):
171 flags = DELETE_FLAGS['SingleRemove']
172178 with going(self.collection.delete_one, {}):
173 delete = self.server.receives(OpDelete({}, flags=flags))
174 self.assertEqual(1, delete.flags)
179 self.server.receives(OpMsg(SON([
180 ('delete', 'collection'),
181 ('writeConcern', {'w': 0})
182 ]), flags=2))
175183
176184 def test_delete_many(self):
177185 with going(self.collection.delete_many, {}):
178 delete = self.server.receives(OpDelete({}, flags=0))
179 self.assertEqual(0, delete.flags)
186 self.server.receives(OpMsg(SON([
187 ('delete', 'collection'),
188 ('writeConcern', {'w': 0})]), flags=2))
180189
181190
182191 class TestMatcher(unittest.TestCase):
195204 self.assertFalse(
196205 Matcher(Command('a', b=1)).matches(Command('a', b=2)))
197206
207 def test_bson_classes(self):
208 _id = '5a918f9fa08bff9c7688d3e1'
209
210 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)),
222 ]:
223 # Basic case.
224 self.assertTrue(
225 Matcher(Command(y=b)).matches(Command(y=b)),
226 "MockupDB %r doesn't equal itself" % (b,))
227
228 # First Command argument is special, try comparing the second also.
229 self.assertTrue(
230 Matcher(Command('x', y=b)).matches(Command('x', y=b)),
231 "MockupDB %r doesn't equal itself" % (b,))
232
233 # In practice, users pass PyMongo classes in message specs.
234 self.assertTrue(
235 Matcher(Command(y=b)).matches(Command(y=a)),
236 "PyMongo %r != MockupDB %r" % (a, b))
237
238 self.assertTrue(
239 Matcher(Command('x', y=b)).matches(Command('x', y=a)),
240 "PyMongo %r != MockupDB %r" % (a, b))
241
198242
199243 class TestAutoresponds(unittest.TestCase):
200244 def test_auto_dequeue(self):
208252 def test_autoresponds_case_insensitive(self):
209253 server = MockupDB(auto_ismaster=True)
210254 # Little M. Note this is only case-insensitive because it's a Command.
211 server.autoresponds(Command('fooBar'), foo='bar')
255 server.autoresponds(CommandBase('fooBar'), foo='bar')
212256 server.run()
213257 response = MongoClient(server.uri).admin.command('Foobar')
214258 self.assertEqual('bar', response['foo'])