record new upstream branch created by importing python-mockupdb_1.4.1.orig.tar.gz and merge it
Ondřej Nový
5 years ago
1 | 1 | |
2 | 2 | Changelog |
3 | 3 | ========= |
4 | ||
5 | 1.4.1 (2018-06-30) | |
6 | ------------------ | |
7 | ||
8 | Fix an inadvertent dependency on PyMongo, which broke the docs build. | |
9 | ||
10 | 1.4.0 (2018-06-29) | |
11 | ------------------ | |
12 | ||
13 | Support, and expect, OP_MSG requests from clients. Thanks to Shane Harvey for | |
14 | the contribution. | |
15 | ||
16 | Update vendored bson library from PyMongo. Support the Decimal128 BSON type. Fix | |
17 | Matcher so it equates BSON objects from PyMongo like ``ObjectId(...)`` with | |
18 | equivalent objects created from MockupDB's vendored bson library. | |
4 | 19 | |
5 | 20 | 1.3.0 (2018-02-19) |
6 | 21 | ------------------ |
0 | 0 | Metadata-Version: 1.1 |
1 | 1 | Name: mockupdb |
2 | Version: 1.3.0 | |
2 | Version: 1.4.1 | |
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.4.1 (2018-06-30) | |
25 | ------------------ | |
26 | ||
27 | Fix an inadvertent dependency on PyMongo, which broke the docs build. | |
28 | ||
29 | 1.4.0 (2018-06-29) | |
30 | ------------------ | |
31 | ||
32 | Support, and expect, OP_MSG requests from clients. Thanks to Shane Harvey for | |
33 | the contribution. | |
34 | ||
35 | Update vendored bson library from PyMongo. Support the Decimal128 BSON type. Fix | |
36 | Matcher so it equates BSON objects from PyMongo like ``ObjectId(...)`` with | |
37 | equivalent objects created from MockupDB's vendored bson library. | |
23 | 38 | |
24 | 39 | 1.3.0 (2018-02-19) |
25 | 40 | ------------------ |
0 | 0 | # see git-dpm(1) from git-dpm package |
1 | e7cbf57d95e3b940244cb64d1cab877837774749 | |
2 | e7cbf57d95e3b940244cb64d1cab877837774749 | |
3 | e7cbf57d95e3b940244cb64d1cab877837774749 | |
4 | e7cbf57d95e3b940244cb64d1cab877837774749 | |
5 | python-mockupdb_1.3.0.orig.tar.gz | |
6 | 77e66429104bcac82e6b83f5d53736315c2c398e | |
7 | 61001 | |
1 | 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc | |
2 | 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc | |
3 | 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc | |
4 | 9f5015bc49c1cf9e8980473c4596e6f7dbe82bdc | |
5 | python-mockupdb_1.4.1.orig.tar.gz | |
6 | cca1a4cfe81f6cd78aebcf89c2c0b4b7566fba43 | |
7 | 75003 | |
8 | 8 | debianTag="debian/%e%v" |
9 | 9 | patchedTag="patched/%e%v" |
10 | 10 | upstreamTag="upstream/%e%u" |
48 | 48 | (Notice that `~Request.replies` returns True. This makes more advanced uses of |
49 | 49 | `~MockupDB.autoresponds` easier, see the reference document.) |
50 | 50 | |
51 | Reply To Legacy Writes | |
52 | ---------------------- | |
53 | ||
54 | Send an unacknowledged OP_INSERT: | |
55 | ||
56 | >>> from pymongo.write_concern import WriteConcern | |
57 | >>> w0 = WriteConcern(w=0) | |
58 | >>> collection = client.db.coll.with_options(write_concern=w0) | |
59 | >>> collection.insert_one({'_id': 1}) # doctest: +ELLIPSIS | |
60 | <pymongo.results.InsertOneResult object at ...> | |
61 | >>> server.receives() | |
62 | OpInsert({"_id": 1}, namespace="db.coll") | |
63 | 51 | |
64 | 52 | Reply To Write Commands |
65 | 53 | ----------------------- |
66 | 54 | |
67 | 55 | If PyMongo sends an unacknowledged OP_INSERT it does not block |
68 | waiting for you to call `~Request.replies`. However, for acknowledge operations | |
56 | waiting for you to call `~Request.replies`. However, for acknowledged operations | |
69 | 57 | it does block. Use `~test.utils.go` to defer PyMongo to a background thread so |
70 | 58 | you can respond from the main thread: |
71 | 59 | |
81 | 69 | |
82 | 70 | >>> cmd = server.receives() |
83 | 71 | >>> cmd |
84 | Command({"insert": "coll", "ordered": true, "documents": [{"_id": 1}]}, namespace="db") | |
72 | OpMsg({"insert": "coll", "ordered": true, "$db": "db", "$readPreference": {"mode": "primary"}, "documents": [{"_id": 1}]}, namespace="db") | |
85 | 73 | |
86 | 74 | (Note how MockupDB renders requests and replies as JSON, not Python. |
87 | 75 | The chief differences are that "true" and "false" are lower-case, and the order |
157 | 145 | ... try: |
158 | 146 | ... while server.running: |
159 | 147 | ... # Match queries most restrictive first. |
160 | ... if server.got(Command('find', 'coll', filter={'a': {'$gt': 1}})): | |
148 | ... if server.got(OpMsg('find', 'coll', filter={'a': {'$gt': 1}})): | |
161 | 149 | ... server.reply(cursor={'id': 0, 'firstBatch':[{'a': 2}]}) |
162 | 150 | ... elif server.got('break'): |
163 | 151 | ... server.ok() |
164 | 152 | ... break |
165 | ... elif server.got(Command('find', 'coll')): | |
153 | ... elif server.got(OpMsg('find', 'coll')): | |
166 | 154 | ... server.reply( |
167 | 155 | ... cursor={'id': 0, 'firstBatch':[{'a': 1}, {'a': 2}]}) |
168 | 156 | ... else: |
246 | 234 | >>> client = MongoClient(server.uri) |
247 | 235 | >>> future = go(client.db.collection.insert, {'_id': 1}) |
248 | 236 | >>> # Assert the command name is "insert" and its parameter is "collection". |
249 | >>> request = server.receives(Command('insert', 'collection')) | |
237 | >>> request = server.receives(OpMsg('insert', 'collection')) | |
250 | 238 | >>> request.ok() |
251 | 239 | True |
252 | 240 | >>> assert future() |
253 | 241 | |
254 | 242 | If the request did not match, MockupDB would raise an `AssertionError`. |
255 | 243 | |
256 | The arguments to `Command` above are an example of a message spec. The | |
244 | The arguments to `OpMsg` above are an example of a message spec. The | |
257 | 245 | pattern-matching rules are implemented in `Matcher`. |
258 | 246 | Here are |
259 | 247 | some more examples. |
320 | 308 | >>> m.matches(OpQuery, {'_id': 1}) |
321 | 309 | True |
322 | 310 | |
323 | Commands are queries on some database's "database.$cmd" namespace. | |
324 | They are specially prohibited from matching regular queries: | |
325 | ||
326 | >>> Matcher(OpQuery).matches(Command) | |
327 | False | |
328 | >>> Matcher(Command).matches(Command) | |
329 | True | |
330 | >>> Matcher(OpQuery).matches(OpQuery) | |
331 | True | |
332 | >>> Matcher(Command).matches(OpQuery) | |
333 | False | |
334 | ||
311 | Commands in MongoDB 3.6 and later use the OP_MSG wire protocol message. | |
335 | 312 | The command name is matched case-insensitively: |
336 | 313 | |
337 | >>> Matcher(Command('ismaster')).matches(Command('IsMaster')) | |
314 | >>> Matcher(OpMsg('ismaster')).matches(OpMsg('IsMaster')) | |
338 | 315 | True |
339 | 316 | |
340 | 317 | You can match properties specific to certain opcodes: |
404 | 381 | document and assumes the value is 1: |
405 | 382 | |
406 | 383 | >>> import mockupdb |
407 | >>> mockupdb.make_reply() | |
408 | OpReply() | |
409 | >>> mockupdb.make_reply(0) | |
410 | OpReply({"ok": 0}) | |
411 | >>> mockupdb.make_reply("foo") | |
412 | OpReply({"foo": 1}) | |
384 | >>> mockupdb.make_op_msg_reply() | |
385 | OpMsgReply() | |
386 | >>> mockupdb.make_op_msg_reply(0) | |
387 | OpMsgReply({"ok": 0}) | |
388 | >>> mockupdb.make_op_msg_reply("foo") | |
389 | OpMsgReply({"foo": 1}) | |
413 | 390 | |
414 | 391 | You can pass a dict or OrderedDict of fields instead of using keyword arguments. |
415 | 392 | This is best for fieldnames that are not valid Python identifiers: |
416 | 393 | |
417 | >>> mockupdb.make_reply(OrderedDict([('ok', 0), ('$err', 'bad')])) | |
418 | OpReply({"ok": 0, "$err": "bad"}) | |
394 | >>> mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')])) | |
395 | OpMsgReply({"ok": 0, "$err": "bad"}) | |
419 | 396 | |
420 | 397 | You can customize the OP_REPLY header flags with the "flags" keyword argument: |
421 | 398 | |
422 | >>> r = mockupdb.make_reply(OrderedDict([('ok', 0), ('$err', 'bad')]), | |
423 | ... flags=REPLY_FLAGS['QueryFailure']) | |
399 | >>> r = mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')]), | |
400 | ... flags=OP_MSG_FLAGS['checksumPresent']) | |
424 | 401 | >>> repr(r) |
425 | 'OpReply({"ok": 0, "$err": "bad"}, flags=QueryFailure)' | |
426 | ||
427 | The above logic, which simulates a query error in MongoDB before 3.2, is | |
428 | provided conveniently in `~Request.fail()`. This protocol is obsolete in MongoDB | |
429 | 3.2+, which uses commands for all operations. | |
430 | ||
431 | Although these examples call `make_reply` explicitly, this is only to | |
402 | 'OpMsgReply({"ok": 0, "$err": "bad"}, flags=checksumPresent)' | |
403 | ||
404 | Although these examples call `make_op_msg_reply` explicitly, this is only to | |
432 | 405 | illustrate how replies are specified. Your code will pass these arguments to a |
433 | 406 | `Request` method like `~Request.replies`. |
434 | 407 | |
454 | 427 | |
455 | 428 | >>> cursor = collection.find().batch_size(1) |
456 | 429 | >>> future = go(next, cursor) |
457 | >>> server.receives(Command('find', 'coll')).fail() | |
430 | >>> server.receives(OpMsg('find', 'coll')).command_err() | |
458 | 431 | True |
459 | 432 | >>> future() |
460 | 433 | Traceback (most recent call last): |
461 | 434 | ... |
462 | OperationFailure: database error: MockupDB query failure | |
435 | OperationFailure: database error: MockupDB command failure | |
463 | 436 | |
464 | 437 | You can simulate normal querying, too: |
465 | 438 | |
466 | 439 | >>> cursor = collection.find().batch_size(2) |
467 | 440 | >>> future = go(list, cursor) |
468 | 441 | >>> documents = [{'_id': 1}, {'x': 2}, {'foo': 'bar'}, {'beauty': True}] |
469 | >>> request = server.receives(Command('find', 'coll')) | |
442 | >>> request = server.receives(OpMsg('find', 'coll')) | |
470 | 443 | >>> n = request['batchSize'] |
471 | 444 | >>> request.replies(cursor={'id': 123, 'firstBatch': documents[:n]}) |
472 | 445 | True |
473 | 446 | >>> while True: |
474 | ... getmore = server.receives(Command('getMore', 123)) | |
447 | ... getmore = server.receives(OpMsg('getMore', 123)) | |
475 | 448 | ... n = getmore['batchSize'] |
476 | 449 | ... if documents: |
477 | 450 | ... cursor_id = 123 |
18 | 18 | |
19 | 19 | __author__ = 'A. Jesse Jiryu Davis' |
20 | 20 | __email__ = 'jesse@mongodb.com' |
21 | __version__ = '1.3.0' | |
21 | __version__ = '1.4.1' | |
22 | 22 | |
23 | 23 | import atexit |
24 | import collections | |
25 | 24 | import contextlib |
26 | 25 | import errno |
27 | 26 | import functools |
45 | 44 | from Queue import Queue, Empty |
46 | 45 | |
47 | 46 | try: |
47 | from collections.abc import Mapping | |
48 | except: | |
49 | from collections import Mapping | |
50 | ||
51 | try: | |
48 | 52 | from collections import OrderedDict |
49 | 53 | except: |
50 | 54 | from ordereddict import OrderedDict # Python 2.6, "pip install ordereddict" |
82 | 86 | 'MockupDB', 'go', 'going', 'Future', 'wait_until', 'interactive_server', |
83 | 87 | |
84 | 88 | 'OP_REPLY', 'OP_UPDATE', 'OP_INSERT', 'OP_QUERY', 'OP_GET_MORE', |
85 | 'OP_DELETE', 'OP_KILL_CURSORS', | |
89 | 'OP_DELETE', 'OP_KILL_CURSORS', 'OP_MSG', | |
86 | 90 | |
87 | 91 | 'QUERY_FLAGS', 'UPDATE_FLAGS', 'INSERT_FLAGS', 'DELETE_FLAGS', |
88 | 'REPLY_FLAGS', | |
92 | 'REPLY_FLAGS', 'OP_MSG_FLAGS', | |
89 | 93 | |
90 | 94 | 'Request', 'Command', 'OpQuery', 'OpGetMore', 'OpKillCursors', 'OpInsert', |
91 | 'OpUpdate', 'OpDelete', 'OpReply', | |
95 | 'OpUpdate', 'OpDelete', 'OpReply', 'OpMsg', | |
92 | 96 | |
93 | 97 | 'Matcher', 'absent', |
94 | 98 | ] |
238 | 242 | OP_GET_MORE = 2005 |
239 | 243 | OP_DELETE = 2006 |
240 | 244 | OP_KILL_CURSORS = 2007 |
245 | OP_MSG = 2013 | |
241 | 246 | |
242 | 247 | QUERY_FLAGS = OrderedDict([ |
243 | 248 | ('TailableCursor', 2), |
262 | 267 | ('CursorNotFound', 1), |
263 | 268 | ('QueryFailure', 2)]) |
264 | 269 | |
270 | OP_MSG_FLAGS = OrderedDict([ | |
271 | ('checksumPresent', 1), | |
272 | ('moreToCome', 2)]) | |
273 | ||
274 | _UNPACK_BYTE = struct.Struct("<b").unpack | |
265 | 275 | _UNPACK_INT = struct.Struct("<i").unpack |
276 | _UNPACK_UINT = struct.Struct("<I").unpack | |
266 | 277 | _UNPACK_LONG = struct.Struct("<q").unpack |
267 | 278 | |
268 | 279 | |
270 | 281 | """Decode a BSON 'C' string to python unicode string.""" |
271 | 282 | end = data.index(b"\x00", position) |
272 | 283 | return _utf_8_decode(data[position:end], None, True)[0], end + 1 |
284 | ||
285 | ||
286 | def _bson_values_equal(a, b): | |
287 | # Check if values are either from our vendored bson or PyMongo's bson. | |
288 | for value in (a, b): | |
289 | if not hasattr(value, '_type_marker'): | |
290 | # Normal equality. | |
291 | return a == b | |
292 | ||
293 | def marker(obj): | |
294 | return getattr(obj, '_type_marker', None) | |
295 | ||
296 | if marker(a) != marker(b): | |
297 | return a == b | |
298 | ||
299 | # Instances of Binary, ObjectId, etc. from our vendored bson don't equal | |
300 | # instances of PyMongo's bson classes that users pass in as message specs, | |
301 | # since isinstance() fails. Reimplement equality checks for each class. | |
302 | key_fn = { | |
303 | # Binary. | |
304 | 5: lambda obj: (obj.subtype, bytes(obj)), | |
305 | # ObjectId. | |
306 | 7: lambda obj: obj.binary, | |
307 | # Regex. | |
308 | 11: lambda obj: (obj.pattern, obj.flags), | |
309 | # Code. | |
310 | 13: lambda obj: (obj.scope, str(obj)), | |
311 | # Timestamp. | |
312 | 17: lambda obj: (obj.time, obj.inc), | |
313 | # Decimal128. | |
314 | 19: lambda obj: obj.bid, | |
315 | # DBRef. | |
316 | 100: lambda obj: (obj.database, obj.collection, obj.id), | |
317 | # MaxKey. | |
318 | 127: lambda obj: 127, | |
319 | # MinKey. | |
320 | 255: lambda obj: 255, | |
321 | }.get(marker(a)) | |
322 | ||
323 | if key_fn: | |
324 | return key_fn(a) == key_fn(b) | |
325 | ||
326 | return a == b | |
273 | 327 | |
274 | 328 | |
275 | 329 | class _PeekableQueue(Queue): |
297 | 351 | |
298 | 352 | |
299 | 353 | class Request(object): |
300 | """Base class for `Command`, `OpInsert`, and so on. | |
354 | """Base class for `Command`, `OpMsg`, and so on. | |
301 | 355 | |
302 | 356 | Some useful asserts you can do in tests: |
303 | 357 | |
309 | 363 | True |
310 | 364 | >>> {'_id': 1} == OpInsert([{'_id': 0}, {'_id': 1}])[1] |
311 | 365 | True |
312 | >>> 'field' in Command(field=1) | |
366 | >>> 'field' in OpMsg(field=1) | |
313 | 367 | True |
314 | >>> 'field' in Command() | |
368 | >>> 'field' in OpMsg() | |
315 | 369 | False |
316 | >>> 'field' in Command('ismaster') | |
370 | >>> 'field' in OpMsg('ismaster') | |
317 | 371 | False |
318 | >>> Command(ismaster=False)['ismaster'] is False | |
372 | >>> OpMsg(ismaster=False)['ismaster'] is False | |
319 | 373 | True |
320 | 374 | """ |
321 | 375 | opcode = None |
332 | 386 | self._verbose = self._server and self._server.verbose |
333 | 387 | self._server_port = kwargs.pop('server_port', None) |
334 | 388 | self._docs = make_docs(*args, **kwargs) |
335 | if not all(isinstance(doc, collections.Mapping) for doc in self._docs): | |
389 | if not all(isinstance(doc, Mapping) for doc in self._docs): | |
336 | 390 | raise_args_err() |
337 | 391 | |
338 | 392 | @property |
456 | 510 | if value is absent: |
457 | 511 | if key in other_doc: |
458 | 512 | return False |
459 | elif other_doc.get(key, None) != value: | |
513 | elif not _bson_values_equal(value, other_doc.get(key, None)): | |
460 | 514 | return False |
461 | 515 | if isinstance(doc, (OrderedDict, _bson.SON)): |
462 | 516 | if not isinstance(other_doc, (OrderedDict, _bson.SON)): |
508 | 562 | parts.append('namespace="%s"' % self._namespace) |
509 | 563 | |
510 | 564 | return '%s(%s)' % (name, ', '.join(str(part) for part in parts)) |
565 | ||
566 | ||
567 | class CommandBase(Request): | |
568 | """A command the client executes on the server.""" | |
569 | is_command = True | |
570 | ||
571 | # Check command name case-insensitively. | |
572 | _non_matched_attrs = Request._non_matched_attrs + ('command_name', ) | |
573 | ||
574 | @property | |
575 | def command_name(self): | |
576 | """The command name or None. | |
577 | ||
578 | >>> OpMsg({'count': 'collection'}).command_name | |
579 | 'count' | |
580 | >>> OpMsg('aggregate', 'collection', cursor=absent).command_name | |
581 | 'aggregate' | |
582 | """ | |
583 | if self.docs and self.docs[0]: | |
584 | return list(self.docs[0])[0] | |
585 | ||
586 | def _matches_docs(self, docs, other_docs): | |
587 | assert len(docs) == len(other_docs) == 1 | |
588 | doc, = docs | |
589 | other_doc, = other_docs | |
590 | items = list(doc.items()) | |
591 | other_items = list(other_doc.items()) | |
592 | ||
593 | # Compare command name case-insensitively. | |
594 | if items and other_items: | |
595 | if items[0][0].lower() != other_items[0][0].lower(): | |
596 | return False | |
597 | if not _bson_values_equal(items[0][1], other_items[0][1]): | |
598 | return False | |
599 | return super(CommandBase, self)._matches_docs( | |
600 | [OrderedDict(items[1:])], | |
601 | [OrderedDict(other_items[1:])]) | |
602 | ||
603 | ||
604 | class OpMsg(CommandBase): | |
605 | """An OP_MSG request the client executes on the server.""" | |
606 | opcode = OP_MSG | |
607 | is_command = True | |
608 | _flags_map = OP_MSG_FLAGS | |
609 | ||
610 | @classmethod | |
611 | def unpack(cls, msg, client, server, request_id): | |
612 | """Parse message and return an `OpMsg`. | |
613 | ||
614 | Takes the client message as bytes, the client and server socket objects, | |
615 | and the client request id. | |
616 | """ | |
617 | flags, = _UNPACK_UINT(msg[:4]) | |
618 | pos = 4 | |
619 | first_payload_type, = _UNPACK_BYTE(msg[pos:pos+1]) | |
620 | pos += 1 | |
621 | first_payload_size, = _UNPACK_INT(msg[pos:pos+4]) | |
622 | if flags != 0 and flags != 2: | |
623 | raise ValueError('OP_MSG flag must be 0 or 2 not %r' % (flags,)) | |
624 | if first_payload_type != 0: | |
625 | raise ValueError('First OP_MSG payload type must be 0 not %r' % ( | |
626 | first_payload_type,)) | |
627 | ||
628 | # Parse the initial document and add the optional payload type 1. | |
629 | payload_document = _bson.decode_all(msg[pos:pos+first_payload_size], | |
630 | CODEC_OPTIONS)[0] | |
631 | pos += first_payload_size | |
632 | if len(msg) != pos: | |
633 | payload_type, = _UNPACK_BYTE(msg[pos:pos+1]) | |
634 | pos += 1 | |
635 | if payload_type != 1: | |
636 | raise ValueError('Second OP_MSG payload type must be 1 not %r' | |
637 | % (payload_type,)) | |
638 | section_size, = _UNPACK_INT(msg[pos:pos+4]) | |
639 | if len(msg) != pos + section_size: | |
640 | raise ValueError('More than two OP_MSG sections unsupported') | |
641 | pos += 4 | |
642 | identifier, pos = _get_c_string(msg, pos) | |
643 | documents = _bson.decode_all(msg[pos:], CODEC_OPTIONS) | |
644 | payload_document[identifier] = documents | |
645 | ||
646 | database = payload_document['$db'] | |
647 | return OpMsg(payload_document, namespace=database, flags=flags, | |
648 | _client=client, request_id=request_id, | |
649 | _server=server) | |
650 | ||
651 | def __init__(self, *args, **kwargs): | |
652 | super(OpMsg, self).__init__(*args, **kwargs) | |
653 | if len(self._docs) > 1: | |
654 | raise_args_err('OpMsg too many documents', ValueError) | |
655 | ||
656 | @property | |
657 | def slave_ok(self): | |
658 | """True if this OpMsg can read from a secondary.""" | |
659 | read_preference = self.doc.get('$readPreference') | |
660 | return read_preference and read_preference.get('mode') != 'primary' | |
661 | ||
662 | slave_okay = slave_ok | |
663 | """Synonym for `.slave_ok`.""" | |
664 | ||
665 | @property | |
666 | def command_name(self): | |
667 | """The command name or None. | |
668 | ||
669 | >>> OpMsg({'count': 'collection'}).command_name | |
670 | 'count' | |
671 | >>> OpMsg('aggregate', 'collection', cursor=absent).command_name | |
672 | 'aggregate' | |
673 | """ | |
674 | if self.docs and self.docs[0]: | |
675 | return list(self.docs[0])[0] | |
676 | ||
677 | def _replies(self, *args, **kwargs): | |
678 | if self.flags & OP_MSG_FLAGS['moreToCome']: | |
679 | assert False, "Cannot reply to OpMsg with moreToCome: %r" % (self,) | |
680 | reply = make_op_msg_reply(*args, **kwargs) | |
681 | if not reply.docs: | |
682 | reply.docs = [{'ok': 1}] | |
683 | else: | |
684 | if len(reply.docs) > 1: | |
685 | raise ValueError('OP_MSG reply with multiple documents: %s' | |
686 | % (reply.docs, )) | |
687 | reply.doc.setdefault('ok', 1) | |
688 | super(OpMsg, self)._replies(reply) | |
511 | 689 | |
512 | 690 | |
513 | 691 | class OpQuery(Request): |
554 | 732 | |
555 | 733 | def __init__(self, *args, **kwargs): |
556 | 734 | fields = kwargs.pop('fields', None) |
557 | if fields is not None and not isinstance(fields, collections.Mapping): | |
735 | if fields is not None and not isinstance(fields, Mapping): | |
558 | 736 | raise_args_err() |
559 | 737 | self._fields = fields |
560 | 738 | self._num_to_skip = kwargs.pop('num_to_skip', None) |
591 | 769 | return rep + ')' |
592 | 770 | |
593 | 771 | |
594 | class Command(OpQuery): | |
772 | class Command(CommandBase, OpQuery): | |
595 | 773 | """A command the client executes on the server.""" |
596 | is_command = True | |
597 | ||
598 | # Check command name case-insensitively. | |
599 | _non_matched_attrs = OpQuery._non_matched_attrs + ('command_name', ) | |
600 | ||
601 | @property | |
602 | def command_name(self): | |
603 | """The command name or None. | |
604 | ||
605 | >>> Command({'count': 'collection'}).command_name | |
606 | 'count' | |
607 | >>> Command('aggregate', 'collection', cursor=absent).command_name | |
608 | 'aggregate' | |
609 | """ | |
610 | if self.docs and self.docs[0]: | |
611 | return list(self.docs[0])[0] | |
612 | ||
613 | def _matches_docs(self, docs, other_docs): | |
614 | assert len(docs) == len(other_docs) == 1 | |
615 | doc, = docs | |
616 | other_doc, = other_docs | |
617 | items = list(doc.items()) | |
618 | other_items = list(other_doc.items()) | |
619 | ||
620 | # Compare command name case-insensitively. | |
621 | if items and other_items: | |
622 | if items[0][0].lower() != other_items[0][0].lower(): | |
623 | return False | |
624 | if items[0][1] != other_items[0][1]: | |
625 | return False | |
626 | return super(Command, self)._matches_docs( | |
627 | [OrderedDict(items[1:])], | |
628 | [OrderedDict(other_items[1:])]) | |
629 | 774 | |
630 | 775 | def _replies(self, *args, **kwargs): |
631 | 776 | reply = make_reply(*args, **kwargs) |
779 | 924 | request_id=request_id, _server=server) |
780 | 925 | |
781 | 926 | |
782 | class OpReply(object): | |
927 | class Reply(object): | |
783 | 928 | """A reply from `MockupDB` to the client.""" |
784 | 929 | def __init__(self, *args, **kwargs): |
785 | 930 | self._flags = kwargs.pop('flags', 0) |
931 | self._docs = make_docs(*args, **kwargs) | |
932 | ||
933 | @property | |
934 | def doc(self): | |
935 | """Contents of reply. | |
936 | ||
937 | Useful for replies to commands; replies to other messages may have no | |
938 | documents or multiple documents. | |
939 | """ | |
940 | assert len(self._docs) == 1, '%s has more than one document' % self | |
941 | return self._docs[0] | |
942 | ||
943 | def __str__(self): | |
944 | return docs_repr(*self._docs) | |
945 | ||
946 | def __repr__(self): | |
947 | rep = '%s(%s' % (self.__class__.__name__, self) | |
948 | if self._flags: | |
949 | rep += ', flags=' + '|'.join( | |
950 | name for name, value in REPLY_FLAGS.items() | |
951 | if self._flags & value) | |
952 | ||
953 | return rep + ')' | |
954 | ||
955 | ||
956 | class OpReply(Reply): | |
957 | """An OP_REPLY reply from `MockupDB` to the client.""" | |
958 | def __init__(self, *args, **kwargs): | |
786 | 959 | self._cursor_id = kwargs.pop('cursor_id', 0) |
787 | 960 | self._starting_from = kwargs.pop('starting_from', 0) |
788 | self._docs = make_docs(*args, **kwargs) | |
961 | super(OpReply, self).__init__(*args, **kwargs) | |
789 | 962 | |
790 | 963 | @property |
791 | 964 | def docs(self): |
795 | 968 | @docs.setter |
796 | 969 | def docs(self, docs): |
797 | 970 | self._docs = make_docs(docs) |
798 | ||
799 | @property | |
800 | def doc(self): | |
801 | """Contents of reply. | |
802 | ||
803 | Useful for replies to commands; replies to other messages may have no | |
804 | documents or multiple documents. | |
805 | """ | |
806 | assert len(self._docs) == 1, '%s has more than one document' % self | |
807 | return self._docs[0] | |
808 | 971 | |
809 | 972 | def update(self, *args, **kwargs): |
810 | 973 | """Update the document. Same as ``dict().update()``. |
835 | 998 | message += struct.pack("<i", OP_REPLY) |
836 | 999 | return message + data |
837 | 1000 | |
838 | def __str__(self): | |
839 | return docs_repr(*self._docs) | |
1001 | ||
1002 | class OpMsgReply(Reply): | |
1003 | """A OP_MSG reply from `MockupDB` to the client.""" | |
1004 | def __init__(self, *args, **kwargs): | |
1005 | super(OpMsgReply, self).__init__(*args, **kwargs) | |
1006 | assert len(self._docs) <= 1, 'OpMsgReply can only have one document' | |
1007 | ||
1008 | @property | |
1009 | def docs(self): | |
1010 | """The reply documents, if any.""" | |
1011 | return self._docs | |
1012 | ||
1013 | @docs.setter | |
1014 | def docs(self, docs): | |
1015 | self._docs = make_docs(docs) | |
1016 | assert len(self._docs) == 1, 'OpMsgReply must have one document' | |
1017 | ||
1018 | def update(self, *args, **kwargs): | |
1019 | """Update the document. Same as ``dict().update()``. | |
1020 | ||
1021 | >>> reply = OpMsgReply({'ismaster': True}) | |
1022 | >>> reply.update(maxWireVersion=3) | |
1023 | >>> reply.doc['maxWireVersion'] | |
1024 | 3 | |
1025 | >>> reply.update({'maxWriteBatchSize': 10, 'msg': 'isdbgrid'}) | |
1026 | """ | |
1027 | self.doc.update(*args, **kwargs) | |
1028 | ||
1029 | def reply_bytes(self, request): | |
1030 | """Take a `Request` and return an OP_MSG message as bytes.""" | |
1031 | flags = struct.pack("<I", self._flags) | |
1032 | payload_type = struct.pack("<b", 0) | |
1033 | payload_data = _bson.BSON.encode(self.doc) | |
1034 | data = b''.join([flags, payload_type, payload_data]) | |
1035 | ||
1036 | reply_id = random.randint(0, 1000000) | |
1037 | response_to = request.request_id | |
1038 | ||
1039 | header = struct.pack( | |
1040 | "<iiii", 16 + len(data), reply_id, response_to, OP_MSG) | |
1041 | return header + data | |
840 | 1042 | |
841 | 1043 | def __repr__(self): |
842 | 1044 | rep = '%s(%s' % (self.__class__.__name__, self) |
843 | if self._starting_from: | |
844 | rep += ', starting_from=%d' % self._starting_from | |
845 | ||
846 | 1045 | if self._flags: |
847 | 1046 | rep += ', flags=' + '|'.join( |
848 | name for name, value in REPLY_FLAGS.items() | |
1047 | name for name, value in OP_MSG_FLAGS.items() | |
849 | 1048 | if self._flags & value) |
850 | 1049 | |
851 | 1050 | return rep + ')' |
1022 | 1221 | self._autoresponders = [] |
1023 | 1222 | |
1024 | 1223 | if auto_ismaster is True: |
1025 | self.autoresponds(Command('ismaster'), | |
1224 | self.autoresponds(CommandBase('ismaster'), | |
1026 | 1225 | {'ismaster': True, |
1027 | 1226 | 'minWireVersion': min_wire_version, |
1028 | 1227 | 'maxWireVersion': max_wire_version}) |
1029 | 1228 | elif auto_ismaster: |
1030 | self.autoresponds(Command('ismaster'), auto_ismaster) | |
1229 | self.autoresponds(CommandBase('ismaster'), auto_ismaster) | |
1031 | 1230 | |
1032 | 1231 | @_synchronized |
1033 | 1232 | def run(self): |
1108 | 1307 | >>> future = go(client.db.command, 'foo') |
1109 | 1308 | >>> s.got('foo') |
1110 | 1309 | True |
1111 | >>> s.got(Command('foo', namespace='db')) | |
1310 | >>> s.got(OpMsg('foo', namespace='db')) | |
1112 | 1311 | True |
1113 | >>> s.got(Command('foo', key='value')) | |
1312 | >>> s.got(OpMsg('foo', key='value')) | |
1114 | 1313 | False |
1115 | 1314 | >>> s.ok() |
1116 | 1315 | >>> future() == {'ok': 1} |
1174 | 1373 | |
1175 | 1374 | The remaining arguments are a :ref:`message spec <message spec>`: |
1176 | 1375 | |
1376 | >>> # ok | |
1177 | 1377 | >>> responder = s.autoresponds('bar', ok=0, errmsg='err') |
1178 | 1378 | >>> client.db.command('bar') |
1179 | 1379 | Traceback (most recent call last): |
1180 | 1380 | ... |
1181 | 1381 | OperationFailure: command SON([('bar', 1)]) on namespace db.$cmd failed: err |
1182 | >>> responder = s.autoresponds(Command('find', 'collection'), | |
1382 | >>> responder = s.autoresponds(OpMsg('find', 'collection'), | |
1183 | 1383 | ... {'cursor': {'id': 0, 'firstBatch': [{'_id': 1}, {'_id': 2}]}}) |
1384 | >>> # ok | |
1184 | 1385 | >>> list(client.db.collection.find()) == [{'_id': 1}, {'_id': 2}] |
1185 | 1386 | True |
1186 | >>> responder = s.autoresponds(Command('find', 'collection'), | |
1387 | >>> responder = s.autoresponds(OpMsg('find', 'collection'), | |
1187 | 1388 | ... {'cursor': {'id': 0, 'firstBatch': [{'a': 1}, {'a': 2}]}}) |
1389 | >>> # bad | |
1188 | 1390 | >>> list(client.db.collection.find()) == [{'a': 1}, {'a': 2}] |
1189 | 1391 | True |
1190 | 1392 | |
1196 | 1398 | and replied to. Future matching requests skip the queue. |
1197 | 1399 | |
1198 | 1400 | >>> future = go(client.db.command, 'baz') |
1401 | >>> # bad | |
1199 | 1402 | >>> responder = s.autoresponds('baz', {'key': 'value'}) |
1200 | 1403 | >>> future() == {'ok': 1, 'key': 'value'} |
1201 | 1404 | True |
1235 | 1438 | ... print('logging: %r' % request) |
1236 | 1439 | >>> responder = s.autoresponds(logger) |
1237 | 1440 | >>> client.db.command('baz') == {'ok': 1, 'a': 2} |
1238 | logging: Command({"baz": 1}, flags=SlaveOkay, namespace="db") | |
1441 | logging: OpMsg({"baz": 1, "$db": "db", "$readPreference": {"mode": "primaryPreferred"}}, namespace="db") | |
1239 | 1442 | True |
1240 | 1443 | |
1241 | 1444 | The synonym `subscribe` better expresses your intent if your handler |
1465 | 1668 | raise socket.error('could not bind socket') |
1466 | 1669 | |
1467 | 1670 | |
1468 | OPCODES = {OP_QUERY: OpQuery, | |
1671 | OPCODES = {OP_MSG: OpMsg, | |
1672 | OP_QUERY: OpQuery, | |
1469 | 1673 | OP_INSERT: OpInsert, |
1470 | 1674 | OP_UPDATE: OpUpdate, |
1471 | 1675 | OP_DELETE: OpDelete, |
1510 | 1714 | |
1511 | 1715 | |
1512 | 1716 | def make_docs(*args, **kwargs): |
1513 | """Make the documents for a `Request` or `OpReply`. | |
1717 | """Make the documents for a `Request` or `Reply`. | |
1514 | 1718 | |
1515 | 1719 | Takes a variety of argument styles, returns a list of dicts. |
1516 | 1720 | |
1536 | 1740 | |
1537 | 1741 | if isinstance(args[0], (list, tuple)): |
1538 | 1742 | # Send a batch: OpReply([{'a': 1}, {'a': 2}]). |
1539 | if not all(isinstance(doc, (OpReply, collections.Mapping)) | |
1743 | if not all(isinstance(doc, (OpReply, Mapping)) | |
1540 | 1744 | for doc in args[0]): |
1541 | 1745 | raise_args_err('each doc must be a dict:') |
1542 | 1746 | if kwargs: |
1560 | 1764 | raise_args_err(err_msg, ValueError) |
1561 | 1765 | |
1562 | 1766 | # Send a batch as varargs: OpReply({'a': 1}, {'a': 2}). |
1563 | if not all(isinstance(doc, (OpReply, collections.Mapping)) for doc in args): | |
1767 | if not all(isinstance(doc, (OpReply, Mapping)) for doc in args): | |
1564 | 1768 | raise_args_err('each doc must be a dict') |
1565 | 1769 | |
1566 | 1770 | return args |
1602 | 1806 | |
1603 | 1807 | def make_reply(*args, **kwargs): |
1604 | 1808 | # Error we might raise. |
1605 | if args and isinstance(args[0], OpReply): | |
1809 | if args and isinstance(args[0], (OpReply, OpMsgReply)): | |
1606 | 1810 | if args[1:] or kwargs: |
1607 | 1811 | raise_args_err("can't interpret args") |
1608 | 1812 | return args[0] |
1609 | 1813 | |
1610 | 1814 | return OpReply(*args, **kwargs) |
1815 | ||
1816 | ||
1817 | def make_op_msg_reply(*args, **kwargs): | |
1818 | # Error we might raise. | |
1819 | if args and isinstance(args[0], (OpReply, OpMsgReply)): | |
1820 | if args[1:] or kwargs: | |
1821 | raise_args_err("can't interpret args") | |
1822 | return args[0] | |
1823 | ||
1824 | return OpMsgReply(*args, **kwargs) | |
1611 | 1825 | |
1612 | 1826 | |
1613 | 1827 | def unprefixed(bson_str): |
1727 | 1941 | server.autoresponds('whatsmyuri', you='localhost:12345') |
1728 | 1942 | server.autoresponds({'getLog': 'startupWarnings'}, |
1729 | 1943 | log=['hello from %s!' % name]) |
1730 | server.autoresponds(Command('buildInfo'), version='MockupDB ' + __version__) | |
1731 | server.autoresponds(Command('listCollections')) | |
1944 | server.autoresponds(OpMsg('buildInfo'), version='MockupDB ' + __version__) | |
1945 | server.autoresponds(OpMsg('listCollections')) | |
1732 | 1946 | server.autoresponds('replSetGetStatus', ok=0) |
1733 | 1947 | return server |
0 | # Copyright 2009-2015 MongoDB, Inc. | |
0 | # Copyright 2009-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
12 | 12 | # limitations under the License. |
13 | 13 | |
14 | 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. | |
15 | 64 | """ |
16 | 65 | |
17 | 66 | import calendar |
18 | import collections | |
19 | 67 | import datetime |
20 | 68 | import itertools |
21 | 69 | import re |
30 | 78 | JAVA_LEGACY, CSHARP_LEGACY, |
31 | 79 | UUIDLegacy) |
32 | 80 | from mockupdb._bson.code import Code |
33 | from mockupdb._bson.codec_options import CodecOptions, DEFAULT_CODEC_OPTIONS | |
81 | from mockupdb._bson.codec_options import ( | |
82 | CodecOptions, DEFAULT_CODEC_OPTIONS, _raw_document_class) | |
34 | 83 | from mockupdb._bson.dbref import DBRef |
84 | from mockupdb._bson.decimal128 import Decimal128 | |
35 | 85 | from mockupdb._bson.errors import (InvalidBSON, |
36 | 86 | InvalidDocument, |
37 | 87 | InvalidStringData) |
39 | 89 | from mockupdb._bson.max_key import MaxKey |
40 | 90 | from mockupdb._bson.min_key import MinKey |
41 | 91 | from mockupdb._bson.objectid import ObjectId |
42 | from mockupdb._bson.py3compat import (b, | |
92 | from mockupdb._bson.py3compat import (abc, | |
93 | b, | |
43 | 94 | PY3, |
44 | 95 | iteritems, |
45 | 96 | text_type, |
80 | 131 | BSONINT = b"\x10" # 32bit int |
81 | 132 | BSONTIM = b"\x11" # Timestamp |
82 | 133 | BSONLON = b"\x12" # 64bit int |
134 | BSONDEC = b"\x13" # Decimal128 | |
83 | 135 | BSONMIN = b"\xFF" # Min key |
84 | 136 | BSONMAX = b"\x7F" # Max key |
85 | 137 | |
91 | 143 | _UNPACK_TIMESTAMP = struct.Struct("<II").unpack |
92 | 144 | |
93 | 145 | |
94 | def _get_int(data, position, dummy0, dummy1): | |
146 | def _raise_unknown_type(element_type, element_name): | |
147 | """Unknown type helper.""" | |
148 | raise InvalidBSON("Detected unknown BSON type %r for fieldname '%s'. Are " | |
149 | "you using the latest driver version?" % ( | |
150 | element_type, element_name)) | |
151 | ||
152 | ||
153 | def _get_int(data, position, dummy0, dummy1, dummy2): | |
95 | 154 | """Decode a BSON int32 to python int.""" |
96 | 155 | end = position + 4 |
97 | 156 | return _UNPACK_INT(data[position:end])[0], end |
104 | 163 | opts.unicode_decode_error_handler, True)[0], end + 1 |
105 | 164 | |
106 | 165 | |
107 | def _get_float(data, position, dummy0, dummy1): | |
166 | def _get_float(data, position, dummy0, dummy1, dummy2): | |
108 | 167 | """Decode a BSON double to python float.""" |
109 | 168 | end = position + 8 |
110 | 169 | return _UNPACK_FLOAT(data[position:end])[0], end |
111 | 170 | |
112 | 171 | |
113 | def _get_string(data, position, obj_end, opts): | |
172 | def _get_string(data, position, obj_end, opts, dummy): | |
114 | 173 | """Decode a BSON string to python unicode string.""" |
115 | 174 | length = _UNPACK_INT(data[position:position + 4])[0] |
116 | 175 | position += 4 |
123 | 182 | opts.unicode_decode_error_handler, True)[0], end + 1 |
124 | 183 | |
125 | 184 | |
126 | def _get_object(data, position, obj_end, opts): | |
185 | def _get_object(data, position, obj_end, opts, dummy): | |
127 | 186 | """Decode a BSON subdocument to opts.document_class or bson.dbref.DBRef.""" |
128 | 187 | obj_size = _UNPACK_INT(data[position:position + 4])[0] |
129 | 188 | end = position + obj_size - 1 |
131 | 190 | raise InvalidBSON("bad eoo") |
132 | 191 | if end >= obj_end: |
133 | 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 | ||
134 | 197 | obj = _elements_to_dict(data, position + 4, end, opts) |
135 | 198 | |
136 | 199 | position += obj_size |
140 | 203 | return obj, position |
141 | 204 | |
142 | 205 | |
143 | def _get_array(data, position, obj_end, opts): | |
206 | def _get_array(data, position, obj_end, opts, element_name): | |
144 | 207 | """Decode a BSON array to python list.""" |
145 | 208 | size = _UNPACK_INT(data[position:position + 4])[0] |
146 | 209 | end = position + size - 1 |
147 | 210 | if data[end:end + 1] != b"\x00": |
148 | 211 | raise InvalidBSON("bad eoo") |
212 | ||
149 | 213 | position += 4 |
150 | 214 | end -= 1 |
151 | 215 | result = [] |
159 | 223 | element_type = data[position:position + 1] |
160 | 224 | # Just skip the keys. |
161 | 225 | position = index(b'\x00', position) + 1 |
162 | value, position = getter[element_type](data, position, obj_end, opts) | |
226 | try: | |
227 | value, position = getter[element_type]( | |
228 | data, position, obj_end, opts, element_name) | |
229 | except KeyError: | |
230 | _raise_unknown_type(element_type, element_name) | |
163 | 231 | append(value) |
232 | ||
233 | if position != end + 1: | |
234 | raise InvalidBSON('bad array length') | |
164 | 235 | return result, position + 1 |
165 | 236 | |
166 | 237 | |
167 | def _get_binary(data, position, dummy, opts): | |
238 | def _get_binary(data, position, obj_end, opts, dummy1): | |
168 | 239 | """Decode a BSON binary to bson.binary.Binary or python UUID.""" |
169 | 240 | length, subtype = _UNPACK_LENGTH_SUBTYPE(data[position:position + 5]) |
170 | 241 | position += 5 |
175 | 246 | raise InvalidBSON("invalid binary (st 2) - lengths don't match!") |
176 | 247 | length = length2 |
177 | 248 | end = position + length |
178 | if subtype in (3, 4): | |
249 | if length < 0 or end > obj_end: | |
250 | raise InvalidBSON('bad binary object length') | |
251 | if subtype == 3: | |
179 | 252 | # Java Legacy |
180 | 253 | uuid_representation = opts.uuid_representation |
181 | 254 | if uuid_representation == JAVA_LEGACY: |
188 | 261 | else: |
189 | 262 | value = uuid.UUID(bytes=data[position:end]) |
190 | 263 | return value, end |
264 | if subtype == 4: | |
265 | return uuid.UUID(bytes=data[position:end]), end | |
191 | 266 | # Python3 special case. Decode subtype 0 to 'bytes'. |
192 | 267 | if PY3 and subtype == 0: |
193 | 268 | value = data[position:end] |
196 | 271 | return value, end |
197 | 272 | |
198 | 273 | |
199 | def _get_oid(data, position, dummy0, dummy1): | |
274 | def _get_oid(data, position, dummy0, dummy1, dummy2): | |
200 | 275 | """Decode a BSON ObjectId to bson.objectid.ObjectId.""" |
201 | 276 | end = position + 12 |
202 | 277 | return ObjectId(data[position:end]), end |
203 | 278 | |
204 | 279 | |
205 | def _get_boolean(data, position, dummy0, dummy1): | |
280 | def _get_boolean(data, position, dummy0, dummy1, dummy2): | |
206 | 281 | """Decode a BSON true/false to python True/False.""" |
207 | 282 | end = position + 1 |
208 | return data[position:end] == b"\x01", end | |
209 | ||
210 | ||
211 | def _get_date(data, position, dummy, opts): | |
283 | boolean_byte = data[position:end] | |
284 | if boolean_byte == b'\x00': | |
285 | return False, end | |
286 | elif boolean_byte == b'\x01': | |
287 | return True, end | |
288 | raise InvalidBSON('invalid boolean value: %r' % boolean_byte) | |
289 | ||
290 | ||
291 | def _get_date(data, position, dummy0, opts, dummy1): | |
212 | 292 | """Decode a BSON datetime to python datetime.datetime.""" |
213 | 293 | end = position + 8 |
214 | 294 | millis = _UNPACK_LONG(data[position:end])[0] |
215 | diff = ((millis % 1000) + 1000) % 1000 | |
216 | seconds = (millis - diff) / 1000 | |
217 | micros = diff * 1000 | |
218 | if opts.tz_aware: | |
219 | dt = EPOCH_AWARE + datetime.timedelta( | |
220 | seconds=seconds, microseconds=micros) | |
221 | if opts.tzinfo: | |
222 | dt = dt.astimezone(opts.tzinfo) | |
223 | else: | |
224 | dt = EPOCH_NAIVE + datetime.timedelta( | |
225 | seconds=seconds, microseconds=micros) | |
226 | return dt, end | |
227 | ||
228 | ||
229 | def _get_code(data, position, obj_end, opts): | |
295 | return _millis_to_datetime(millis, opts), end | |
296 | ||
297 | ||
298 | def _get_code(data, position, obj_end, opts, element_name): | |
230 | 299 | """Decode a BSON code to bson.code.Code.""" |
231 | code, position = _get_string(data, position, obj_end, opts) | |
300 | code, position = _get_string(data, position, obj_end, opts, element_name) | |
232 | 301 | return Code(code), position |
233 | 302 | |
234 | 303 | |
235 | def _get_code_w_scope(data, position, obj_end, opts): | |
304 | def _get_code_w_scope(data, position, obj_end, opts, element_name): | |
236 | 305 | """Decode a BSON code_w_scope to bson.code.Code.""" |
237 | code, position = _get_string(data, position + 4, obj_end, opts) | |
238 | scope, position = _get_object(data, position, obj_end, opts) | |
306 | code_end = position + _UNPACK_INT(data[position:position + 4])[0] | |
307 | code, position = _get_string( | |
308 | data, position + 4, code_end, opts, element_name) | |
309 | scope, position = _get_object(data, position, code_end, opts, element_name) | |
310 | if position != code_end: | |
311 | raise InvalidBSON('scope outside of javascript code boundaries') | |
239 | 312 | return Code(code, scope), position |
240 | 313 | |
241 | 314 | |
242 | def _get_regex(data, position, dummy0, opts): | |
315 | def _get_regex(data, position, dummy0, opts, dummy1): | |
243 | 316 | """Decode a BSON regex to bson.regex.Regex or a python pattern object.""" |
244 | 317 | pattern, position = _get_c_string(data, position, opts) |
245 | 318 | bson_flags, position = _get_c_string(data, position, opts) |
247 | 320 | return bson_re, position |
248 | 321 | |
249 | 322 | |
250 | def _get_ref(data, position, obj_end, opts): | |
323 | def _get_ref(data, position, obj_end, opts, element_name): | |
251 | 324 | """Decode (deprecated) BSON DBPointer to bson.dbref.DBRef.""" |
252 | collection, position = _get_string(data, position, obj_end, opts) | |
253 | oid, position = _get_oid(data, position, obj_end, opts) | |
325 | collection, position = _get_string( | |
326 | data, position, obj_end, opts, element_name) | |
327 | oid, position = _get_oid(data, position, obj_end, opts, element_name) | |
254 | 328 | return DBRef(collection, oid), position |
255 | 329 | |
256 | 330 | |
257 | def _get_timestamp(data, position, dummy0, dummy1): | |
331 | def _get_timestamp(data, position, dummy0, dummy1, dummy2): | |
258 | 332 | """Decode a BSON timestamp to bson.timestamp.Timestamp.""" |
259 | 333 | end = position + 8 |
260 | 334 | inc, timestamp = _UNPACK_TIMESTAMP(data[position:end]) |
261 | 335 | return Timestamp(timestamp, inc), end |
262 | 336 | |
263 | 337 | |
264 | def _get_int64(data, position, dummy0, dummy1): | |
338 | def _get_int64(data, position, dummy0, dummy1, dummy2): | |
265 | 339 | """Decode a BSON int64 to bson.int64.Int64.""" |
266 | 340 | end = position + 8 |
267 | 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 | |
268 | 348 | |
269 | 349 | |
270 | 350 | # Each decoder function's signature is: |
278 | 358 | BSONOBJ: _get_object, |
279 | 359 | BSONARR: _get_array, |
280 | 360 | BSONBIN: _get_binary, |
281 | BSONUND: lambda w, x, y, z: (None, x), # Deprecated undefined | |
361 | BSONUND: lambda v, w, x, y, z: (None, w), # Deprecated undefined | |
282 | 362 | BSONOID: _get_oid, |
283 | 363 | BSONBOO: _get_boolean, |
284 | 364 | BSONDAT: _get_date, |
285 | BSONNUL: lambda w, x, y, z: (None, x), | |
365 | BSONNUL: lambda v, w, x, y, z: (None, w), | |
286 | 366 | BSONRGX: _get_regex, |
287 | 367 | BSONREF: _get_ref, # Deprecated DBPointer |
288 | 368 | BSONCOD: _get_code, |
291 | 371 | BSONINT: _get_int, |
292 | 372 | BSONTIM: _get_timestamp, |
293 | 373 | BSONLON: _get_int64, |
294 | BSONMIN: lambda w, x, y, z: (MinKey(), x), | |
295 | BSONMAX: lambda w, x, y, z: (MaxKey(), x)} | |
374 | BSONDEC: _get_decimal128, | |
375 | BSONMIN: lambda v, w, x, y, z: (MinKey(), w), | |
376 | BSONMAX: lambda v, w, x, y, z: (MaxKey(), w)} | |
296 | 377 | |
297 | 378 | |
298 | 379 | def _element_to_dict(data, position, obj_end, opts): |
300 | 381 | element_type = data[position:position + 1] |
301 | 382 | position += 1 |
302 | 383 | element_name, position = _get_c_string(data, position, opts) |
303 | value, position = _ELEMENT_GETTER[element_type](data, | |
304 | position, obj_end, opts) | |
384 | try: | |
385 | value, position = _ELEMENT_GETTER[element_type](data, position, | |
386 | obj_end, opts, | |
387 | element_name) | |
388 | except KeyError: | |
389 | _raise_unknown_type(element_type, element_name) | |
305 | 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 | |
306 | 400 | |
307 | 401 | |
308 | 402 | def _elements_to_dict(data, position, obj_end, opts): |
309 | 403 | """Decode a BSON document.""" |
310 | 404 | result = opts.document_class() |
311 | end = obj_end - 1 | |
312 | while position < end: | |
313 | (key, value, position) = _element_to_dict(data, position, obj_end, opts) | |
405 | pos = position | |
406 | for key, value, pos in _iterate_elements(data, position, obj_end, opts): | |
314 | 407 | result[key] = value |
408 | if pos != obj_end: | |
409 | raise InvalidBSON('bad object or element length') | |
315 | 410 | return result |
316 | 411 | |
317 | 412 | |
326 | 421 | if data[obj_size - 1:obj_size] != b"\x00": |
327 | 422 | raise InvalidBSON("bad eoo") |
328 | 423 | try: |
424 | if _raw_document_class(opts.document_class): | |
425 | return opts.document_class(data, opts) | |
329 | 426 | return _elements_to_dict(data, 4, obj_size - 1, opts) |
330 | 427 | except InvalidBSON: |
331 | 428 | raise |
428 | 525 | |
429 | 526 | def _encode_mapping(name, value, check_keys, opts): |
430 | 527 | """Encode a mapping type.""" |
528 | if _raw_document_class(value): | |
529 | return b'\x03' + name + value.raw | |
431 | 530 | data = b"".join([_element_to_bson(key, val, check_keys, opts) |
432 | 531 | for key, val in iteritems(value)]) |
433 | 532 | return b"\x03" + name + _PACK_INT(len(data) + 5) + data + b"\x00" |
508 | 607 | |
509 | 608 | def _encode_datetime(name, value, dummy0, dummy1): |
510 | 609 | """Encode datetime.datetime.""" |
511 | if value.utcoffset() is not None: | |
512 | value = value - value.utcoffset() | |
513 | millis = int(calendar.timegm(value.timetuple()) * 1000 + | |
514 | value.microsecond / 1000) | |
610 | millis = _datetime_to_millis(value) | |
515 | 611 | return b"\x09" + name + _PACK_LONG(millis) |
516 | 612 | |
517 | 613 | |
551 | 647 | """Encode bson.code.Code.""" |
552 | 648 | cstring = _make_c_string(value) |
553 | 649 | cstrlen = len(cstring) |
554 | if not value.scope: | |
650 | if value.scope is None: | |
555 | 651 | return b"\x0D" + name + _PACK_INT(cstrlen) + cstring |
556 | 652 | scope = _dict_to_bson(value.scope, False, opts, False) |
557 | 653 | full_length = _PACK_INT(8 + cstrlen + len(scope)) |
580 | 676 | return b"\x12" + name + _PACK_LONG(value) |
581 | 677 | except struct.error: |
582 | 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 | |
583 | 684 | |
584 | 685 | |
585 | 686 | def _encode_minkey(name, dummy0, dummy1, dummy2): |
622 | 723 | SON: _encode_mapping, |
623 | 724 | Timestamp: _encode_timestamp, |
624 | 725 | UUIDLegacy: _encode_binary, |
726 | Decimal128: _encode_decimal128, | |
625 | 727 | # Special case. This will never be looked up directly. |
626 | collections.Mapping: _encode_mapping, | |
728 | abc.Mapping: _encode_mapping, | |
627 | 729 | } |
628 | 730 | |
629 | 731 | |
693 | 795 | |
694 | 796 | def _dict_to_bson(doc, check_keys, opts, top_level=True): |
695 | 797 | """Encode a document to BSON.""" |
798 | if _raw_document_class(doc): | |
799 | return doc.raw | |
696 | 800 | try: |
697 | 801 | elements = [] |
698 | 802 | if top_level and "_id" in doc: |
711 | 815 | _dict_to_bson = _cbson._dict_to_bson |
712 | 816 | |
713 | 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 | ||
714 | 842 | _CODEC_OPTIONS_TYPE_ERROR = TypeError( |
715 | 843 | "codec_options must be an instance of CodecOptions") |
716 | 844 | |
750 | 878 | docs = [] |
751 | 879 | position = 0 |
752 | 880 | end = len(data) - 1 |
881 | use_raw = _raw_document_class(codec_options.document_class) | |
753 | 882 | try: |
754 | 883 | while position < end: |
755 | 884 | obj_size = _UNPACK_INT(data[position:position + 4])[0] |
758 | 887 | obj_end = position + obj_size - 1 |
759 | 888 | if data[obj_end:position + obj_size] != b"\x00": |
760 | 889 | raise InvalidBSON("bad eoo") |
761 | docs.append(_elements_to_dict(data, | |
762 | position + 4, | |
763 | obj_end, | |
764 | codec_options)) | |
890 | if use_raw: | |
891 | docs.append( | |
892 | codec_options.document_class( | |
893 | data[position:obj_end + 1], codec_options)) | |
894 | else: | |
895 | docs.append(_elements_to_dict(data, | |
896 | position + 4, | |
897 | obj_end, | |
898 | codec_options)) | |
765 | 899 | position += obj_size |
766 | 900 | return docs |
767 | 901 | except InvalidBSON: |
0 | # Copyright 2009-2015 MongoDB, Inc. | |
0 | # Copyright 2009-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
79 | 79 | """The Java legacy UUID representation. |
80 | 80 | |
81 | 81 | :class:`uuid.UUID` instances will automatically be encoded to |
82 | and decoded from mockupdb._bson binary, using the Java driver's legacy | |
83 | byte order with binary subtype :data:`OLD_UUID_SUBTYPE`. | |
84 | ||
82 | and decoded from mockupdb._bson binary subtype :data:`OLD_UUID_SUBTYPE`, | |
83 | using the Java driver's legacy byte order. | |
84 | ||
85 | .. versionchanged:: 3.6 | |
86 | BSON binary subtype 4 is decoded using RFC-4122 byte order. | |
85 | 87 | .. versionadded:: 2.3 |
86 | 88 | """ |
87 | 89 | |
89 | 91 | """The C#/.net legacy UUID representation. |
90 | 92 | |
91 | 93 | :class:`uuid.UUID` instances will automatically be encoded to |
92 | and decoded from mockupdb._bson binary, using the C# driver's legacy | |
93 | byte order and binary subtype :data:`OLD_UUID_SUBTYPE`. | |
94 | ||
94 | and decoded from mockupdb._bson binary subtype :data:`OLD_UUID_SUBTYPE`, | |
95 | using the C# driver's legacy byte order. | |
96 | ||
97 | .. versionchanged:: 3.6 | |
98 | BSON binary subtype 4 is decoded using RFC-4122 byte order. | |
95 | 99 | .. versionadded:: 2.3 |
96 | 100 | """ |
97 | 101 |
0 | # Copyright 2009-2015 MongoDB, Inc. | |
0 | # Copyright 2009-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
13 | 13 | |
14 | 14 | """Tools for representing JavaScript code in BSON. |
15 | 15 | """ |
16 | import collections | |
17 | 16 | |
18 | from mockupdb._bson.py3compat import string_type | |
17 | from mockupdb._bson.py3compat import abc, string_type, PY3, text_type | |
19 | 18 | |
20 | 19 | |
21 | 20 | class Code(str): |
31 | 30 | the `scope` dictionary. |
32 | 31 | |
33 | 32 | :Parameters: |
34 | - `code`: string containing JavaScript code to be evaluated | |
33 | - `code`: A string containing JavaScript code to be evaluated or another | |
34 | instance of Code. In the latter case, the scope of `code` becomes this | |
35 | Code's :attr:`scope`. | |
35 | 36 | - `scope` (optional): dictionary representing the scope in which |
36 | 37 | `code` should be evaluated - a mapping from identifiers (as |
37 | strings) to values | |
38 | strings) to values. Defaults to ``None``. This is applied after any | |
39 | scope associated with a given `code` above. | |
38 | 40 | - `**kwargs` (optional): scope variables can also be passed as |
39 | keyword arguments | |
41 | keyword arguments. These are applied after `scope` and `code`. | |
42 | ||
43 | .. versionchanged:: 3.4 | |
44 | The default value for :attr:`scope` is ``None`` instead of ``{}``. | |
45 | ||
40 | 46 | """ |
41 | 47 | |
42 | 48 | _type_marker = 13 |
46 | 52 | raise TypeError("code must be an " |
47 | 53 | "instance of %s" % (string_type.__name__)) |
48 | 54 | |
49 | self = str.__new__(cls, code) | |
55 | if not PY3 and isinstance(code, text_type): | |
56 | self = str.__new__(cls, code.encode('utf8')) | |
57 | else: | |
58 | self = str.__new__(cls, code) | |
50 | 59 | |
51 | 60 | try: |
52 | 61 | self.__scope = code.scope |
53 | 62 | except AttributeError: |
54 | self.__scope = {} | |
63 | self.__scope = None | |
55 | 64 | |
56 | 65 | if scope is not None: |
57 | if not isinstance(scope, collections.Mapping): | |
66 | if not isinstance(scope, abc.Mapping): | |
58 | 67 | raise TypeError("scope must be an instance of dict") |
59 | self.__scope.update(scope) | |
68 | if self.__scope is not None: | |
69 | self.__scope.update(scope) | |
70 | else: | |
71 | self.__scope = scope | |
60 | 72 | |
61 | self.__scope.update(kwargs) | |
73 | if kwargs: | |
74 | if self.__scope is not None: | |
75 | self.__scope.update(kwargs) | |
76 | else: | |
77 | self.__scope = kwargs | |
62 | 78 | |
63 | 79 | return self |
64 | 80 | |
65 | 81 | @property |
66 | 82 | def scope(self): |
67 | """Scope dictionary for this instance. | |
83 | """Scope dictionary for this instance or ``None``. | |
68 | 84 | """ |
69 | 85 | return self.__scope |
70 | 86 |
0 | # Copyright 2014-2015 MongoDB, Inc. | |
0 | # Copyright 2014-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
15 | 15 | |
16 | 16 | import datetime |
17 | 17 | |
18 | from collections import MutableMapping, namedtuple | |
18 | from collections import namedtuple | |
19 | 19 | |
20 | from mockupdb._bson.py3compat import string_type | |
20 | from mockupdb._bson.py3compat import abc, string_type | |
21 | 21 | from mockupdb._bson.binary import (ALL_UUID_REPRESENTATIONS, |
22 | PYTHON_LEGACY, | |
23 | UUID_REPRESENTATION_NAMES) | |
22 | PYTHON_LEGACY, | |
23 | UUID_REPRESENTATION_NAMES) | |
24 | ||
25 | _RAW_BSON_DOCUMENT_MARKER = 101 | |
26 | ||
27 | ||
28 | def _raw_document_class(document_class): | |
29 | """Determine if a document_class is a RawBSONDocument class.""" | |
30 | marker = getattr(document_class, '_type_marker', None) | |
31 | return marker == _RAW_BSON_DOCUMENT_MARKER | |
24 | 32 | |
25 | 33 | |
26 | 34 | _options_base = namedtuple( |
43 | 51 | and decoding instances of :class:`~uuid.UUID`. Defaults to |
44 | 52 | :data:`~bson.binary.PYTHON_LEGACY`. |
45 | 53 | - `unicode_decode_error_handler`: The error handler to use when decoding |
46 | an invalid BSON string. Valid options include 'strict', 'replace', and | |
47 | 'ignore'. Defaults to 'strict'. | |
54 | an invalid BSON string. Valid options include 'strict', 'replace', and | |
55 | 'ignore'. Defaults to 'strict'. | |
56 | - `tzinfo`: A :class:`~datetime.tzinfo` subclass that specifies the | |
57 | timezone to/from which :class:`~datetime.datetime` objects should be | |
58 | encoded/decoded. | |
48 | 59 | |
49 | 60 | .. warning:: Care must be taken when changing |
50 | 61 | `unicode_decode_error_handler` from its default value ('strict'). |
51 | 62 | The 'replace' and 'ignore' modes should not be used when documents |
52 | 63 | retrieved from the server will be modified in the client application |
53 | 64 | and stored back to the server. |
54 | ||
55 | - `tzinfo`: A :class:`~datetime.tzinfo` subclass that specifies the | |
56 | timezone to/from which :class:`~datetime.datetime` objects should be | |
57 | encoded/decoded. | |
58 | ||
59 | 65 | """ |
60 | 66 | |
61 | 67 | def __new__(cls, document_class=dict, |
62 | 68 | tz_aware=False, uuid_representation=PYTHON_LEGACY, |
63 | 69 | unicode_decode_error_handler="strict", |
64 | 70 | tzinfo=None): |
65 | if not issubclass(document_class, MutableMapping): | |
66 | raise TypeError("document_class must be dict, bson.son.SON, or " | |
67 | "another subclass of collections.MutableMapping") | |
71 | if not (issubclass(document_class, abc.MutableMapping) or | |
72 | _raw_document_class(document_class)): | |
73 | raise TypeError("document_class must be dict, bson.son.SON, " | |
74 | "bson.raw_bson.RawBSONDocument, or a " | |
75 | "sublass of collections.MutableMapping") | |
68 | 76 | if not isinstance(tz_aware, bool): |
69 | 77 | raise TypeError("tz_aware must be True or False") |
70 | 78 | if uuid_representation not in ALL_UUID_REPRESENTATIONS: |
85 | 93 | cls, (document_class, tz_aware, uuid_representation, |
86 | 94 | unicode_decode_error_handler, tzinfo)) |
87 | 95 | |
88 | def __repr__(self): | |
96 | def _arguments_repr(self): | |
97 | """Representation of the arguments used to create this object.""" | |
89 | 98 | document_class_repr = ( |
90 | 99 | 'dict' if self.document_class is dict |
91 | 100 | else repr(self.document_class)) |
93 | 102 | uuid_rep_repr = UUID_REPRESENTATION_NAMES.get(self.uuid_representation, |
94 | 103 | self.uuid_representation) |
95 | 104 | |
96 | return ( | |
97 | 'CodecOptions(document_class=%s, tz_aware=%r, uuid_representation=' | |
98 | '%s, unicode_decode_error_handler=%r, tzinfo=%r)' % | |
99 | (document_class_repr, self.tz_aware, uuid_rep_repr, | |
100 | self.unicode_decode_error_handler, | |
101 | self.tzinfo)) | |
105 | return ('document_class=%s, tz_aware=%r, uuid_representation=' | |
106 | '%s, unicode_decode_error_handler=%r, tzinfo=%r' % | |
107 | (document_class_repr, self.tz_aware, uuid_rep_repr, | |
108 | self.unicode_decode_error_handler, self.tzinfo)) | |
109 | ||
110 | def __repr__(self): | |
111 | return '%s(%s)' % (self.__class__.__name__, self._arguments_repr()) | |
112 | ||
113 | def with_options(self, **kwargs): | |
114 | """Make a copy of this CodecOptions, overriding some options:: | |
115 | ||
116 | >>> from mockupdb._bson.codec_options import DEFAULT_CODEC_OPTIONS | |
117 | >>> DEFAULT_CODEC_OPTIONS.tz_aware | |
118 | False | |
119 | >>> options = DEFAULT_CODEC_OPTIONS.with_options(tz_aware=True) | |
120 | >>> options.tz_aware | |
121 | True | |
122 | ||
123 | .. versionadded:: 3.5 | |
124 | """ | |
125 | return CodecOptions( | |
126 | kwargs.get('document_class', self.document_class), | |
127 | kwargs.get('tz_aware', self.tz_aware), | |
128 | kwargs.get('uuid_representation', self.uuid_representation), | |
129 | kwargs.get('unicode_decode_error_handler', | |
130 | self.unicode_decode_error_handler), | |
131 | kwargs.get('tzinfo', self.tzinfo)) | |
102 | 132 | |
103 | 133 | |
104 | 134 | DEFAULT_CODEC_OPTIONS = CodecOptions() |
0 | # Copyright 2016-present MongoDB, Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | """Tools for working with the BSON decimal128 type. | |
15 | ||
16 | .. versionadded:: 3.4 | |
17 | ||
18 | .. note:: The Decimal128 BSON type requires MongoDB 3.4+. | |
19 | """ | |
20 | ||
21 | import decimal | |
22 | import struct | |
23 | import sys | |
24 | ||
25 | from mockupdb._bson.py3compat import (PY3 as _PY3, | |
26 | string_type as _string_type) | |
27 | ||
28 | ||
29 | if _PY3: | |
30 | _from_bytes = int.from_bytes # pylint: disable=no-member, invalid-name | |
31 | else: | |
32 | import binascii | |
33 | def _from_bytes(value, dummy, _int=int, _hexlify=binascii.hexlify): | |
34 | "An implementation of int.from_bytes for python 2.x." | |
35 | return _int(_hexlify(value), 16) | |
36 | ||
37 | if sys.version_info[:2] == (2, 6): | |
38 | def _bit_length(num): | |
39 | """bit_length for python 2.6""" | |
40 | if num: | |
41 | # bin() was new in 2.6. Note that this won't work | |
42 | # for values less than 0, which we never have here. | |
43 | return len(bin(num)) - 2 | |
44 | # bit_length(0) is 0, but len(bin(0)) - 2 is 1 | |
45 | return 0 | |
46 | else: | |
47 | def _bit_length(num): | |
48 | """bit_length for python >= 2.7""" | |
49 | # num could be int or long in python 2.7 | |
50 | return num.bit_length() | |
51 | ||
52 | ||
53 | _PACK_64 = struct.Struct("<Q").pack | |
54 | _UNPACK_64 = struct.Struct("<Q").unpack | |
55 | ||
56 | _EXPONENT_MASK = 3 << 61 | |
57 | _EXPONENT_BIAS = 6176 | |
58 | _EXPONENT_MAX = 6144 | |
59 | _EXPONENT_MIN = -6143 | |
60 | _MAX_DIGITS = 34 | |
61 | ||
62 | _INF = 0x7800000000000000 | |
63 | _NAN = 0x7c00000000000000 | |
64 | _SNAN = 0x7e00000000000000 | |
65 | _SIGN = 0x8000000000000000 | |
66 | ||
67 | _NINF = (_INF + _SIGN, 0) | |
68 | _PINF = (_INF, 0) | |
69 | _NNAN = (_NAN + _SIGN, 0) | |
70 | _PNAN = (_NAN, 0) | |
71 | _NSNAN = (_SNAN + _SIGN, 0) | |
72 | _PSNAN = (_SNAN, 0) | |
73 | ||
74 | _CTX_OPTIONS = { | |
75 | 'prec': _MAX_DIGITS, | |
76 | 'rounding': decimal.ROUND_HALF_EVEN, | |
77 | 'Emin': _EXPONENT_MIN, | |
78 | 'Emax': _EXPONENT_MAX, | |
79 | 'capitals': 1, | |
80 | 'flags': [], | |
81 | 'traps': [decimal.InvalidOperation, | |
82 | decimal.Overflow, | |
83 | decimal.Inexact] | |
84 | } | |
85 | ||
86 | try: | |
87 | # Python >= 3.3, cdecimal | |
88 | decimal.Context(clamp=1) # pylint: disable=unexpected-keyword-arg | |
89 | _CTX_OPTIONS['clamp'] = 1 | |
90 | except TypeError: | |
91 | # Python < 3.3 | |
92 | _CTX_OPTIONS['_clamp'] = 1 | |
93 | ||
94 | _DEC128_CTX = decimal.Context(**_CTX_OPTIONS.copy()) | |
95 | ||
96 | ||
97 | def create_decimal128_context(): | |
98 | """Returns an instance of :class:`decimal.Context` appropriate | |
99 | for working with IEEE-754 128-bit decimal floating point values. | |
100 | """ | |
101 | opts = _CTX_OPTIONS.copy() | |
102 | opts['traps'] = [] | |
103 | return decimal.Context(**opts) | |
104 | ||
105 | ||
106 | def _decimal_to_128(value): | |
107 | """Converts a decimal.Decimal to BID (high bits, low bits). | |
108 | ||
109 | :Parameters: | |
110 | - `value`: An instance of decimal.Decimal | |
111 | """ | |
112 | with decimal.localcontext(_DEC128_CTX) as ctx: | |
113 | value = ctx.create_decimal(value) | |
114 | ||
115 | if value.is_infinite(): | |
116 | return _NINF if value.is_signed() else _PINF | |
117 | ||
118 | sign, digits, exponent = value.as_tuple() | |
119 | ||
120 | if value.is_nan(): | |
121 | if digits: | |
122 | raise ValueError("NaN with debug payload is not supported") | |
123 | if value.is_snan(): | |
124 | return _NSNAN if value.is_signed() else _PSNAN | |
125 | return _NNAN if value.is_signed() else _PNAN | |
126 | ||
127 | significand = int("".join([str(digit) for digit in digits])) | |
128 | bit_length = _bit_length(significand) | |
129 | ||
130 | high = 0 | |
131 | low = 0 | |
132 | for i in range(min(64, bit_length)): | |
133 | if significand & (1 << i): | |
134 | low |= 1 << i | |
135 | ||
136 | for i in range(64, bit_length): | |
137 | if significand & (1 << i): | |
138 | high |= 1 << (i - 64) | |
139 | ||
140 | biased_exponent = exponent + _EXPONENT_BIAS | |
141 | ||
142 | if high >> 49 == 1: | |
143 | high = high & 0x7fffffffffff | |
144 | high |= _EXPONENT_MASK | |
145 | high |= (biased_exponent & 0x3fff) << 47 | |
146 | else: | |
147 | high |= biased_exponent << 49 | |
148 | ||
149 | if sign: | |
150 | high |= _SIGN | |
151 | ||
152 | return high, low | |
153 | ||
154 | ||
155 | class Decimal128(object): | |
156 | """BSON Decimal128 type:: | |
157 | ||
158 | >>> Decimal128(Decimal("0.0005")) | |
159 | Decimal128('0.0005') | |
160 | >>> Decimal128("0.0005") | |
161 | Decimal128('0.0005') | |
162 | >>> Decimal128((3474527112516337664, 5)) | |
163 | Decimal128('0.0005') | |
164 | ||
165 | :Parameters: | |
166 | - `value`: An instance of :class:`decimal.Decimal`, string, or tuple of | |
167 | (high bits, low bits) from Binary Integer Decimal (BID) format. | |
168 | ||
169 | .. note:: :class:`~Decimal128` uses an instance of :class:`decimal.Context` | |
170 | configured for IEEE-754 Decimal128 when validating parameters. | |
171 | Signals like :class:`decimal.InvalidOperation`, :class:`decimal.Inexact`, | |
172 | and :class:`decimal.Overflow` are trapped and raised as exceptions:: | |
173 | ||
174 | >>> Decimal128(".13.1") | |
175 | Traceback (most recent call last): | |
176 | File "<stdin>", line 1, in <module> | |
177 | ... | |
178 | decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>] | |
179 | >>> | |
180 | >>> Decimal128("1E-6177") | |
181 | Traceback (most recent call last): | |
182 | File "<stdin>", line 1, in <module> | |
183 | ... | |
184 | decimal.Inexact: [<class 'decimal.Inexact'>] | |
185 | >>> | |
186 | >>> Decimal128("1E6145") | |
187 | Traceback (most recent call last): | |
188 | File "<stdin>", line 1, in <module> | |
189 | ... | |
190 | decimal.Overflow: [<class 'decimal.Overflow'>, <class 'decimal.Rounded'>] | |
191 | ||
192 | To ensure the result of a calculation can always be stored as BSON | |
193 | Decimal128 use the context returned by | |
194 | :func:`create_decimal128_context`:: | |
195 | ||
196 | >>> import decimal | |
197 | >>> decimal128_ctx = create_decimal128_context() | |
198 | >>> with decimal.localcontext(decimal128_ctx) as ctx: | |
199 | ... Decimal128(ctx.create_decimal(".13.3")) | |
200 | ... | |
201 | Decimal128('NaN') | |
202 | >>> | |
203 | >>> with decimal.localcontext(decimal128_ctx) as ctx: | |
204 | ... Decimal128(ctx.create_decimal("1E-6177")) | |
205 | ... | |
206 | Decimal128('0E-6176') | |
207 | >>> | |
208 | >>> with decimal.localcontext(DECIMAL128_CTX) as ctx: | |
209 | ... Decimal128(ctx.create_decimal("1E6145")) | |
210 | ... | |
211 | Decimal128('Infinity') | |
212 | ||
213 | To match the behavior of MongoDB's Decimal128 implementation | |
214 | str(Decimal(value)) may not match str(Decimal128(value)) for NaN values:: | |
215 | ||
216 | >>> Decimal128(Decimal('NaN')) | |
217 | Decimal128('NaN') | |
218 | >>> Decimal128(Decimal('-NaN')) | |
219 | Decimal128('NaN') | |
220 | >>> Decimal128(Decimal('sNaN')) | |
221 | Decimal128('NaN') | |
222 | >>> Decimal128(Decimal('-sNaN')) | |
223 | Decimal128('NaN') | |
224 | ||
225 | However, :meth:`~Decimal128.to_decimal` will return the exact value:: | |
226 | ||
227 | >>> Decimal128(Decimal('NaN')).to_decimal() | |
228 | Decimal('NaN') | |
229 | >>> Decimal128(Decimal('-NaN')).to_decimal() | |
230 | Decimal('-NaN') | |
231 | >>> Decimal128(Decimal('sNaN')).to_decimal() | |
232 | Decimal('sNaN') | |
233 | >>> Decimal128(Decimal('-sNaN')).to_decimal() | |
234 | Decimal('-sNaN') | |
235 | ||
236 | Two instances of :class:`Decimal128` compare equal if their Binary | |
237 | Integer Decimal encodings are equal:: | |
238 | ||
239 | >>> Decimal128('NaN') == Decimal128('NaN') | |
240 | True | |
241 | >>> Decimal128('NaN').bid == Decimal128('NaN').bid | |
242 | True | |
243 | ||
244 | This differs from :class:`decimal.Decimal` comparisons for NaN:: | |
245 | ||
246 | >>> Decimal('NaN') == Decimal('NaN') | |
247 | False | |
248 | """ | |
249 | __slots__ = ('__high', '__low') | |
250 | ||
251 | _type_marker = 19 | |
252 | ||
253 | def __init__(self, value): | |
254 | if isinstance(value, (_string_type, decimal.Decimal)): | |
255 | self.__high, self.__low = _decimal_to_128(value) | |
256 | elif isinstance(value, (list, tuple)): | |
257 | if len(value) != 2: | |
258 | raise ValueError('Invalid size for creation of Decimal128 ' | |
259 | 'from list or tuple. Must have exactly 2 ' | |
260 | 'elements.') | |
261 | self.__high, self.__low = value | |
262 | else: | |
263 | raise TypeError("Cannot convert %r to Decimal128" % (value,)) | |
264 | ||
265 | def to_decimal(self): | |
266 | """Returns an instance of :class:`decimal.Decimal` for this | |
267 | :class:`Decimal128`. | |
268 | """ | |
269 | high = self.__high | |
270 | low = self.__low | |
271 | sign = 1 if (high & _SIGN) else 0 | |
272 | ||
273 | if (high & _SNAN) == _SNAN: | |
274 | return decimal.Decimal((sign, (), 'N')) | |
275 | elif (high & _NAN) == _NAN: | |
276 | return decimal.Decimal((sign, (), 'n')) | |
277 | elif (high & _INF) == _INF: | |
278 | return decimal.Decimal((sign, (), 'F')) | |
279 | ||
280 | if (high & _EXPONENT_MASK) == _EXPONENT_MASK: | |
281 | exponent = ((high & 0x1fffe00000000000) >> 47) - _EXPONENT_BIAS | |
282 | return decimal.Decimal((sign, (0,), exponent)) | |
283 | else: | |
284 | exponent = ((high & 0x7fff800000000000) >> 49) - _EXPONENT_BIAS | |
285 | ||
286 | arr = bytearray(15) | |
287 | mask = 0x00000000000000ff | |
288 | for i in range(14, 6, -1): | |
289 | arr[i] = (low & mask) >> ((14 - i) << 3) | |
290 | mask = mask << 8 | |
291 | ||
292 | mask = 0x00000000000000ff | |
293 | for i in range(6, 0, -1): | |
294 | arr[i] = (high & mask) >> ((6 - i) << 3) | |
295 | mask = mask << 8 | |
296 | ||
297 | mask = 0x0001000000000000 | |
298 | arr[0] = (high & mask) >> 48 | |
299 | ||
300 | # Have to convert bytearray to bytes for python 2.6. | |
301 | # cdecimal only accepts a tuple for digits. | |
302 | digits = tuple( | |
303 | int(digit) for digit in str(_from_bytes(bytes(arr), 'big'))) | |
304 | ||
305 | with decimal.localcontext(_DEC128_CTX) as ctx: | |
306 | return ctx.create_decimal((sign, digits, exponent)) | |
307 | ||
308 | @classmethod | |
309 | def from_bid(cls, value): | |
310 | """Create an instance of :class:`Decimal128` from Binary Integer | |
311 | Decimal string. | |
312 | ||
313 | :Parameters: | |
314 | - `value`: 16 byte string (128-bit IEEE 754-2008 decimal floating | |
315 | point in Binary Integer Decimal (BID) format). | |
316 | """ | |
317 | if not isinstance(value, bytes): | |
318 | raise TypeError("value must be an instance of bytes") | |
319 | if len(value) != 16: | |
320 | raise ValueError("value must be exactly 16 bytes") | |
321 | return cls((_UNPACK_64(value[8:])[0], _UNPACK_64(value[:8])[0])) | |
322 | ||
323 | @property | |
324 | def bid(self): | |
325 | """The Binary Integer Decimal (BID) encoding of this instance.""" | |
326 | return _PACK_64(self.__low) + _PACK_64(self.__high) | |
327 | ||
328 | def __str__(self): | |
329 | dec = self.to_decimal() | |
330 | if dec.is_nan(): | |
331 | # Required by the drivers spec to match MongoDB behavior. | |
332 | return "NaN" | |
333 | return str(dec) | |
334 | ||
335 | def __repr__(self): | |
336 | return "Decimal128('%s')" % (str(self),) | |
337 | ||
338 | def __setstate__(self, value): | |
339 | self.__high, self.__low = value | |
340 | ||
341 | def __getstate__(self): | |
342 | return self.__high, self.__low | |
343 | ||
344 | def __eq__(self, other): | |
345 | if isinstance(other, Decimal128): | |
346 | return self.bid == other.bid | |
347 | return NotImplemented | |
348 | ||
349 | def __ne__(self, other): | |
350 | return not self == other |
0 | # Copyright 2009-2015 MongoDB, Inc. | |
0 | # Copyright 2009-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
0 | # Copyright 2009-2015 MongoDB, Inc. | |
0 | # Copyright 2009-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
15 | 15 | |
16 | 16 | This module provides two helper methods `dumps` and `loads` that wrap the |
17 | 17 | native :mod:`json` methods and provide explicit BSON conversion to and from |
18 | json. This allows for specialized encoding and decoding of BSON documents | |
19 | into `Mongo Extended JSON | |
20 | <http://www.mongodb.org/display/DOCS/Mongo+Extended+JSON>`_'s *Strict* | |
21 | mode. This lets you encode / decode BSON documents to JSON even when | |
22 | they use special BSON types. | |
18 | JSON. :class:`~bson.json_util.JSONOptions` provides a way to control how JSON | |
19 | is emitted and parsed, with the default being the legacy PyMongo format. | |
20 | :mod:`~bson.json_util` can also generate Canonical or Relaxed `Extended JSON`_ | |
21 | when :const:`CANONICAL_JSON_OPTIONS` or :const:`RELAXED_JSON_OPTIONS` is | |
22 | provided, respectively. | |
23 | ||
24 | .. _Extended JSON: https://github.com/mongodb/specifications/blob/master/source/extended-json.rst | |
25 | ||
26 | Example usage (deserialization): | |
27 | ||
28 | .. doctest:: | |
29 | ||
30 | >>> from mockupdb._bson.json_util import loads | |
31 | >>> loads('[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": "80", "$binary": "AQIDBA=="}}]') | |
32 | [{u'foo': [1, 2]}, {u'bar': {u'hello': u'world'}}, {u'code': Code('function x() { return 1; }', {})}, {u'bin': Binary('...', 128)}] | |
23 | 33 | |
24 | 34 | Example usage (serialization): |
25 | 35 | |
29 | 39 | >>> from mockupdb._bson.json_util import dumps |
30 | 40 | >>> dumps([{'foo': [1, 2]}, |
31 | 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'}}, | |
32 | 54 | ... {'code': Code("function x() { return 1; }")}, |
33 | ... {'bin': Binary("\x01\x02\x03\x04")}]) | |
34 | '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }", "$scope": {}}}, {"bin": {"$binary": "AQIDBA==", "$type": "00"}}]' | |
35 | ||
36 | Example usage (deserialization): | |
55 | ... {'bin': Binary(b"\x01\x02\x03\x04")}], | |
56 | ... json_options=CANONICAL_JSON_OPTIONS) | |
57 | '[{"foo": [{"$numberInt": "1"}, {"$numberInt": "2"}]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]' | |
58 | ||
59 | Example usage (with :const:`RELAXED_JSON_OPTIONS`): | |
37 | 60 | |
38 | 61 | .. doctest:: |
39 | 62 | |
40 | >>> from mockupdb._bson.json_util import loads | |
41 | >>> loads('[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$scope": {}, "$code": "function x() { return 1; }"}}, {"bin": {"$type": "00", "$binary": "AQIDBA=="}}]') | |
42 | [{u'foo': [1, 2]}, {u'bar': {u'hello': u'world'}}, {u'code': Code('function x() { return 1; }', {})}, {u'bin': Binary('...', 0)}] | |
63 | >>> from mockupdb._bson import Binary, Code | |
64 | >>> from mockupdb._bson.json_util import dumps, RELAXED_JSON_OPTIONS | |
65 | >>> dumps([{'foo': [1, 2]}, | |
66 | ... {'bar': {'hello': 'world'}}, | |
67 | ... {'code': Code("function x() { return 1; }")}, | |
68 | ... {'bin': Binary(b"\x01\x02\x03\x04")}], | |
69 | ... json_options=RELAXED_JSON_OPTIONS) | |
70 | '[{"foo": [1, 2]}, {"bar": {"hello": "world"}}, {"code": {"$code": "function x() { return 1; }"}}, {"bin": {"$binary": {"base64": "AQIDBA==", "subType": "00"}}}]' | |
43 | 71 | |
44 | 72 | Alternatively, you can manually pass the `default` to :func:`json.dumps`. |
45 | 73 | It won't handle :class:`~bson.binary.Binary` and :class:`~bson.code.Code` |
46 | 74 | instances (as they are extended strings you can't provide custom defaults), |
47 | 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`. | |
48 | 86 | |
49 | 87 | .. versionchanged:: 2.8 |
50 | 88 | The output format for :class:`~bson.timestamp.Timestamp` has changed from |
67 | 105 | """ |
68 | 106 | |
69 | 107 | import base64 |
70 | import calendar | |
71 | import collections | |
72 | 108 | import datetime |
73 | import json | |
109 | import math | |
74 | 110 | import re |
111 | import sys | |
75 | 112 | import uuid |
76 | 113 | |
77 | from mockupdb._bson import EPOCH_AWARE, RE_TYPE, SON | |
78 | from mockupdb._bson.binary import Binary | |
114 | if sys.version_info[:2] == (2, 6): | |
115 | # In Python 2.6, json does not include object_pairs_hook. Use simplejson | |
116 | # instead. | |
117 | try: | |
118 | import simplejson as json | |
119 | except ImportError: | |
120 | import json | |
121 | else: | |
122 | import json | |
123 | ||
124 | class ConfigurationError(Exception): | |
125 | pass | |
126 | ||
127 | import mockupdb._bson as bson | |
128 | from mockupdb._bson import EPOCH_AWARE, EPOCH_NAIVE, RE_TYPE, SON | |
129 | from mockupdb._bson.binary import (Binary, JAVA_LEGACY, CSHARP_LEGACY, | |
130 | OLD_UUID_SUBTYPE, | |
131 | UUID_SUBTYPE) | |
79 | 132 | from mockupdb._bson.code import Code |
133 | from mockupdb._bson.codec_options import CodecOptions | |
80 | 134 | from mockupdb._bson.dbref import DBRef |
135 | from mockupdb._bson.decimal128 import Decimal128 | |
81 | 136 | from mockupdb._bson.int64 import Int64 |
82 | 137 | from mockupdb._bson.max_key import MaxKey |
83 | 138 | from mockupdb._bson.min_key import MinKey |
84 | 139 | from mockupdb._bson.objectid import ObjectId |
140 | from mockupdb._bson.py3compat import (PY3, iteritems, integer_types, | |
141 | string_type, | |
142 | text_type) | |
85 | 143 | from mockupdb._bson.regex import Regex |
86 | 144 | from mockupdb._bson.timestamp import Timestamp |
87 | 145 | from mockupdb._bson.tz_util import utc |
88 | 146 | |
89 | from mockupdb._bson.py3compat import PY3, iteritems, string_type, text_type | |
147 | ||
148 | try: | |
149 | json.loads("{}", object_pairs_hook=dict) | |
150 | _HAS_OBJECT_PAIRS_HOOK = True | |
151 | except TypeError: | |
152 | _HAS_OBJECT_PAIRS_HOOK = False | |
90 | 153 | |
91 | 154 | |
92 | 155 | _RE_OPT_TABLE = { |
98 | 161 | "x": re.X, |
99 | 162 | } |
100 | 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 | ||
101 | 390 | |
102 | 391 | def dumps(obj, *args, **kwargs): |
103 | """Helper function that wraps :class:`json.dumps`. | |
392 | """Helper function that wraps :func:`json.dumps`. | |
104 | 393 | |
105 | 394 | Recursive function that handles all BSON types including |
106 | 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`. | |
107 | 404 | |
108 | 405 | .. versionchanged:: 2.7 |
109 | 406 | Preserves order when rendering SON, Timestamp, Code, Binary, and DBRef |
110 | 407 | instances. |
111 | 408 | """ |
112 | return json.dumps(_json_convert(obj), *args, **kwargs) | |
409 | json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS) | |
410 | return json.dumps(_json_convert(obj, json_options), *args, **kwargs) | |
113 | 411 | |
114 | 412 | |
115 | 413 | def loads(s, *args, **kwargs): |
116 | """Helper function that wraps :class:`json.loads`. | |
414 | """Helper function that wraps :func:`json.loads`. | |
117 | 415 | |
118 | 416 | Automatically passes the object_hook for BSON type conversion. |
119 | """ | |
120 | kwargs['object_hook'] = lambda dct: object_hook(dct) | |
417 | ||
418 | Raises ``TypeError``, ``ValueError``, ``KeyError``, or | |
419 | :exc:`~bson.errors.InvalidId` on invalid MongoDB Extended JSON. | |
420 | ||
421 | :Parameters: | |
422 | - `json_options`: A :class:`JSONOptions` instance used to modify the | |
423 | decoding of MongoDB Extended JSON types. Defaults to | |
424 | :const:`DEFAULT_JSON_OPTIONS`. | |
425 | ||
426 | .. versionchanged:: 3.5 | |
427 | Parses Relaxed and Canonical Extended JSON as well as PyMongo's legacy | |
428 | format. Now raises ``TypeError`` or ``ValueError`` when parsing JSON | |
429 | type wrappers with values of the wrong type or any extra keys. | |
430 | ||
431 | .. versionchanged:: 3.4 | |
432 | Accepts optional parameter `json_options`. See :class:`JSONOptions`. | |
433 | """ | |
434 | json_options = kwargs.pop("json_options", DEFAULT_JSON_OPTIONS) | |
435 | if _HAS_OBJECT_PAIRS_HOOK: | |
436 | kwargs["object_pairs_hook"] = lambda pairs: object_pairs_hook( | |
437 | pairs, json_options) | |
438 | else: | |
439 | kwargs["object_hook"] = lambda obj: object_hook(obj, json_options) | |
121 | 440 | return json.loads(s, *args, **kwargs) |
122 | 441 | |
123 | 442 | |
124 | def _json_convert(obj): | |
443 | def _json_convert(obj, json_options=DEFAULT_JSON_OPTIONS): | |
125 | 444 | """Recursive helper method that converts BSON types so they can be |
126 | 445 | converted into json. |
127 | 446 | """ |
128 | 447 | if hasattr(obj, 'iteritems') or hasattr(obj, 'items'): # PY3 support |
129 | return SON(((k, _json_convert(v)) for k, v in iteritems(obj))) | |
448 | return SON(((k, _json_convert(v, json_options)) | |
449 | for k, v in iteritems(obj))) | |
130 | 450 | elif hasattr(obj, '__iter__') and not isinstance(obj, (text_type, bytes)): |
131 | return list((_json_convert(v) for v in obj)) | |
451 | return list((_json_convert(v, json_options) for v in obj)) | |
132 | 452 | try: |
133 | return default(obj) | |
453 | return default(obj, json_options) | |
134 | 454 | except TypeError: |
135 | 455 | return obj |
136 | 456 | |
137 | 457 | |
138 | def object_hook(dct): | |
458 | def object_pairs_hook(pairs, json_options=DEFAULT_JSON_OPTIONS): | |
459 | return object_hook(json_options.document_class(pairs), json_options) | |
460 | ||
461 | ||
462 | def object_hook(dct, json_options=DEFAULT_JSON_OPTIONS): | |
139 | 463 | if "$oid" in dct: |
140 | return ObjectId(str(dct["$oid"])) | |
464 | return _parse_canonical_oid(dct) | |
141 | 465 | if "$ref" in dct: |
142 | return DBRef(dct["$ref"], dct["$id"], dct.get("$db", None)) | |
466 | return _parse_canonical_dbref(dct) | |
143 | 467 | if "$date" in dct: |
144 | dtm = dct["$date"] | |
145 | # mongoexport 2.6 and newer | |
146 | if isinstance(dtm, string_type): | |
147 | aware = datetime.datetime.strptime( | |
148 | dtm[:23], "%Y-%m-%dT%H:%M:%S.%f").replace(tzinfo=utc) | |
149 | offset = dtm[23:] | |
150 | if not offset or offset == 'Z': | |
151 | # UTC | |
152 | return aware | |
153 | else: | |
154 | if len(offset) == 5: | |
155 | # Offset from mongoexport is in format (+|-)HHMM | |
156 | secs = (int(offset[1:3]) * 3600 + int(offset[3:]) * 60) | |
157 | elif ':' in offset and len(offset) == 6: | |
158 | # RFC-3339 format (+|-)HH:MM | |
159 | hours, minutes = offset[1:].split(':') | |
160 | secs = (int(hours) * 3600 + int(minutes) * 60) | |
161 | else: | |
162 | # Not RFC-3339 compliant or mongoexport output. | |
163 | raise ValueError("invalid format for offset") | |
164 | if offset[0] == "-": | |
165 | secs *= -1 | |
166 | return aware - datetime.timedelta(seconds=secs) | |
167 | # mongoexport 2.6 and newer, time before the epoch (SERVER-15275) | |
168 | elif isinstance(dtm, collections.Mapping): | |
169 | secs = float(dtm["$numberLong"]) / 1000.0 | |
170 | # mongoexport before 2.6 | |
468 | return _parse_canonical_datetime(dct, json_options) | |
469 | if "$regex" in dct: | |
470 | return _parse_legacy_regex(dct) | |
471 | if "$minKey" in dct: | |
472 | return _parse_canonical_minkey(dct) | |
473 | if "$maxKey" in dct: | |
474 | return _parse_canonical_maxkey(dct) | |
475 | if "$binary" in dct: | |
476 | if "$type" in dct: | |
477 | return _parse_legacy_binary(dct, json_options) | |
171 | 478 | else: |
172 | secs = float(dtm) / 1000.0 | |
173 | return EPOCH_AWARE + datetime.timedelta(seconds=secs) | |
174 | if "$regex" in dct: | |
175 | flags = 0 | |
176 | # PyMongo always adds $options but some other tools may not. | |
177 | for opt in dct.get("$options", ""): | |
178 | flags |= _RE_OPT_TABLE.get(opt, 0) | |
179 | return Regex(dct["$regex"], flags) | |
180 | if "$minKey" in dct: | |
181 | return MinKey() | |
182 | if "$maxKey" in dct: | |
183 | return MaxKey() | |
184 | if "$binary" in dct: | |
185 | if isinstance(dct["$type"], int): | |
186 | dct["$type"] = "%02x" % dct["$type"] | |
187 | subtype = int(dct["$type"], 16) | |
188 | if subtype >= 0xffffff80: # Handle mongoexport values | |
189 | subtype = int(dct["$type"][6:], 16) | |
190 | return Binary(base64.b64decode(dct["$binary"].encode()), subtype) | |
479 | return _parse_canonical_binary(dct, json_options) | |
191 | 480 | if "$code" in dct: |
192 | return Code(dct["$code"], dct.get("$scope")) | |
481 | return _parse_canonical_code(dct) | |
193 | 482 | if "$uuid" in dct: |
194 | return uuid.UUID(dct["$uuid"]) | |
483 | return _parse_legacy_uuid(dct) | |
195 | 484 | if "$undefined" in dct: |
196 | 485 | return None |
197 | 486 | if "$numberLong" in dct: |
198 | return Int64(dct["$numberLong"]) | |
487 | return _parse_canonical_int64(dct) | |
199 | 488 | if "$timestamp" in dct: |
200 | 489 | tsp = dct["$timestamp"] |
201 | 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) | |
202 | 503 | return dct |
203 | 504 | |
204 | 505 | |
205 | def default(obj): | |
506 | def _parse_legacy_regex(doc): | |
507 | pattern = doc["$regex"] | |
508 | # Check if this is the $regex query operator. | |
509 | if isinstance(pattern, Regex): | |
510 | return doc | |
511 | flags = 0 | |
512 | # PyMongo always adds $options but some other tools may not. | |
513 | for opt in doc.get("$options", ""): | |
514 | flags |= _RE_OPT_TABLE.get(opt, 0) | |
515 | return Regex(pattern, flags) | |
516 | ||
517 | ||
518 | def _parse_legacy_uuid(doc): | |
519 | """Decode a JSON legacy $uuid to Python UUID.""" | |
520 | if len(doc) != 1: | |
521 | raise TypeError('Bad $uuid, extra field(s): %s' % (doc,)) | |
522 | return uuid.UUID(doc["$uuid"]) | |
523 | ||
524 | ||
525 | def _binary_or_uuid(data, subtype, json_options): | |
526 | # special handling for UUID | |
527 | if subtype == OLD_UUID_SUBTYPE: | |
528 | if json_options.uuid_representation == CSHARP_LEGACY: | |
529 | return uuid.UUID(bytes_le=data) | |
530 | if json_options.uuid_representation == JAVA_LEGACY: | |
531 | data = data[7::-1] + data[:7:-1] | |
532 | return uuid.UUID(bytes=data) | |
533 | if subtype == UUID_SUBTYPE: | |
534 | return uuid.UUID(bytes=data) | |
535 | if PY3 and subtype == 0: | |
536 | return data | |
537 | return Binary(data, subtype) | |
538 | ||
539 | ||
540 | def _parse_legacy_binary(doc, json_options): | |
541 | if isinstance(doc["$type"], int): | |
542 | doc["$type"] = "%02x" % doc["$type"] | |
543 | subtype = int(doc["$type"], 16) | |
544 | if subtype >= 0xffffff80: # Handle mongoexport values | |
545 | subtype = int(doc["$type"][6:], 16) | |
546 | data = base64.b64decode(doc["$binary"].encode()) | |
547 | return _binary_or_uuid(data, subtype, json_options) | |
548 | ||
549 | ||
550 | def _parse_canonical_binary(doc, json_options): | |
551 | binary = doc["$binary"] | |
552 | b64 = binary["base64"] | |
553 | subtype = binary["subType"] | |
554 | if not isinstance(b64, string_type): | |
555 | raise TypeError('$binary base64 must be a string: %s' % (doc,)) | |
556 | if not isinstance(subtype, string_type) or len(subtype) > 2: | |
557 | raise TypeError('$binary subType must be a string at most 2 ' | |
558 | 'characters: %s' % (doc,)) | |
559 | if len(binary) != 2: | |
560 | raise TypeError('$binary must include only "base64" and "subType" ' | |
561 | 'components: %s' % (doc,)) | |
562 | ||
563 | data = base64.b64decode(b64.encode()) | |
564 | return _binary_or_uuid(data, int(subtype, 16), json_options) | |
565 | ||
566 | ||
567 | def _parse_canonical_datetime(doc, json_options): | |
568 | """Decode a JSON datetime to python datetime.datetime.""" | |
569 | dtm = doc["$date"] | |
570 | if len(doc) != 1: | |
571 | raise TypeError('Bad $date, extra field(s): %s' % (doc,)) | |
572 | # mongoexport 2.6 and newer | |
573 | if isinstance(dtm, string_type): | |
574 | # Parse offset | |
575 | if dtm[-1] == 'Z': | |
576 | dt = dtm[:-1] | |
577 | offset = 'Z' | |
578 | elif dtm[-3] == ':': | |
579 | # (+|-)HH:MM | |
580 | dt = dtm[:-6] | |
581 | offset = dtm[-6:] | |
582 | elif dtm[-5] in ('+', '-'): | |
583 | # (+|-)HHMM | |
584 | dt = dtm[:-5] | |
585 | offset = dtm[-5:] | |
586 | elif dtm[-3] in ('+', '-'): | |
587 | # (+|-)HH | |
588 | dt = dtm[:-3] | |
589 | offset = dtm[-3:] | |
590 | else: | |
591 | dt = dtm | |
592 | offset = '' | |
593 | ||
594 | # Parse the optional factional seconds portion. | |
595 | dot_index = dt.rfind('.') | |
596 | microsecond = 0 | |
597 | if dot_index != -1: | |
598 | microsecond = int(float(dt[dot_index:]) * 1000000) | |
599 | dt = dt[:dot_index] | |
600 | ||
601 | aware = datetime.datetime.strptime( | |
602 | dt, "%Y-%m-%dT%H:%M:%S").replace(microsecond=microsecond, | |
603 | tzinfo=utc) | |
604 | ||
605 | if offset and offset != 'Z': | |
606 | if len(offset) == 6: | |
607 | hours, minutes = offset[1:].split(':') | |
608 | secs = (int(hours) * 3600 + int(minutes) * 60) | |
609 | elif len(offset) == 5: | |
610 | secs = (int(offset[1:3]) * 3600 + int(offset[3:]) * 60) | |
611 | elif len(offset) == 3: | |
612 | secs = int(offset[1:3]) * 3600 | |
613 | if offset[0] == "-": | |
614 | secs *= -1 | |
615 | aware = aware - datetime.timedelta(seconds=secs) | |
616 | ||
617 | if json_options.tz_aware: | |
618 | if json_options.tzinfo: | |
619 | aware = aware.astimezone(json_options.tzinfo) | |
620 | return aware | |
621 | else: | |
622 | return aware.replace(tzinfo=None) | |
623 | return bson._millis_to_datetime(int(dtm), json_options) | |
624 | ||
625 | ||
626 | def _parse_canonical_oid(doc): | |
627 | """Decode a JSON ObjectId to bson.objectid.ObjectId.""" | |
628 | if len(doc) != 1: | |
629 | raise TypeError('Bad $oid, extra field(s): %s' % (doc,)) | |
630 | return ObjectId(doc['$oid']) | |
631 | ||
632 | ||
633 | def _parse_canonical_symbol(doc): | |
634 | """Decode a JSON symbol to Python string.""" | |
635 | symbol = doc['$symbol'] | |
636 | if len(doc) != 1: | |
637 | raise TypeError('Bad $symbol, extra field(s): %s' % (doc,)) | |
638 | return text_type(symbol) | |
639 | ||
640 | ||
641 | def _parse_canonical_code(doc): | |
642 | """Decode a JSON code to bson.code.Code.""" | |
643 | for key in doc: | |
644 | if key not in ('$code', '$scope'): | |
645 | raise TypeError('Bad $code, extra field(s): %s' % (doc,)) | |
646 | return Code(doc['$code'], scope=doc.get('$scope')) | |
647 | ||
648 | ||
649 | def _parse_canonical_regex(doc): | |
650 | """Decode a JSON regex to bson.regex.Regex.""" | |
651 | regex = doc['$regularExpression'] | |
652 | if len(doc) != 1: | |
653 | raise TypeError('Bad $regularExpression, extra field(s): %s' % (doc,)) | |
654 | if len(regex) != 2: | |
655 | raise TypeError('Bad $regularExpression must include only "pattern"' | |
656 | 'and "options" components: %s' % (doc,)) | |
657 | return Regex(regex['pattern'], regex['options']) | |
658 | ||
659 | ||
660 | def _parse_canonical_dbref(doc): | |
661 | """Decode a JSON DBRef to bson.dbref.DBRef.""" | |
662 | for key in doc: | |
663 | if key.startswith('$') and key not in _DBREF_KEYS: | |
664 | # Other keys start with $, so dct cannot be parsed as a DBRef. | |
665 | return doc | |
666 | return DBRef(doc.pop('$ref'), doc.pop('$id'), | |
667 | database=doc.pop('$db', None), **doc) | |
668 | ||
669 | ||
670 | def _parse_canonical_dbpointer(doc): | |
671 | """Decode a JSON (deprecated) DBPointer to bson.dbref.DBRef.""" | |
672 | dbref = doc['$dbPointer'] | |
673 | if len(doc) != 1: | |
674 | raise TypeError('Bad $dbPointer, extra field(s): %s' % (doc,)) | |
675 | if isinstance(dbref, DBRef): | |
676 | dbref_doc = dbref.as_doc() | |
677 | # DBPointer must not contain $db in its value. | |
678 | if dbref.database is not None: | |
679 | raise TypeError( | |
680 | 'Bad $dbPointer, extra field $db: %s' % (dbref_doc,)) | |
681 | if not isinstance(dbref.id, ObjectId): | |
682 | raise TypeError( | |
683 | 'Bad $dbPointer, $id must be an ObjectId: %s' % (dbref_doc,)) | |
684 | if len(dbref_doc) != 2: | |
685 | raise TypeError( | |
686 | 'Bad $dbPointer, extra field(s) in DBRef: %s' % (dbref_doc,)) | |
687 | return dbref | |
688 | else: | |
689 | raise TypeError('Bad $dbPointer, expected a DBRef: %s' % (doc,)) | |
690 | ||
691 | ||
692 | def _parse_canonical_int32(doc): | |
693 | """Decode a JSON int32 to python int.""" | |
694 | i_str = doc['$numberInt'] | |
695 | if len(doc) != 1: | |
696 | raise TypeError('Bad $numberInt, extra field(s): %s' % (doc,)) | |
697 | if not isinstance(i_str, string_type): | |
698 | raise TypeError('$numberInt must be string: %s' % (doc,)) | |
699 | return int(i_str) | |
700 | ||
701 | ||
702 | def _parse_canonical_int64(doc): | |
703 | """Decode a JSON int64 to bson.int64.Int64.""" | |
704 | l_str = doc['$numberLong'] | |
705 | if len(doc) != 1: | |
706 | raise TypeError('Bad $numberLong, extra field(s): %s' % (doc,)) | |
707 | return Int64(l_str) | |
708 | ||
709 | ||
710 | def _parse_canonical_double(doc): | |
711 | """Decode a JSON double to python float.""" | |
712 | d_str = doc['$numberDouble'] | |
713 | if len(doc) != 1: | |
714 | raise TypeError('Bad $numberDouble, extra field(s): %s' % (doc,)) | |
715 | if not isinstance(d_str, string_type): | |
716 | raise TypeError('$numberDouble must be string: %s' % (doc,)) | |
717 | return float(d_str) | |
718 | ||
719 | ||
720 | def _parse_canonical_decimal128(doc): | |
721 | """Decode a JSON decimal128 to bson.decimal128.Decimal128.""" | |
722 | d_str = doc['$numberDecimal'] | |
723 | if len(doc) != 1: | |
724 | raise TypeError('Bad $numberDecimal, extra field(s): %s' % (doc,)) | |
725 | if not isinstance(d_str, string_type): | |
726 | raise TypeError('$numberDecimal must be string: %s' % (doc,)) | |
727 | return Decimal128(d_str) | |
728 | ||
729 | ||
730 | def _parse_canonical_minkey(doc): | |
731 | """Decode a JSON MinKey to bson.min_key.MinKey.""" | |
732 | if doc['$minKey'] is not 1: | |
733 | raise TypeError('$minKey value must be 1: %s' % (doc,)) | |
734 | if len(doc) != 1: | |
735 | raise TypeError('Bad $minKey, extra field(s): %s' % (doc,)) | |
736 | return MinKey() | |
737 | ||
738 | ||
739 | def _parse_canonical_maxkey(doc): | |
740 | """Decode a JSON MaxKey to bson.max_key.MaxKey.""" | |
741 | if doc['$maxKey'] is not 1: | |
742 | raise TypeError('$maxKey value must be 1: %s', (doc,)) | |
743 | if len(doc) != 1: | |
744 | raise TypeError('Bad $minKey, extra field(s): %s' % (doc,)) | |
745 | return MaxKey() | |
746 | ||
747 | ||
748 | def _encode_binary(data, subtype, json_options): | |
749 | if json_options.json_mode == JSONMode.LEGACY: | |
750 | return SON([ | |
751 | ('$binary', base64.b64encode(data).decode()), | |
752 | ('$type', "%02x" % subtype)]) | |
753 | return {'$binary': SON([ | |
754 | ('base64', base64.b64encode(data).decode()), | |
755 | ('subType', "%02x" % subtype)])} | |
756 | ||
757 | ||
758 | def default(obj, json_options=DEFAULT_JSON_OPTIONS): | |
206 | 759 | # We preserve key order when rendering SON, DBRef, etc. as JSON by |
207 | 760 | # returning a SON for those types instead of a dict. |
208 | 761 | if isinstance(obj, ObjectId): |
209 | 762 | return {"$oid": str(obj)} |
210 | 763 | if isinstance(obj, DBRef): |
211 | return _json_convert(obj.as_doc()) | |
764 | return _json_convert(obj.as_doc(), json_options=json_options) | |
212 | 765 | if isinstance(obj, datetime.datetime): |
213 | # TODO share this code w/ bson.py? | |
214 | if obj.utcoffset() is not None: | |
215 | obj = obj - obj.utcoffset() | |
216 | millis = int(calendar.timegm(obj.timetuple()) * 1000 + | |
217 | obj.microsecond / 1000) | |
218 | return {"$date": millis} | |
766 | if (json_options.datetime_representation == | |
767 | DatetimeRepresentation.ISO8601): | |
768 | if not obj.tzinfo: | |
769 | obj = obj.replace(tzinfo=utc) | |
770 | if obj >= EPOCH_AWARE: | |
771 | off = obj.tzinfo.utcoffset(obj) | |
772 | if (off.days, off.seconds, off.microseconds) == (0, 0, 0): | |
773 | tz_string = 'Z' | |
774 | else: | |
775 | tz_string = obj.strftime('%z') | |
776 | millis = int(obj.microsecond / 1000) | |
777 | fracsecs = ".%03d" % (millis,) if millis else "" | |
778 | return {"$date": "%s%s%s" % ( | |
779 | obj.strftime("%Y-%m-%dT%H:%M:%S"), fracsecs, tz_string)} | |
780 | ||
781 | millis = bson._datetime_to_millis(obj) | |
782 | if (json_options.datetime_representation == | |
783 | DatetimeRepresentation.LEGACY): | |
784 | return {"$date": millis} | |
785 | return {"$date": {"$numberLong": str(millis)}} | |
786 | if json_options.strict_number_long and isinstance(obj, Int64): | |
787 | return {"$numberLong": str(obj)} | |
219 | 788 | if isinstance(obj, (RE_TYPE, Regex)): |
220 | 789 | flags = "" |
221 | 790 | if obj.flags & re.IGNORECASE: |
234 | 803 | pattern = obj.pattern |
235 | 804 | else: |
236 | 805 | pattern = obj.pattern.decode('utf-8') |
237 | return SON([("$regex", pattern), ("$options", flags)]) | |
806 | if json_options.json_mode == JSONMode.LEGACY: | |
807 | return SON([("$regex", pattern), ("$options", flags)]) | |
808 | return {'$regularExpression': SON([("pattern", pattern), | |
809 | ("options", flags)])} | |
238 | 810 | if isinstance(obj, MinKey): |
239 | 811 | return {"$minKey": 1} |
240 | 812 | if isinstance(obj, MaxKey): |
242 | 814 | if isinstance(obj, Timestamp): |
243 | 815 | return {"$timestamp": SON([("t", obj.time), ("i", obj.inc)])} |
244 | 816 | if isinstance(obj, Code): |
245 | return SON([('$code', str(obj)), ('$scope', obj.scope)]) | |
817 | if obj.scope is None: | |
818 | return {'$code': str(obj)} | |
819 | return SON([ | |
820 | ('$code', str(obj)), | |
821 | ('$scope', _json_convert(obj.scope, json_options))]) | |
246 | 822 | if isinstance(obj, Binary): |
247 | return SON([ | |
248 | ('$binary', base64.b64encode(obj).decode()), | |
249 | ('$type', "%02x" % obj.subtype)]) | |
823 | return _encode_binary(obj, obj.subtype, json_options) | |
250 | 824 | if PY3 and isinstance(obj, bytes): |
251 | return SON([ | |
252 | ('$binary', base64.b64encode(obj).decode()), | |
253 | ('$type', "00")]) | |
825 | return _encode_binary(obj, 0, json_options) | |
254 | 826 | if isinstance(obj, uuid.UUID): |
255 | return {"$uuid": obj.hex} | |
827 | if json_options.strict_uuid: | |
828 | data = obj.bytes | |
829 | subtype = OLD_UUID_SUBTYPE | |
830 | if json_options.uuid_representation == CSHARP_LEGACY: | |
831 | data = obj.bytes_le | |
832 | elif json_options.uuid_representation == JAVA_LEGACY: | |
833 | data = data[7::-1] + data[:7:-1] | |
834 | elif json_options.uuid_representation == UUID_SUBTYPE: | |
835 | subtype = UUID_SUBTYPE | |
836 | return _encode_binary(data, subtype, json_options) | |
837 | else: | |
838 | return {"$uuid": obj.hex} | |
839 | if isinstance(obj, Decimal128): | |
840 | return {"$numberDecimal": str(obj)} | |
841 | if isinstance(obj, bool): | |
842 | return obj | |
843 | if (json_options.json_mode == JSONMode.CANONICAL and | |
844 | isinstance(obj, integer_types)): | |
845 | if -2 ** 31 <= obj < 2 ** 31: | |
846 | return {'$numberInt': text_type(obj)} | |
847 | return {'$numberLong': text_type(obj)} | |
848 | if json_options.json_mode != JSONMode.LEGACY and isinstance(obj, float): | |
849 | if math.isnan(obj): | |
850 | return {'$numberDouble': 'NaN'} | |
851 | elif math.isinf(obj): | |
852 | representation = 'Infinity' if obj > 0 else '-Infinity' | |
853 | return {'$numberDouble': representation} | |
854 | elif json_options.json_mode == JSONMode.CANONICAL: | |
855 | # repr() will return the shortest string guaranteed to produce the | |
856 | # original value, when float() is called on it. str produces a | |
857 | # shorter string in Python 2. | |
858 | return {'$numberDouble': text_type(repr(obj))} | |
256 | 859 | raise TypeError("%r is not JSON serializable" % obj) |
0 | # Copyright 2010-2015 MongoDB, Inc. | |
0 | # Copyright 2010-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
35 | 35 | |
36 | 36 | def __le__(self, other): |
37 | 37 | return isinstance(other, MaxKey) |
38 | ||
38 | ||
39 | 39 | def __lt__(self, dummy): |
40 | 40 | return False |
41 | 41 | |
42 | 42 | def __ge__(self, dummy): |
43 | 43 | return True |
44 | ||
44 | ||
45 | 45 | def __gt__(self, other): |
46 | 46 | return not isinstance(other, MaxKey) |
47 | 47 |
0 | # Copyright 2010-2015 MongoDB, Inc. | |
0 | # Copyright 2010-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
32 | 32 | |
33 | 33 | def __ne__(self, other): |
34 | 34 | return not self == other |
35 | ||
35 | ||
36 | 36 | def __le__(self, dummy): |
37 | 37 | return True |
38 | ||
38 | ||
39 | 39 | def __lt__(self, other): |
40 | 40 | return not isinstance(other, MinKey) |
41 | 41 | |
42 | 42 | def __ge__(self, other): |
43 | 43 | return isinstance(other, MinKey) |
44 | ||
44 | ||
45 | 45 | def __gt__(self, dummy): |
46 | 46 | return False |
47 | 47 |
234 | 234 | def __setstate__(self, value): |
235 | 235 | """explicit state set from pickling |
236 | 236 | """ |
237 | # Provide backwards compatibility with OIDs | |
237 | # Provide backwards compatability with OIDs | |
238 | 238 | # pickled with pymongo-1.9 or older. |
239 | 239 | if isinstance(value, dict): |
240 | 240 | oid = value["_ObjectId__id"] |
0 | # Copyright 2009-2015 MongoDB, Inc. | |
0 | # Copyright 2009-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); you |
3 | 3 | # may not use this file except in compliance with the License. You |
21 | 21 | import codecs |
22 | 22 | import _thread as thread |
23 | 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 | ||
24 | 31 | MAXSIZE = sys.maxsize |
25 | 32 | |
26 | 33 | imap = map |
32 | 39 | # the b prefix (e.g. b'foo'). |
33 | 40 | # See http://python3porting.com/problems.html#nicer-solutions |
34 | 41 | return codecs.latin_1_encode(s)[0] |
35 | ||
36 | def u(s): | |
37 | # PY3 strings may already be treated as unicode literals | |
38 | return s | |
39 | 42 | |
40 | 43 | def bytes_from_hex(h): |
41 | 44 | return bytes.fromhex(h) |
56 | 59 | string_type = str |
57 | 60 | integer_types = int |
58 | 61 | else: |
62 | import collections as abc | |
59 | 63 | import thread |
60 | 64 | |
61 | 65 | from itertools import imap |
69 | 73 | def b(s): |
70 | 74 | # See comments above. In python 2.x b('foo') is just 'foo'. |
71 | 75 | return s |
72 | ||
73 | def u(s): | |
74 | """Replacement for unicode literal prefix.""" | |
75 | return unicode(s.replace('\\', '\\\\'), 'unicode_escape') | |
76 | 76 | |
77 | 77 | def bytes_from_hex(h): |
78 | 78 | return h.decode('hex') |
0 | # Copyright 2015-present MongoDB, Inc. | |
1 | # | |
2 | # Licensed under the Apache License, Version 2.0 (the "License"); | |
3 | # you may not use this file except in compliance with the License. | |
4 | # You may obtain a copy of the License at | |
5 | # | |
6 | # http://www.apache.org/licenses/LICENSE-2.0 | |
7 | # | |
8 | # Unless required by applicable law or agreed to in writing, software | |
9 | # distributed under the License is distributed on an "AS IS" BASIS, | |
10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
11 | # See the License for the specific language governing permissions and | |
12 | # limitations under the License. | |
13 | ||
14 | """Tools for representing raw BSON documents. | |
15 | """ | |
16 | ||
17 | from mockupdb._bson import _UNPACK_INT, _iterate_elements | |
18 | from mockupdb._bson.py3compat import abc, iteritems | |
19 | from mockupdb._bson.codec_options import ( | |
20 | DEFAULT_CODEC_OPTIONS as DEFAULT, _RAW_BSON_DOCUMENT_MARKER) | |
21 | from mockupdb._bson.errors import InvalidBSON | |
22 | ||
23 | ||
24 | class RawBSONDocument(abc.Mapping): | |
25 | """Representation for a MongoDB document that provides access to the raw | |
26 | BSON bytes that compose it. | |
27 | ||
28 | Only when a field is accessed or modified within the document does | |
29 | RawBSONDocument decode its bytes. | |
30 | """ | |
31 | ||
32 | __slots__ = ('__raw', '__inflated_doc', '__codec_options') | |
33 | _type_marker = _RAW_BSON_DOCUMENT_MARKER | |
34 | ||
35 | def __init__(self, bson_bytes, codec_options=None): | |
36 | """Create a new :class:`RawBSONDocument`. | |
37 | ||
38 | :Parameters: | |
39 | - `bson_bytes`: the BSON bytes that compose this document | |
40 | - `codec_options` (optional): An instance of | |
41 | :class:`~bson.codec_options.CodecOptions`. | |
42 | ||
43 | .. versionchanged:: 3.5 | |
44 | If a :class:`~bson.codec_options.CodecOptions` is passed in, its | |
45 | `document_class` must be :class:`RawBSONDocument`. | |
46 | """ | |
47 | self.__raw = bson_bytes | |
48 | self.__inflated_doc = None | |
49 | # Can't default codec_options to DEFAULT_RAW_BSON_OPTIONS in signature, | |
50 | # it refers to this class RawBSONDocument. | |
51 | if codec_options is None: | |
52 | codec_options = DEFAULT_RAW_BSON_OPTIONS | |
53 | elif codec_options.document_class is not RawBSONDocument: | |
54 | raise TypeError( | |
55 | "RawBSONDocument cannot use CodecOptions with document " | |
56 | "class %s" % (codec_options.document_class, )) | |
57 | self.__codec_options = codec_options | |
58 | ||
59 | @property | |
60 | def raw(self): | |
61 | """The raw BSON bytes composing this document.""" | |
62 | return self.__raw | |
63 | ||
64 | def items(self): | |
65 | """Lazily decode and iterate elements in this document.""" | |
66 | return iteritems(self.__inflated) | |
67 | ||
68 | @property | |
69 | def __inflated(self): | |
70 | if self.__inflated_doc is None: | |
71 | # We already validated the object's size when this document was | |
72 | # created, so no need to do that again. We still need to check the | |
73 | # size of all the elements and compare to the document size. | |
74 | object_size = _UNPACK_INT(self.__raw[:4])[0] - 1 | |
75 | position = 0 | |
76 | self.__inflated_doc = {} | |
77 | for key, value, position in _iterate_elements( | |
78 | self.__raw, 4, object_size, self.__codec_options): | |
79 | self.__inflated_doc[key] = value | |
80 | if position != object_size: | |
81 | raise InvalidBSON('bad object or element length') | |
82 | return self.__inflated_doc | |
83 | ||
84 | def __getitem__(self, item): | |
85 | return self.__inflated[item] | |
86 | ||
87 | def __iter__(self): | |
88 | return iter(self.__inflated) | |
89 | ||
90 | def __len__(self): | |
91 | return len(self.__inflated) | |
92 | ||
93 | def __eq__(self, other): | |
94 | if isinstance(other, RawBSONDocument): | |
95 | return self.__raw == other.raw | |
96 | return NotImplemented | |
97 | ||
98 | def __repr__(self): | |
99 | return ("RawBSONDocument(%r, codec_options=%r)" | |
100 | % (self.raw, self.__codec_options)) | |
101 | ||
102 | ||
103 | DEFAULT_RAW_BSON_OPTIONS = DEFAULT.with_options(document_class=RawBSONDocument) |
0 | # Copyright 2013-2015 MongoDB, Inc. | |
0 | # Copyright 2013-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
99 | 99 | |
100 | 100 | def __eq__(self, other): |
101 | 101 | if isinstance(other, Regex): |
102 | return self.pattern == self.pattern and self.flags == other.flags | |
102 | return self.pattern == other.pattern and self.flags == other.flags | |
103 | 103 | else: |
104 | 104 | return NotImplemented |
105 | 105 |
0 | # Copyright 2009-2015 MongoDB, Inc. | |
0 | # Copyright 2009-present MongoDB, Inc. | |
1 | 1 | # |
2 | 2 | # Licensed under the Apache License, Version 2.0 (the "License"); |
3 | 3 | # you may not use this file except in compliance with the License. |
17 | 17 | of keys is important. A SON object can be used just like a normal Python |
18 | 18 | dictionary.""" |
19 | 19 | |
20 | import collections | |
21 | 20 | import copy |
22 | 21 | import re |
23 | 22 | |
24 | from mockupdb._bson.py3compat import iteritems | |
23 | from mockupdb._bson.py3compat import abc, iteritems | |
25 | 24 | |
26 | 25 | |
27 | 26 | # This sort of sucks, but seems to be as good as it gets... |
33 | 32 | """SON data. |
34 | 33 | |
35 | 34 | A subclass of dict that maintains ordering of keys and provides a |
36 | few extra niceties for dealing with SON. SON objects can be | |
37 | converted to and from mockupdb._bson. | |
38 | ||
39 | The mapping from Python types to BSON types is as follows: | |
40 | ||
41 | ======================================= ============= =================== | |
42 | Python Type BSON Type Supported Direction | |
43 | ======================================= ============= =================== | |
44 | None null both | |
45 | bool boolean both | |
46 | int [#int]_ int32 / int64 py -> bson | |
47 | long int64 py -> bson | |
48 | `bson.int64.Int64` int64 both | |
49 | float number (real) both | |
50 | string string py -> bson | |
51 | unicode string both | |
52 | list array both | |
53 | dict / `SON` object both | |
54 | datetime.datetime [#dt]_ [#dt2]_ date both | |
55 | `bson.regex.Regex` regex both | |
56 | compiled re [#re]_ regex py -> bson | |
57 | `bson.binary.Binary` binary both | |
58 | `bson.objectid.ObjectId` oid both | |
59 | `bson.dbref.DBRef` dbref both | |
60 | None undefined bson -> py | |
61 | unicode code bson -> py | |
62 | `bson.code.Code` code py -> bson | |
63 | unicode symbol bson -> py | |
64 | bytes (Python 3) [#bytes]_ binary both | |
65 | ======================================= ============= =================== | |
66 | ||
67 | Note that to save binary data it must be wrapped as an instance of | |
68 | `bson.binary.Binary`. Otherwise it will be saved as a BSON string | |
69 | and retrieved as unicode. | |
70 | ||
71 | .. [#int] A Python int will be saved as a BSON int32 or BSON int64 depending | |
72 | on its size. A BSON int32 will always decode to a Python int. A BSON | |
73 | int64 will always decode to a :class:`~bson.int64.Int64`. | |
74 | .. [#dt] datetime.datetime instances will be rounded to the nearest | |
75 | millisecond when saved | |
76 | .. [#dt2] all datetime.datetime instances are treated as *naive*. clients | |
77 | should always use UTC. | |
78 | .. [#re] :class:`~bson.regex.Regex` instances and regular expression | |
79 | objects from ``re.compile()`` are both saved as BSON regular expressions. | |
80 | BSON regular expressions are decoded as :class:`~bson.regex.Regex` | |
81 | instances. | |
82 | .. [#bytes] The bytes type from Python 3.x is encoded as BSON binary with | |
83 | subtype 0. In Python 3.x it will be decoded back to bytes. In Python 2.x | |
84 | it will be decoded to an instance of :class:`~bson.binary.Binary` with | |
85 | subtype 0. | |
35 | few extra niceties for dealing with SON. SON provides an API | |
36 | similar to collections.OrderedDict from Python 2.7+. | |
86 | 37 | """ |
87 | 38 | |
88 | 39 | def __init__(self, data=None, **kwargs): |
226 | 177 | def transform_value(value): |
227 | 178 | if isinstance(value, list): |
228 | 179 | return [transform_value(v) for v in value] |
229 | elif isinstance(value, collections.Mapping): | |
180 | elif isinstance(value, abc.Mapping): | |
230 | 181 | return dict([ |
231 | 182 | (k, transform_value(v)) |
232 | 183 | for k, v in iteritems(value)]) |
0 | 0 | Metadata-Version: 1.1 |
1 | 1 | Name: mockupdb |
2 | Version: 1.3.0 | |
2 | Version: 1.4.1 | |
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.4.1 (2018-06-30) | |
25 | ------------------ | |
26 | ||
27 | Fix an inadvertent dependency on PyMongo, which broke the docs build. | |
28 | ||
29 | 1.4.0 (2018-06-29) | |
30 | ------------------ | |
31 | ||
32 | Support, and expect, OP_MSG requests from clients. Thanks to Shane Harvey for | |
33 | the contribution. | |
34 | ||
35 | Update vendored bson library from PyMongo. Support the Decimal128 BSON type. Fix | |
36 | Matcher so it equates BSON objects from PyMongo like ``ObjectId(...)`` with | |
37 | equivalent objects created from MockupDB's vendored bson library. | |
23 | 38 | |
24 | 39 | 1.3.0 (2018-02-19) |
25 | 40 | ------------------ |
28 | 28 | mockupdb/_bson/code.py |
29 | 29 | mockupdb/_bson/codec_options.py |
30 | 30 | mockupdb/_bson/dbref.py |
31 | mockupdb/_bson/decimal128.py | |
31 | 32 | mockupdb/_bson/errors.py |
32 | 33 | mockupdb/_bson/int64.py |
33 | 34 | mockupdb/_bson/json_util.py |
35 | 36 | mockupdb/_bson/min_key.py |
36 | 37 | mockupdb/_bson/objectid.py |
37 | 38 | mockupdb/_bson/py3compat.py |
39 | mockupdb/_bson/raw_bson.py | |
38 | 40 | mockupdb/_bson/regex.py |
39 | 41 | mockupdb/_bson/son.py |
40 | 42 | mockupdb/_bson/timestamp.py |
23 | 23 | |
24 | 24 | setup( |
25 | 25 | name='mockupdb', |
26 | version='1.3.0', | |
26 | version='1.4.1', | |
27 | 27 | description="MongoDB Wire Protocol server library", |
28 | 28 | long_description=readme + '\n\n' + changelog, |
29 | 29 | author="A. Jesse Jiryu Davis", |
3 | 3 | """Test MockupDB.""" |
4 | 4 | |
5 | 5 | import contextlib |
6 | import os | |
7 | 6 | import ssl |
8 | 7 | import sys |
9 | 8 | |
18 | 17 | from Queue import Queue |
19 | 18 | |
20 | 19 | # Tests depend on PyMongo's BSON implementation, but MockupDB itself does not. |
21 | from bson import SON | |
20 | from bson import (Binary, Code, DBRef, Decimal128, MaxKey, MinKey, ObjectId, | |
21 | Regex, SON, Timestamp) | |
22 | 22 | from bson.codec_options import CodecOptions |
23 | 23 | from pymongo import MongoClient, message, WriteConcern |
24 | 24 | |
25 | from mockupdb import (go, going, | |
26 | Command, Matcher, MockupDB, Request, | |
27 | OpDelete, OpInsert, OpQuery, OpUpdate, | |
28 | DELETE_FLAGS, INSERT_FLAGS, UPDATE_FLAGS, QUERY_FLAGS) | |
29 | ||
25 | from mockupdb import (_bson as mockup_bson, go, going, | |
26 | Command, CommandBase, Matcher, MockupDB, Request, | |
27 | OpInsert, OpMsg, OpQuery, QUERY_FLAGS) | |
30 | 28 | from tests import unittest # unittest2 on Python 2.6. |
31 | 29 | |
32 | 30 | |
135 | 133 | request.assert_matches(Command('foo')) |
136 | 134 | |
137 | 135 | |
138 | class TestLegacyWrites(unittest.TestCase): | |
136 | class TestUnacknowledgedWrites(unittest.TestCase): | |
139 | 137 | def setUp(self): |
140 | 138 | self.server = MockupDB(auto_ismaster=True) |
141 | 139 | self.server.run() |
146 | 144 | |
147 | 145 | def test_insert_one(self): |
148 | 146 | with going(self.collection.insert_one, {'_id': 1}): |
149 | self.server.receives(OpInsert({'_id': 1}, flags=0)) | |
147 | # The moreToCome flag = 2. | |
148 | self.server.receives( | |
149 | OpMsg('insert', 'collection', writeConcern={'w': 0}, flags=2)) | |
150 | 150 | |
151 | 151 | def test_insert_many(self): |
152 | 152 | collection = self.collection.with_options( |
153 | 153 | write_concern=WriteConcern(0)) |
154 | 154 | |
155 | flags = INSERT_FLAGS['ContinueOnError'] | |
156 | 155 | docs = [{'_id': 1}, {'_id': 2}] |
157 | 156 | with going(collection.insert_many, docs, ordered=False): |
158 | self.server.receives(OpInsert(docs, flags=flags)) | |
157 | self.server.receives(OpMsg(SON([ | |
158 | ('insert', 'collection'), | |
159 | ('ordered', False), | |
160 | ('writeConcern', {'w': 0})]), flags=2)) | |
159 | 161 | |
160 | 162 | def test_replace_one(self): |
161 | 163 | with going(self.collection.replace_one, {}, {}): |
162 | self.server.receives(OpUpdate({}, {}, flags=0)) | |
164 | self.server.receives(OpMsg(SON([ | |
165 | ('update', 'collection'), | |
166 | ('writeConcern', {'w': 0}) | |
167 | ]), flags=2)) | |
163 | 168 | |
164 | 169 | def test_update_many(self): |
165 | flags = UPDATE_FLAGS['MultiUpdate'] | |
166 | 170 | with going(self.collection.update_many, {}, {'$unset': 'a'}): |
167 | update = self.server.receives(OpUpdate({}, {}, flags=flags)) | |
168 | self.assertEqual(2, update.flags) | |
171 | self.server.receives(OpMsg(SON([ | |
172 | ('update', 'collection'), | |
173 | ('ordered', True), | |
174 | ('writeConcern', {'w': 0}) | |
175 | ]), flags=2)) | |
169 | 176 | |
170 | 177 | def test_delete_one(self): |
171 | flags = DELETE_FLAGS['SingleRemove'] | |
172 | 178 | with going(self.collection.delete_one, {}): |
173 | delete = self.server.receives(OpDelete({}, flags=flags)) | |
174 | self.assertEqual(1, delete.flags) | |
179 | self.server.receives(OpMsg(SON([ | |
180 | ('delete', 'collection'), | |
181 | ('writeConcern', {'w': 0}) | |
182 | ]), flags=2)) | |
175 | 183 | |
176 | 184 | def test_delete_many(self): |
177 | 185 | with going(self.collection.delete_many, {}): |
178 | delete = self.server.receives(OpDelete({}, flags=0)) | |
179 | self.assertEqual(0, delete.flags) | |
186 | self.server.receives(OpMsg(SON([ | |
187 | ('delete', 'collection'), | |
188 | ('writeConcern', {'w': 0})]), flags=2)) | |
180 | 189 | |
181 | 190 | |
182 | 191 | class TestMatcher(unittest.TestCase): |
195 | 204 | self.assertFalse( |
196 | 205 | Matcher(Command('a', b=1)).matches(Command('a', b=2))) |
197 | 206 | |
207 | def test_bson_classes(self): | |
208 | _id = '5a918f9fa08bff9c7688d3e1' | |
209 | ||
210 | for a, b in [ | |
211 | (Binary(b'foo'), mockup_bson.Binary(b'foo')), | |
212 | (Code('foo'), mockup_bson.Code('foo')), | |
213 | (Code('foo', {'x': 1}), mockup_bson.Code('foo', {'x': 1})), | |
214 | (DBRef('coll', 1), mockup_bson.DBRef('coll', 1)), | |
215 | (DBRef('coll', 1, 'db'), mockup_bson.DBRef('coll', 1, 'db')), | |
216 | (Decimal128('1'), mockup_bson.Decimal128('1')), | |
217 | (MaxKey(), mockup_bson.MaxKey()), | |
218 | (MinKey(), mockup_bson.MinKey()), | |
219 | (ObjectId(_id), mockup_bson.ObjectId(_id)), | |
220 | (Regex('foo', 'i'), mockup_bson.Regex('foo', 'i')), | |
221 | (Timestamp(1, 2), mockup_bson.Timestamp(1, 2)), | |
222 | ]: | |
223 | # Basic case. | |
224 | self.assertTrue( | |
225 | Matcher(Command(y=b)).matches(Command(y=b)), | |
226 | "MockupDB %r doesn't equal itself" % (b,)) | |
227 | ||
228 | # First Command argument is special, try comparing the second also. | |
229 | self.assertTrue( | |
230 | Matcher(Command('x', y=b)).matches(Command('x', y=b)), | |
231 | "MockupDB %r doesn't equal itself" % (b,)) | |
232 | ||
233 | # In practice, users pass PyMongo classes in message specs. | |
234 | self.assertTrue( | |
235 | Matcher(Command(y=b)).matches(Command(y=a)), | |
236 | "PyMongo %r != MockupDB %r" % (a, b)) | |
237 | ||
238 | self.assertTrue( | |
239 | Matcher(Command('x', y=b)).matches(Command('x', y=a)), | |
240 | "PyMongo %r != MockupDB %r" % (a, b)) | |
241 | ||
198 | 242 | |
199 | 243 | class TestAutoresponds(unittest.TestCase): |
200 | 244 | def test_auto_dequeue(self): |
208 | 252 | def test_autoresponds_case_insensitive(self): |
209 | 253 | server = MockupDB(auto_ismaster=True) |
210 | 254 | # Little M. Note this is only case-insensitive because it's a Command. |
211 | server.autoresponds(Command('fooBar'), foo='bar') | |
255 | server.autoresponds(CommandBase('fooBar'), foo='bar') | |
212 | 256 | server.run() |
213 | 257 | response = MongoClient(server.uri).admin.command('Foobar') |
214 | 258 | self.assertEqual('bar', response['foo']) |