|
1 |
| -from collections import Mapping |
2 | 1 | from functools import reduce
|
3 | 2 | from operator import and_
|
4 | 3 | from typing import Union, Set
|
5 | 4 |
|
6 |
| -from pymongo.cursor import Cursor, CursorType |
| 5 | +from pymongo.cursor import Cursor as PyMongoCursor |
7 | 6 | from pymongo.collection import Collection as PyMongoCollection
|
8 | 7 |
|
9 | 8 | from ..types import Optional, Set, Mapping, Tuple
|
|
13 | 12 | from ....package.loader import traverse
|
14 | 13 |
|
15 | 14 |
|
16 |
| -class QueryableFilter(Filter): |
17 |
| - """Additional behaviour for queryable filters. |
18 |
| - |
19 |
| - This provides a simplified query syntax, free of nearly all boilerplate (repeated mandatory code). |
20 |
| - |
21 |
| - Additional semantics include: |
22 |
| - |
23 |
| - * Filters generated from Queryable collections may be iterated to issue the query and produce a PyMongo Cursor. |
24 |
| - |
25 |
| - for record in User.age > 27: |
26 |
| - print(record) |
27 |
| - |
28 |
| - * Filters may be combined as before (using binary "&" and "|" operators), however this has been extended to support |
29 |
| - sets of additional criteria, and additional operators: addition and subtraction. Where addition performs an "or" |
30 |
| - to include additional results, a subtraction performs an "and" of the inverse of the set contents, excluding |
31 |
| - results. |
32 |
| - |
33 |
| - The last offers a few powerful additions: |
34 |
| - |
35 |
| - * Examine the "contents" of a "room" record (an `@property` returning a `QueryableFilter`) and return only the |
36 |
| - siblings of the current record: |
37 |
| - |
38 |
| - siblings = self.room.contents - {self} |
39 |
| - """ |
40 |
| - |
41 |
| - def __getitem__(self, item:Union[str,int,slice]): |
42 |
| - """Retrieve the value of the given string key, or apply skip/limit options.""" |
43 |
| - |
44 |
| - if isinstance(item, str): |
45 |
| - return super().__getitem__(item) |
46 |
| - |
47 |
| - return None |
48 |
| - |
49 |
| - def __iter__(self) -> Cursor: |
50 |
| - if hasattr(self.document, 'find') and getattr(self.document, '__bound__', None): |
51 |
| - return self.document.find(self) |
52 |
| - |
53 |
| - elif self.collection: |
54 |
| - return self.collection.find(self) |
55 |
| - |
56 |
| - raise TypeError("Can not iterate an unbound Filter instance.") |
57 |
| - |
58 |
| - |
59 | 15 | class Queryable(Collection):
|
60 | 16 | """Extend active collection behaviours to include querying.
|
61 | 17 |
|
@@ -109,7 +65,51 @@ class Queryable(Collection):
|
109 | 65 | 'use_cursor': 'useCursor',
|
110 | 66 | }
|
111 | 67 |
|
112 |
| - # _Filter = QueryableFilter |
| 68 | + Filter = Filter |
| 69 | + |
| 70 | + class Cursor(PyMongoCursor): |
| 71 | + def __init__(self, document_class, *args, **kw): |
| 72 | + self._Document = document_class |
| 73 | + super().__init__(*args, **kw) |
| 74 | + |
| 75 | + def __getitem__(self, index): |
| 76 | + return self._Document.from_mongo(super().__getitem__(index)) |
| 77 | + |
| 78 | + def first(self): |
| 79 | + try: |
| 80 | + return self.limit(-1).next() |
| 81 | + except StopIteration: |
| 82 | + return None |
| 83 | + |
| 84 | + def next(self): |
| 85 | + return self._Document.from_mongo(super().__next__()) |
| 86 | + |
| 87 | + __next__ = next |
| 88 | + |
| 89 | + __cursor_defaults__ = { |
| 90 | + #projection=None, # Apply a default projection. |
| 91 | + #skip=0, # Apply a default initial offset. |
| 92 | + #limit=0, # Apply a default integer record count. |
| 93 | + #no_cursor_timeout=False, # Disable cursor timeouts. |
| 94 | + #cursor_type=CursorType.NON_TAILABLE, # Alter the default "type" of cursor requested. |
| 95 | + #sort=None, # Default sort order. |
| 96 | + #allow_partial_results=False, |
| 97 | + #oplog_replay=False, |
| 98 | + #modifiers=None, |
| 99 | + #batch_size=0, |
| 100 | + #manipulate=True, |
| 101 | + #collation=None, |
| 102 | + #hint=None, |
| 103 | + #max_scan=None, |
| 104 | + #max_time_ms=None, |
| 105 | + #max=None, |
| 106 | + #min=None, |
| 107 | + #return_key=False, |
| 108 | + #show_record_id=False, |
| 109 | + #snapshot=False, |
| 110 | + #comment=None, |
| 111 | + #session=None, |
| 112 | + } |
113 | 113 |
|
114 | 114 | @classmethod
|
115 | 115 | def _prepare_query(cls, mapping:Mapping[str,str], valid, *args, **kw) -> Tuple[Collection, PyMongoCollection, Filter, dict]:
|
@@ -236,18 +236,25 @@ def _prepare_aggregate(cls, *args, **kw) -> Tuple[Collection, PyMongoCollection,
|
236 | 236 | return cls, collection, stages, options
|
237 | 237 |
|
238 | 238 | @classmethod
|
239 |
| - def find(cls, *args, **kw) -> Cursor: |
| 239 | + def find(cls, *args, **kw) -> PyMongoCursor: |
240 | 240 | """Query the collection this class is bound to.
|
241 | 241 |
|
242 | 242 | Additional arguments are processed according to `_prepare_find` prior to passing to PyMongo, where positional
|
243 | 243 | parameters are interpreted as query fragments, parametric keyword arguments combined, and other keyword
|
244 | 244 | arguments passed along with minor transformation.
|
245 | 245 |
|
| 246 | + Defaults for values passed to `find_one` (the `Cursor` class) may be specified as a dictionary / mapping |
| 247 | + attribute of your model named `__cursor_defaults__`. |
| 248 | + |
| 249 | + Documents returned by the resulting Cursor will be automatically encapsulated in their associated Document |
| 250 | + subclass. |
| 251 | + |
246 | 252 | https://api.mongodb.com/python/current/api/pymongo/collection.html#pymongo.collection.Collection.find
|
247 | 253 | """
|
248 | 254 |
|
249 | 255 | Doc, collection, query, options = cls._prepare_find(*args, **kw)
|
250 |
| - return collection.find(query, **options) |
| 256 | + return cls.Cursor(cls, collection, query, **{**cls.__cursor_defaults__, **options}) |
| 257 | + # The above, somewhat odd ** expansion structure, avoids "multiple values for keyword" errors. |
251 | 258 |
|
252 | 259 | @classmethod
|
253 | 260 | def find_one(cls, *args, **kw) -> Optional[Document]:
|
|
0 commit comments