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