autonomous-app 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. autonomous/__init__.py +1 -1
  2. autonomous/ai/audioagent.py +1 -1
  3. autonomous/ai/imageagent.py +1 -1
  4. autonomous/ai/jsonagent.py +1 -1
  5. autonomous/ai/models/openai.py +81 -53
  6. autonomous/ai/oaiagent.py +1 -14
  7. autonomous/ai/textagent.py +1 -1
  8. autonomous/auth/autoauth.py +10 -10
  9. autonomous/auth/user.py +17 -2
  10. autonomous/db/__init__.py +42 -0
  11. autonomous/db/base/__init__.py +33 -0
  12. autonomous/db/base/common.py +62 -0
  13. autonomous/db/base/datastructures.py +476 -0
  14. autonomous/db/base/document.py +1230 -0
  15. autonomous/db/base/fields.py +767 -0
  16. autonomous/db/base/metaclasses.py +468 -0
  17. autonomous/db/base/utils.py +22 -0
  18. autonomous/db/common.py +79 -0
  19. autonomous/db/connection.py +472 -0
  20. autonomous/db/context_managers.py +313 -0
  21. autonomous/db/dereference.py +291 -0
  22. autonomous/db/document.py +1141 -0
  23. autonomous/db/errors.py +165 -0
  24. autonomous/db/fields.py +2732 -0
  25. autonomous/db/mongodb_support.py +24 -0
  26. autonomous/db/pymongo_support.py +80 -0
  27. autonomous/db/queryset/__init__.py +28 -0
  28. autonomous/db/queryset/base.py +2033 -0
  29. autonomous/db/queryset/field_list.py +88 -0
  30. autonomous/db/queryset/manager.py +58 -0
  31. autonomous/db/queryset/queryset.py +189 -0
  32. autonomous/db/queryset/transform.py +527 -0
  33. autonomous/db/queryset/visitor.py +189 -0
  34. autonomous/db/signals.py +59 -0
  35. autonomous/logger.py +3 -0
  36. autonomous/model/autoattr.py +56 -41
  37. autonomous/model/automodel.py +95 -34
  38. autonomous/storage/imagestorage.py +49 -8
  39. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/METADATA +2 -2
  40. autonomous_app-0.3.2.dist-info/RECORD +60 -0
  41. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/WHEEL +1 -1
  42. autonomous_app-0.3.0.dist-info/RECORD +0 -35
  43. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/LICENSE +0 -0
  44. {autonomous_app-0.3.0.dist-info → autonomous_app-0.3.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2033 @@
1
+ import copy
2
+ import itertools
3
+ import re
4
+ import warnings
5
+ from collections.abc import Mapping
6
+
7
+ import pymongo
8
+ import pymongo.errors
9
+ from bson import SON, json_util
10
+ from bson.code import Code
11
+ from pymongo.collection import ReturnDocument
12
+ from pymongo.common import validate_read_preference
13
+ from pymongo.read_concern import ReadConcern
14
+
15
+ from autonomous.db import signals
16
+ from autonomous.db.base import get_document
17
+ from autonomous.db.common import _import_class
18
+ from autonomous.db.connection import get_db
19
+ from autonomous.db.context_managers import (
20
+ no_dereferencing_active_for_class,
21
+ set_read_write_concern,
22
+ set_write_concern,
23
+ switch_db,
24
+ )
25
+ from autonomous.db.errors import (
26
+ BulkWriteError,
27
+ InvalidQueryError,
28
+ LookUpError,
29
+ NotUniqueError,
30
+ OperationError,
31
+ )
32
+ from autonomous.db.pymongo_support import (
33
+ LEGACY_JSON_OPTIONS,
34
+ count_documents,
35
+ )
36
+ from autonomous.db.queryset import transform
37
+ from autonomous.db.queryset.field_list import QueryFieldList
38
+ from autonomous.db.queryset.visitor import Q, QNode
39
+
40
+ __all__ = ("BaseQuerySet", "DO_NOTHING", "NULLIFY", "CASCADE", "DENY", "PULL")
41
+
42
+ # Delete rules
43
+ DO_NOTHING = 0
44
+ NULLIFY = 1
45
+ CASCADE = 2
46
+ DENY = 3
47
+ PULL = 4
48
+
49
+
50
+ class BaseQuerySet:
51
+ """A set of results returned from a query. Wraps a MongoDB cursor,
52
+ providing :class:`~autonomous.db.Document` objects as the results.
53
+ """
54
+
55
+ def __init__(self, document, collection):
56
+ self._document = document
57
+ self._collection_obj = collection
58
+ self._mongo_query = None
59
+ self._query_obj = Q()
60
+ self._cls_query = {}
61
+ self._where_clause = None
62
+ self._loaded_fields = QueryFieldList()
63
+ self._ordering = None
64
+ self._snapshot = False
65
+ self._timeout = True
66
+ self._allow_disk_use = False
67
+ self._read_preference = None
68
+ self._read_concern = None
69
+ self._iter = False
70
+ self._scalar = []
71
+ self._none = False
72
+ self._as_pymongo = False
73
+ self._search_text = None
74
+ self._search_text_score = None
75
+
76
+ self.__dereference = False
77
+ self.__auto_dereference = True
78
+
79
+ # If inheritance is allowed, only return instances and instances of
80
+ # subclasses of the class being used
81
+ if document._meta.get("allow_inheritance") is True:
82
+ if len(self._document._subclasses) == 1:
83
+ self._cls_query = {"_cls": self._document._subclasses[0]}
84
+ else:
85
+ self._cls_query = {"_cls": {"$in": self._document._subclasses}}
86
+ self._loaded_fields = QueryFieldList(always_include=["_cls"])
87
+
88
+ self._cursor_obj = None
89
+ self._limit = None
90
+ self._skip = None
91
+
92
+ self._hint = -1 # Using -1 as None is a valid value for hint
93
+ self._collation = None
94
+ self._batch_size = None
95
+ self._max_time_ms = None
96
+ self._comment = None
97
+
98
+ # Hack - As people expect cursor[5:5] to return
99
+ # an empty result set. It's hard to do that right, though, because the
100
+ # server uses limit(0) to mean 'no limit'. So we set _empty
101
+ # in that case and check for it when iterating. We also unset
102
+ # it anytime we change _limit. Inspired by how it is done in pymongo.Cursor
103
+ self._empty = False
104
+
105
+ def __call__(self, q_obj=None, **query):
106
+ """Filter the selected documents by calling the
107
+ :class:`~autonomous.db.queryset.QuerySet` with a query.
108
+
109
+ :param q_obj: a :class:`~autonomous.db.queryset.Q` object to be used in
110
+ the query; the :class:`~autonomous.db.queryset.QuerySet` is filtered
111
+ multiple times with different :class:`~autonomous.db.queryset.Q`
112
+ objects, only the last one will be used.
113
+ :param query: Django-style query keyword arguments.
114
+ """
115
+ query = Q(**query)
116
+ if q_obj:
117
+ # Make sure proper query object is passed.
118
+ if not isinstance(q_obj, QNode):
119
+ msg = (
120
+ "Not a query object: %s. "
121
+ "Did you intend to use key=value?" % q_obj
122
+ )
123
+ raise InvalidQueryError(msg)
124
+ query &= q_obj
125
+
126
+ queryset = self.clone()
127
+ queryset._query_obj &= query
128
+ queryset._mongo_query = None
129
+ queryset._cursor_obj = None
130
+
131
+ return queryset
132
+
133
+ def __getstate__(self):
134
+ """
135
+ Need for pickling queryset
136
+
137
+ See https://github.com/MongoEngine/autonomous.db/issues/442
138
+ """
139
+
140
+ obj_dict = self.__dict__.copy()
141
+
142
+ # don't picke collection, instead pickle collection params
143
+ obj_dict.pop("_collection_obj")
144
+
145
+ # don't pickle cursor
146
+ obj_dict["_cursor_obj"] = None
147
+
148
+ return obj_dict
149
+
150
+ def __setstate__(self, obj_dict):
151
+ """
152
+ Need for pickling queryset
153
+
154
+ See https://github.com/MongoEngine/autonomous.db/issues/442
155
+ """
156
+
157
+ obj_dict["_collection_obj"] = obj_dict["_document"]._get_collection()
158
+
159
+ # update attributes
160
+ self.__dict__.update(obj_dict)
161
+
162
+ # forse load cursor
163
+ # self._cursor
164
+
165
+ def __getitem__(self, key):
166
+ """Return a document instance corresponding to a given index if
167
+ the key is an integer. If the key is a slice, translate its
168
+ bounds into a skip and a limit, and return a cloned queryset
169
+ with that skip/limit applied. For example:
170
+
171
+ >>> User.objects[0]
172
+ <User: User object>
173
+ >>> User.objects[1:3]
174
+ [<User: User object>, <User: User object>]
175
+ """
176
+ queryset = self.clone()
177
+ queryset._empty = False
178
+
179
+ # Handle a slice
180
+ if isinstance(key, slice):
181
+ queryset._cursor_obj = queryset._cursor[key]
182
+ queryset._skip, queryset._limit = key.start, key.stop
183
+ if key.start and key.stop:
184
+ queryset._limit = key.stop - key.start
185
+ if queryset._limit == 0:
186
+ queryset._empty = True
187
+
188
+ # Allow further QuerySet modifications to be performed
189
+ return queryset
190
+
191
+ # Handle an index
192
+ elif isinstance(key, int):
193
+ if queryset._scalar:
194
+ return queryset._get_scalar(
195
+ queryset._document._from_son(
196
+ queryset._cursor[key],
197
+ _auto_dereference=self._auto_dereference,
198
+ )
199
+ )
200
+
201
+ if queryset._as_pymongo:
202
+ return queryset._cursor[key]
203
+
204
+ return queryset._document._from_son(
205
+ queryset._cursor[key],
206
+ _auto_dereference=self._auto_dereference,
207
+ )
208
+
209
+ raise TypeError("Provide a slice or an integer index")
210
+
211
+ def __iter__(self):
212
+ raise NotImplementedError
213
+
214
+ def _has_data(self):
215
+ """Return True if cursor has any data."""
216
+ queryset = self.order_by()
217
+ return False if queryset.first() is None else True
218
+
219
+ def __bool__(self):
220
+ """Avoid to open all records in an if stmt in Py3."""
221
+ return self._has_data()
222
+
223
+ # Core functions
224
+
225
+ def all(self):
226
+ """Returns a copy of the current QuerySet."""
227
+ return self.__call__()
228
+
229
+ def filter(self, *q_objs, **query):
230
+ """An alias of :meth:`~autonomous.db.queryset.QuerySet.__call__`"""
231
+ return self.__call__(*q_objs, **query)
232
+
233
+ def search_text(self, text, language=None, text_score=True):
234
+ """
235
+ Start a text search, using text indexes.
236
+ Require: MongoDB server version 2.6+.
237
+
238
+ :param language: The language that determines the list of stop words
239
+ for the search and the rules for the stemmer and tokenizer.
240
+ If not specified, the search uses the default language of the index.
241
+ For supported languages, see
242
+ `Text Search Languages <https://docs.mongodb.org/manual/reference/text-search-languages/#text-search-languages>`.
243
+ :param text_score: True to have it return the text_score (available through get_text_score()), False to disable that
244
+ Note that unless you order the results, leaving text_score=True may provide randomness in the returned documents
245
+ """
246
+ queryset = self.clone()
247
+ if queryset._search_text:
248
+ raise OperationError("It is not possible to use search_text two times.")
249
+
250
+ query_kwargs = SON({"$search": text})
251
+ if language:
252
+ query_kwargs["$language"] = language
253
+
254
+ queryset._query_obj &= Q(__raw__={"$text": query_kwargs})
255
+ queryset._mongo_query = None
256
+ queryset._cursor_obj = None
257
+ queryset._search_text = text
258
+ queryset._search_text_score = text_score
259
+
260
+ return queryset
261
+
262
+ def get(self, *q_objs, **query):
263
+ """Retrieve the matching object raising
264
+ :class:`~autonomous.db.queryset.MultipleObjectsReturned` or
265
+ `DocumentName.MultipleObjectsReturned` exception if multiple results
266
+ and :class:`~autonomous.db.queryset.DoesNotExist` or
267
+ `DocumentName.DoesNotExist` if no results are found.
268
+ """
269
+ queryset = self.clone()
270
+ queryset = queryset.order_by().limit(2)
271
+ queryset = queryset.filter(*q_objs, **query)
272
+
273
+ try:
274
+ result = next(queryset)
275
+ except StopIteration:
276
+ msg = "%s matching query does not exist." % queryset._document._class_name
277
+ raise queryset._document.DoesNotExist(msg)
278
+
279
+ try:
280
+ # Check if there is another match
281
+ next(queryset)
282
+ except StopIteration:
283
+ return result
284
+
285
+ # If we were able to retrieve the 2nd doc, raise the MultipleObjectsReturned exception.
286
+ raise queryset._document.MultipleObjectsReturned(
287
+ "2 or more items returned, instead of 1"
288
+ )
289
+
290
+ def create(self, **kwargs):
291
+ """Create new object. Returns the saved object instance."""
292
+ return self._document(**kwargs).save(force_insert=True)
293
+
294
+ def first(self):
295
+ """Retrieve the first object matching the query."""
296
+ queryset = self.clone()
297
+ if self._none or self._empty:
298
+ return None
299
+
300
+ try:
301
+ result = queryset[0]
302
+ except IndexError:
303
+ result = None
304
+ return result
305
+
306
+ def insert(
307
+ self, doc_or_docs, load_bulk=True, write_concern=None, signal_kwargs=None
308
+ ):
309
+ """bulk insert documents
310
+
311
+ :param doc_or_docs: a document or list of documents to be inserted
312
+ :param load_bulk (optional): If True returns the list of document
313
+ instances
314
+ :param write_concern: Extra keyword arguments are passed down to
315
+ :meth:`~pymongo.collection.Collection.insert`
316
+ which will be used as options for the resultant
317
+ ``getLastError`` command. For example,
318
+ ``insert(..., {w: 2, fsync: True})`` will wait until at least
319
+ two servers have recorded the write and will force an fsync on
320
+ each server being written to.
321
+ :param signal_kwargs: (optional) kwargs dictionary to be passed to
322
+ the signal calls.
323
+
324
+ By default returns document instances, set ``load_bulk`` to False to
325
+ return just ``ObjectIds``
326
+ """
327
+ Document = _import_class("Document")
328
+
329
+ if write_concern is None:
330
+ write_concern = {}
331
+
332
+ docs = doc_or_docs
333
+ return_one = False
334
+ if isinstance(docs, Document) or issubclass(docs.__class__, Document):
335
+ return_one = True
336
+ docs = [docs]
337
+
338
+ for doc in docs:
339
+ if not isinstance(doc, self._document):
340
+ msg = "Some documents inserted aren't instances of %s" % str(
341
+ self._document
342
+ )
343
+ raise OperationError(msg)
344
+ if doc.pk and not doc._created:
345
+ msg = "Some documents have ObjectIds, use doc.update() instead"
346
+ raise OperationError(msg)
347
+
348
+ signal_kwargs = signal_kwargs or {}
349
+ signals.pre_bulk_insert.send(self._document, documents=docs, **signal_kwargs)
350
+
351
+ raw = [doc.to_mongo() for doc in docs]
352
+
353
+ with set_write_concern(self._collection, write_concern) as collection:
354
+ insert_func = collection.insert_many
355
+ if return_one:
356
+ raw = raw[0]
357
+ insert_func = collection.insert_one
358
+
359
+ try:
360
+ inserted_result = insert_func(raw)
361
+ ids = (
362
+ [inserted_result.inserted_id]
363
+ if return_one
364
+ else inserted_result.inserted_ids
365
+ )
366
+ except pymongo.errors.DuplicateKeyError as err:
367
+ message = "Could not save document (%s)"
368
+ raise NotUniqueError(message % err)
369
+ except pymongo.errors.BulkWriteError as err:
370
+ # inserting documents that already have an _id field will
371
+ # give huge performance debt or raise
372
+ message = "Bulk write error: (%s)"
373
+ raise BulkWriteError(message % err.details)
374
+ except pymongo.errors.OperationFailure as err:
375
+ message = "Could not save document (%s)"
376
+ if re.match("^E1100[01] duplicate key", str(err)):
377
+ # E11000 - duplicate key error index
378
+ # E11001 - duplicate key on update
379
+ message = "Tried to save duplicate unique keys (%s)"
380
+ raise NotUniqueError(message % err)
381
+ raise OperationError(message % err)
382
+
383
+ # Apply inserted_ids to documents
384
+ for doc, doc_id in zip(docs, ids):
385
+ doc.pk = doc_id
386
+
387
+ if not load_bulk:
388
+ signals.post_bulk_insert.send(
389
+ self._document, documents=docs, loaded=False, **signal_kwargs
390
+ )
391
+ return ids[0] if return_one else ids
392
+
393
+ documents = self.in_bulk(ids)
394
+ results = [documents.get(obj_id) for obj_id in ids]
395
+ signals.post_bulk_insert.send(
396
+ self._document, documents=results, loaded=True, **signal_kwargs
397
+ )
398
+ return results[0] if return_one else results
399
+
400
+ def count(self, with_limit_and_skip=False):
401
+ """Count the selected elements in the query.
402
+
403
+ :param with_limit_and_skip (optional): take any :meth:`limit` or
404
+ :meth:`skip` that has been applied to this cursor into account when
405
+ getting the count
406
+ """
407
+ # mimic the fact that setting .limit(0) in pymongo sets no limit
408
+ # https://www.mongodb.com/docs/manual/reference/method/cursor.limit/#zero-value
409
+ if (
410
+ self._limit == 0
411
+ and with_limit_and_skip is False
412
+ or self._none
413
+ or self._empty
414
+ ):
415
+ return 0
416
+
417
+ kwargs = (
418
+ {"limit": self._limit, "skip": self._skip} if with_limit_and_skip else {}
419
+ )
420
+
421
+ if self._limit == 0:
422
+ # mimic the fact that historically .limit(0) sets no limit
423
+ kwargs.pop("limit", None)
424
+
425
+ if self._hint not in (-1, None):
426
+ kwargs["hint"] = self._hint
427
+
428
+ if self._collation:
429
+ kwargs["collation"] = self._collation
430
+
431
+ count = count_documents(
432
+ collection=self._cursor.collection,
433
+ filter=self._query,
434
+ **kwargs,
435
+ )
436
+
437
+ self._cursor_obj = None
438
+ return count
439
+
440
+ def delete(self, write_concern=None, _from_doc_delete=False, cascade_refs=None):
441
+ """Delete the documents matched by the query.
442
+
443
+ :param write_concern: Extra keyword arguments are passed down which
444
+ will be used as options for the resultant
445
+ ``getLastError`` command. For example,
446
+ ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
447
+ wait until at least two servers have recorded the write and
448
+ will force an fsync on the primary server.
449
+ :param _from_doc_delete: True when called from document delete therefore
450
+ signals will have been triggered so don't loop.
451
+
452
+ :returns number of deleted documents
453
+ """
454
+ queryset = self.clone()
455
+ doc = queryset._document
456
+
457
+ if write_concern is None:
458
+ write_concern = {}
459
+
460
+ # Handle deletes where skips or limits have been applied or
461
+ # there is an untriggered delete signal
462
+ has_delete_signal = signals.signals_available and (
463
+ signals.pre_delete.has_receivers_for(doc)
464
+ or signals.post_delete.has_receivers_for(doc)
465
+ )
466
+
467
+ call_document_delete = (
468
+ queryset._skip or queryset._limit or has_delete_signal
469
+ ) and not _from_doc_delete
470
+
471
+ if call_document_delete:
472
+ cnt = 0
473
+ for doc in queryset:
474
+ doc.delete(**write_concern)
475
+ cnt += 1
476
+ return cnt
477
+
478
+ delete_rules = doc._meta.get("delete_rules") or {}
479
+ delete_rules = list(delete_rules.items())
480
+
481
+ # Check for DENY rules before actually deleting/nullifying any other
482
+ # references
483
+ for rule_entry, rule in delete_rules:
484
+ document_cls, field_name = rule_entry
485
+ if document_cls._meta.get("abstract"):
486
+ continue
487
+
488
+ if rule == DENY:
489
+ refs = document_cls.objects(**{field_name + "__in": self})
490
+ if refs.limit(1).count() > 0:
491
+ raise OperationError(
492
+ "Could not delete document (%s.%s refers to it)"
493
+ % (document_cls.__name__, field_name)
494
+ )
495
+
496
+ # Check all the other rules
497
+ for rule_entry, rule in delete_rules:
498
+ document_cls, field_name = rule_entry
499
+ if document_cls._meta.get("abstract"):
500
+ continue
501
+
502
+ if rule == CASCADE:
503
+ cascade_refs = set() if cascade_refs is None else cascade_refs
504
+ # Handle recursive reference
505
+ if doc._collection == document_cls._collection:
506
+ for ref in queryset:
507
+ cascade_refs.add(ref.id)
508
+ refs = document_cls.objects(
509
+ **{field_name + "__in": self, "pk__nin": cascade_refs}
510
+ )
511
+ if refs.count() > 0:
512
+ refs.delete(write_concern=write_concern, cascade_refs=cascade_refs)
513
+ elif rule == NULLIFY:
514
+ document_cls.objects(**{field_name + "__in": self}).update(
515
+ write_concern=write_concern, **{"unset__%s" % field_name: 1}
516
+ )
517
+ elif rule == PULL:
518
+ document_cls.objects(**{field_name + "__in": self}).update(
519
+ write_concern=write_concern, **{"pull_all__%s" % field_name: self}
520
+ )
521
+
522
+ with set_write_concern(queryset._collection, write_concern) as collection:
523
+ result = collection.delete_many(queryset._query)
524
+
525
+ # If we're using an unack'd write concern, we don't really know how
526
+ # many items have been deleted at this point, hence we only return
527
+ # the count for ack'd ops.
528
+ if result.acknowledged:
529
+ return result.deleted_count
530
+
531
+ def update(
532
+ self,
533
+ upsert=False,
534
+ multi=True,
535
+ write_concern=None,
536
+ read_concern=None,
537
+ full_result=False,
538
+ array_filters=None,
539
+ **update,
540
+ ):
541
+ """Perform an atomic update on the fields matched by the query.
542
+
543
+ :param upsert: insert if document doesn't exist (default ``False``)
544
+ :param multi: Update multiple documents.
545
+ :param write_concern: Extra keyword arguments are passed down which
546
+ will be used as options for the resultant
547
+ ``getLastError`` command. For example,
548
+ ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
549
+ wait until at least two servers have recorded the write and
550
+ will force an fsync on the primary server.
551
+ :param read_concern: Override the read concern for the operation
552
+ :param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number
553
+ updated items
554
+ :param array_filters: A list of filters specifying which array elements an update should apply.
555
+ :param update: Django-style update keyword arguments
556
+
557
+ :returns the number of updated documents (unless ``full_result`` is True)
558
+ """
559
+ if not update and not upsert:
560
+ raise OperationError("No update parameters, would remove data")
561
+
562
+ if write_concern is None:
563
+ write_concern = {}
564
+ if self._none or self._empty:
565
+ return 0
566
+
567
+ queryset = self.clone()
568
+ query = queryset._query
569
+ if "__raw__" in update and isinstance(
570
+ update["__raw__"], list
571
+ ): # Case of Update with Aggregation Pipeline
572
+ update = [
573
+ transform.update(queryset._document, **{"__raw__": u})
574
+ for u in update["__raw__"]
575
+ ]
576
+ else:
577
+ update = transform.update(queryset._document, **update)
578
+ # If doing an atomic upsert on an inheritable class
579
+ # then ensure we add _cls to the update operation
580
+ if upsert and "_cls" in query:
581
+ if "$set" in update:
582
+ update["$set"]["_cls"] = queryset._document._class_name
583
+ else:
584
+ update["$set"] = {"_cls": queryset._document._class_name}
585
+ try:
586
+ with set_read_write_concern(
587
+ queryset._collection, write_concern, read_concern
588
+ ) as collection:
589
+ update_func = collection.update_one
590
+ if multi:
591
+ update_func = collection.update_many
592
+ result = update_func(
593
+ query, update, upsert=upsert, array_filters=array_filters
594
+ )
595
+ if full_result:
596
+ return result
597
+ elif result.raw_result:
598
+ return result.raw_result["n"]
599
+ except pymongo.errors.DuplicateKeyError as err:
600
+ raise NotUniqueError("Update failed (%s)" % err)
601
+ except pymongo.errors.OperationFailure as err:
602
+ if str(err) == "multi not coded yet":
603
+ message = "update() method requires MongoDB 1.1.3+"
604
+ raise OperationError(message)
605
+ raise OperationError("Update failed (%s)" % err)
606
+
607
+ def upsert_one(self, write_concern=None, read_concern=None, **update):
608
+ """Overwrite or add the first document matched by the query.
609
+
610
+ :param write_concern: Extra keyword arguments are passed down which
611
+ will be used as options for the resultant
612
+ ``getLastError`` command. For example,
613
+ ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
614
+ wait until at least two servers have recorded the write and
615
+ will force an fsync on the primary server.
616
+ :param read_concern: Override the read concern for the operation
617
+ :param update: Django-style update keyword arguments
618
+
619
+ :returns the new or overwritten document
620
+ """
621
+
622
+ atomic_update = self.update(
623
+ multi=False,
624
+ upsert=True,
625
+ write_concern=write_concern,
626
+ read_concern=read_concern,
627
+ full_result=True,
628
+ **update,
629
+ )
630
+
631
+ if atomic_update.raw_result["updatedExisting"]:
632
+ document = self.get()
633
+ else:
634
+ document = self._document.objects.with_id(atomic_update.upserted_id)
635
+ return document
636
+
637
+ def update_one(
638
+ self,
639
+ upsert=False,
640
+ write_concern=None,
641
+ full_result=False,
642
+ array_filters=None,
643
+ **update,
644
+ ):
645
+ """Perform an atomic update on the fields of the first document
646
+ matched by the query.
647
+
648
+ :param upsert: insert if document doesn't exist (default ``False``)
649
+ :param write_concern: Extra keyword arguments are passed down which
650
+ will be used as options for the resultant
651
+ ``getLastError`` command. For example,
652
+ ``save(..., write_concern={w: 2, fsync: True}, ...)`` will
653
+ wait until at least two servers have recorded the write and
654
+ will force an fsync on the primary server.
655
+ :param full_result: Return the associated ``pymongo.UpdateResult`` rather than just the number
656
+ updated items
657
+ :param array_filters: A list of filters specifying which array elements an update should apply.
658
+ :param update: Django-style update keyword arguments
659
+ full_result
660
+ :returns the number of updated documents (unless ``full_result`` is True)
661
+ """
662
+ return self.update(
663
+ upsert=upsert,
664
+ multi=False,
665
+ write_concern=write_concern,
666
+ full_result=full_result,
667
+ array_filters=array_filters,
668
+ **update,
669
+ )
670
+
671
+ def modify(
672
+ self,
673
+ upsert=False,
674
+ full_response=False,
675
+ remove=False,
676
+ new=False,
677
+ array_filters=None,
678
+ **update,
679
+ ):
680
+ """Update and return the updated document.
681
+
682
+ Returns either the document before or after modification based on `new`
683
+ parameter. If no documents match the query and `upsert` is false,
684
+ returns ``None``. If upserting and `new` is false, returns ``None``.
685
+
686
+ If the full_response parameter is ``True``, the return value will be
687
+ the entire response object from the server, including the 'ok' and
688
+ 'lastErrorObject' fields, rather than just the modified document.
689
+ This is useful mainly because the 'lastErrorObject' document holds
690
+ information about the command's execution.
691
+
692
+ :param upsert: insert if document doesn't exist (default ``False``)
693
+ :param full_response: return the entire response object from the
694
+ server (default ``False``, not available for PyMongo 3+)
695
+ :param remove: remove rather than updating (default ``False``)
696
+ :param new: return updated rather than original document
697
+ (default ``False``)
698
+ :param array_filters: A list of filters specifying which array elements an update should apply.
699
+ :param update: Django-style update keyword arguments
700
+ """
701
+
702
+ if remove and new:
703
+ raise OperationError("Conflicting parameters: remove and new")
704
+
705
+ if not update and not upsert and not remove:
706
+ raise OperationError("No update parameters, must either update or remove")
707
+
708
+ if self._none or self._empty:
709
+ return None
710
+
711
+ queryset = self.clone()
712
+ query = queryset._query
713
+ if not remove:
714
+ update = transform.update(queryset._document, **update)
715
+ sort = queryset._ordering
716
+
717
+ try:
718
+ if full_response:
719
+ msg = "With PyMongo 3+, it is not possible anymore to get the full response."
720
+ warnings.warn(msg, DeprecationWarning)
721
+ if remove:
722
+ result = queryset._collection.find_one_and_delete(
723
+ query, sort=sort, **self._cursor_args
724
+ )
725
+ else:
726
+ if new:
727
+ return_doc = ReturnDocument.AFTER
728
+ else:
729
+ return_doc = ReturnDocument.BEFORE
730
+ result = queryset._collection.find_one_and_update(
731
+ query,
732
+ update,
733
+ upsert=upsert,
734
+ sort=sort,
735
+ return_document=return_doc,
736
+ array_filters=array_filters,
737
+ **self._cursor_args,
738
+ )
739
+ except pymongo.errors.DuplicateKeyError as err:
740
+ raise NotUniqueError("Update failed (%s)" % err)
741
+ except pymongo.errors.OperationFailure as err:
742
+ raise OperationError("Update failed (%s)" % err)
743
+
744
+ if full_response:
745
+ if result["value"] is not None:
746
+ result["value"] = self._document._from_son(result["value"])
747
+ else:
748
+ if result is not None:
749
+ result = self._document._from_son(result)
750
+
751
+ return result
752
+
753
+ def with_id(self, object_id):
754
+ """Retrieve the object matching the id provided. Uses `object_id` only
755
+ and raises InvalidQueryError if a filter has been applied. Returns
756
+ `None` if no document exists with that id.
757
+
758
+ :param object_id: the value for the id of the document to look up
759
+ """
760
+ queryset = self.clone()
761
+ if queryset._query_obj:
762
+ msg = "Cannot use a filter whilst using `with_id`"
763
+ raise InvalidQueryError(msg)
764
+ return queryset.filter(pk=object_id).first()
765
+
766
+ def in_bulk(self, object_ids):
767
+ """Retrieve a set of documents by their ids.
768
+
769
+ :param object_ids: a list or tuple of ObjectId's
770
+ :rtype: dict of ObjectId's as keys and collection-specific
771
+ Document subclasses as values.
772
+ """
773
+ doc_map = {}
774
+
775
+ docs = self._collection.find({"_id": {"$in": object_ids}}, **self._cursor_args)
776
+ if self._scalar:
777
+ for doc in docs:
778
+ doc_map[doc["_id"]] = self._get_scalar(self._document._from_son(doc))
779
+ elif self._as_pymongo:
780
+ for doc in docs:
781
+ doc_map[doc["_id"]] = doc
782
+ else:
783
+ for doc in docs:
784
+ doc_map[doc["_id"]] = self._document._from_son(
785
+ doc,
786
+ _auto_dereference=self._auto_dereference,
787
+ )
788
+
789
+ return doc_map
790
+
791
+ def none(self):
792
+ """Returns a queryset that never returns any objects and no query will be executed when accessing the results
793
+ inspired by django none() https://docs.djangoproject.com/en/dev/ref/models/querysets/#none
794
+ """
795
+ queryset = self.clone()
796
+ queryset._none = True
797
+ return queryset
798
+
799
+ def no_sub_classes(self):
800
+ """Filter for only the instances of this specific document.
801
+
802
+ Do NOT return any inherited documents.
803
+ """
804
+ if self._document._meta.get("allow_inheritance") is True:
805
+ self._cls_query = {"_cls": self._document._class_name}
806
+
807
+ return self
808
+
809
+ def using(self, alias):
810
+ """This method is for controlling which database the QuerySet will be
811
+ evaluated against if you are using more than one database.
812
+
813
+ :param alias: The database alias
814
+ """
815
+
816
+ with switch_db(self._document, alias) as cls:
817
+ collection = cls._get_collection()
818
+
819
+ return self._clone_into(self.__class__(self._document, collection))
820
+
821
+ def clone(self):
822
+ """Create a copy of the current queryset."""
823
+ return self._clone_into(self.__class__(self._document, self._collection_obj))
824
+
825
+ def _clone_into(self, new_qs):
826
+ """Copy all the relevant properties of this queryset to
827
+ a new queryset (which has to be an instance of
828
+ :class:`~autonomous.db.queryset.base.BaseQuerySet`).
829
+ """
830
+ if not isinstance(new_qs, BaseQuerySet):
831
+ raise OperationError(
832
+ "%s is not a subclass of BaseQuerySet" % new_qs.__name__
833
+ )
834
+
835
+ copy_props = (
836
+ "_mongo_query",
837
+ "_cls_query",
838
+ "_none",
839
+ "_query_obj",
840
+ "_where_clause",
841
+ "_loaded_fields",
842
+ "_ordering",
843
+ "_snapshot",
844
+ "_timeout",
845
+ "_allow_disk_use",
846
+ "_read_preference",
847
+ "_read_concern",
848
+ "_iter",
849
+ "_scalar",
850
+ "_as_pymongo",
851
+ "_limit",
852
+ "_skip",
853
+ "_empty",
854
+ "_hint",
855
+ "_collation",
856
+ "_search_text",
857
+ "_search_text_score",
858
+ "_max_time_ms",
859
+ "_comment",
860
+ "_batch_size",
861
+ )
862
+
863
+ for prop in copy_props:
864
+ val = getattr(self, prop)
865
+ setattr(new_qs, prop, copy.copy(val))
866
+
867
+ new_qs.__auto_dereference = self._BaseQuerySet__auto_dereference
868
+
869
+ if self._cursor_obj:
870
+ new_qs._cursor_obj = self._cursor_obj.clone()
871
+
872
+ return new_qs
873
+
874
+ def select_related(self, max_depth=1):
875
+ """Handles dereferencing of :class:`~bson.dbref.DBRef` objects or
876
+ :class:`~bson.object_id.ObjectId` a maximum depth in order to cut down
877
+ the number queries to mongodb.
878
+ """
879
+ # Make select related work the same for querysets
880
+ max_depth += 1
881
+ queryset = self.clone()
882
+ return queryset._dereference(queryset, max_depth=max_depth)
883
+
884
+ def limit(self, n):
885
+ """Limit the number of returned documents to `n`. This may also be
886
+ achieved using array-slicing syntax (e.g. ``User.objects[:5]``).
887
+
888
+ :param n: the maximum number of objects to return if n is greater than 0.
889
+ When 0 is passed, returns all the documents in the cursor
890
+ """
891
+ queryset = self.clone()
892
+ queryset._limit = n
893
+ queryset._empty = False # cancels the effect of empty
894
+
895
+ # If a cursor object has already been created, apply the limit to it.
896
+ if queryset._cursor_obj:
897
+ queryset._cursor_obj.limit(queryset._limit)
898
+
899
+ return queryset
900
+
901
+ def skip(self, n):
902
+ """Skip `n` documents before returning the results. This may also be
903
+ achieved using array-slicing syntax (e.g. ``User.objects[5:]``).
904
+
905
+ :param n: the number of objects to skip before returning results
906
+ """
907
+ queryset = self.clone()
908
+ queryset._skip = n
909
+
910
+ # If a cursor object has already been created, apply the skip to it.
911
+ if queryset._cursor_obj:
912
+ queryset._cursor_obj.skip(queryset._skip)
913
+
914
+ return queryset
915
+
916
+ def hint(self, index=None):
917
+ """Added 'hint' support, telling Mongo the proper index to use for the
918
+ query.
919
+
920
+ Judicious use of hints can greatly improve query performance. When
921
+ doing a query on multiple fields (at least one of which is indexed)
922
+ pass the indexed field as a hint to the query.
923
+
924
+ Hinting will not do anything if the corresponding index does not exist.
925
+ The last hint applied to this cursor takes precedence over all others.
926
+ """
927
+ queryset = self.clone()
928
+ queryset._hint = index
929
+
930
+ # If a cursor object has already been created, apply the hint to it.
931
+ if queryset._cursor_obj:
932
+ queryset._cursor_obj.hint(queryset._hint)
933
+
934
+ return queryset
935
+
936
+ def collation(self, collation=None):
937
+ """
938
+ Collation allows users to specify language-specific rules for string
939
+ comparison, such as rules for lettercase and accent marks.
940
+ :param collation: `~pymongo.collation.Collation` or dict with
941
+ following fields:
942
+ {
943
+ locale: str,
944
+ caseLevel: bool,
945
+ caseFirst: str,
946
+ strength: int,
947
+ numericOrdering: bool,
948
+ alternate: str,
949
+ maxVariable: str,
950
+ backwards: str
951
+ }
952
+ Collation should be added to indexes like in test example
953
+ """
954
+ queryset = self.clone()
955
+ queryset._collation = collation
956
+
957
+ if queryset._cursor_obj:
958
+ queryset._cursor_obj.collation(collation)
959
+
960
+ return queryset
961
+
962
+ def batch_size(self, size):
963
+ """Limit the number of documents returned in a single batch (each
964
+ batch requires a round trip to the server).
965
+
966
+ See https://pymongo.readthedocs.io/en/stable/api/pymongo/cursor.html#pymongo.cursor.Cursor
967
+ for details.
968
+
969
+ :param size: desired size of each batch.
970
+ """
971
+ queryset = self.clone()
972
+ queryset._batch_size = size
973
+
974
+ # If a cursor object has already been created, apply the batch size to it.
975
+ if queryset._cursor_obj:
976
+ queryset._cursor_obj.batch_size(queryset._batch_size)
977
+
978
+ return queryset
979
+
980
+ def distinct(self, field):
981
+ """Return a list of distinct values for a given field.
982
+
983
+ :param field: the field to select distinct values from
984
+
985
+ .. note:: This is a command and won't take ordering or limit into
986
+ account.
987
+ """
988
+ queryset = self.clone()
989
+
990
+ try:
991
+ field = self._fields_to_dbfields([field]).pop()
992
+ except LookUpError:
993
+ pass
994
+
995
+ raw_values = queryset._cursor.distinct(field)
996
+ if not self._auto_dereference:
997
+ return raw_values
998
+
999
+ distinct = self._dereference(raw_values, 1, name=field, instance=self._document)
1000
+
1001
+ doc_field = self._document._fields.get(field.split(".", 1)[0])
1002
+ instance = None
1003
+
1004
+ # We may need to cast to the correct type eg. ListField(EmbeddedDocumentField)
1005
+ EmbeddedDocumentField = _import_class("EmbeddedDocumentField")
1006
+ ListField = _import_class("ListField")
1007
+ GenericEmbeddedDocumentField = _import_class("GenericEmbeddedDocumentField")
1008
+ if isinstance(doc_field, ListField):
1009
+ doc_field = getattr(doc_field, "field", doc_field)
1010
+ if isinstance(doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)):
1011
+ instance = getattr(doc_field, "document_type", None)
1012
+
1013
+ # handle distinct on subdocuments
1014
+ if "." in field:
1015
+ for field_part in field.split(".")[1:]:
1016
+ # if looping on embedded document, get the document type instance
1017
+ if instance and isinstance(
1018
+ doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)
1019
+ ):
1020
+ doc_field = instance
1021
+ # now get the subdocument
1022
+ doc_field = getattr(doc_field, field_part, doc_field)
1023
+ # We may need to cast to the correct type eg. ListField(EmbeddedDocumentField)
1024
+ if isinstance(doc_field, ListField):
1025
+ doc_field = getattr(doc_field, "field", doc_field)
1026
+ if isinstance(
1027
+ doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)
1028
+ ):
1029
+ instance = getattr(doc_field, "document_type", None)
1030
+
1031
+ if instance and isinstance(
1032
+ doc_field, (EmbeddedDocumentField, GenericEmbeddedDocumentField)
1033
+ ):
1034
+ distinct = [instance(**doc) for doc in distinct]
1035
+
1036
+ return distinct
1037
+
1038
+ def only(self, *fields):
1039
+ """Load only a subset of this document's fields. ::
1040
+
1041
+ post = BlogPost.objects(...).only('title', 'author.name')
1042
+
1043
+ .. note :: `only()` is chainable and will perform a union ::
1044
+ So with the following it will fetch both: `title` and `author.name`::
1045
+
1046
+ post = BlogPost.objects.only('title').only('author.name')
1047
+
1048
+ :func:`~autonomous.db.queryset.QuerySet.all_fields` will reset any
1049
+ field filters.
1050
+
1051
+ :param fields: fields to include
1052
+ """
1053
+ fields = {f: QueryFieldList.ONLY for f in fields}
1054
+ return self.fields(True, **fields)
1055
+
1056
+ def exclude(self, *fields):
1057
+ """Opposite to .only(), exclude some document's fields. ::
1058
+
1059
+ post = BlogPost.objects(...).exclude('comments')
1060
+
1061
+ .. note :: `exclude()` is chainable and will perform a union ::
1062
+ So with the following it will exclude both: `title` and `author.name`::
1063
+
1064
+ post = BlogPost.objects.exclude('title').exclude('author.name')
1065
+
1066
+ :func:`~autonomous.db.queryset.QuerySet.all_fields` will reset any
1067
+ field filters.
1068
+
1069
+ :param fields: fields to exclude
1070
+ """
1071
+ fields = {f: QueryFieldList.EXCLUDE for f in fields}
1072
+ return self.fields(**fields)
1073
+
1074
+ def fields(self, _only_called=False, **kwargs):
1075
+ """Manipulate how you load this document's fields. Used by `.only()`
1076
+ and `.exclude()` to manipulate which fields to retrieve. If called
1077
+ directly, use a set of kwargs similar to the MongoDB projection
1078
+ document. For example:
1079
+
1080
+ Include only a subset of fields:
1081
+
1082
+ posts = BlogPost.objects(...).fields(author=1, title=1)
1083
+
1084
+ Exclude a specific field:
1085
+
1086
+ posts = BlogPost.objects(...).fields(comments=0)
1087
+
1088
+ To retrieve a subrange or sublist of array elements,
1089
+ support exist for both the `slice` and `elemMatch` projection operator:
1090
+
1091
+ posts = BlogPost.objects(...).fields(slice__comments=5)
1092
+ posts = BlogPost.objects(...).fields(elemMatch__comments="test")
1093
+
1094
+ :param kwargs: A set of keyword arguments identifying what to
1095
+ include, exclude, or slice.
1096
+ """
1097
+
1098
+ # Check for an operator and transform to mongo-style if there is
1099
+ operators = ["slice", "elemMatch"]
1100
+ cleaned_fields = []
1101
+ for key, value in kwargs.items():
1102
+ parts = key.split("__")
1103
+ if parts[0] in operators:
1104
+ op = parts.pop(0)
1105
+ value = {"$" + op: value}
1106
+ key = ".".join(parts)
1107
+ cleaned_fields.append((key, value))
1108
+
1109
+ # Sort fields by their values, explicitly excluded fields first, then
1110
+ # explicitly included, and then more complicated operators such as
1111
+ # $slice.
1112
+ def _sort_key(field_tuple):
1113
+ _, value = field_tuple
1114
+ if isinstance(value, int):
1115
+ return value # 0 for exclusion, 1 for inclusion
1116
+ return 2 # so that complex values appear last
1117
+
1118
+ fields = sorted(cleaned_fields, key=_sort_key)
1119
+
1120
+ # Clone the queryset, group all fields by their value, convert
1121
+ # each of them to db_fields, and set the queryset's _loaded_fields
1122
+ queryset = self.clone()
1123
+ for value, group in itertools.groupby(fields, lambda x: x[1]):
1124
+ fields = [field for field, value in group]
1125
+ fields = queryset._fields_to_dbfields(fields)
1126
+ queryset._loaded_fields += QueryFieldList(
1127
+ fields, value=value, _only_called=_only_called
1128
+ )
1129
+
1130
+ return queryset
1131
+
1132
+ def all_fields(self):
1133
+ """Include all fields. Reset all previously calls of .only() or
1134
+ .exclude(). ::
1135
+
1136
+ post = BlogPost.objects.exclude('comments').all_fields()
1137
+ """
1138
+ queryset = self.clone()
1139
+ queryset._loaded_fields = QueryFieldList(
1140
+ always_include=queryset._loaded_fields.always_include
1141
+ )
1142
+ return queryset
1143
+
1144
+ def order_by(self, *keys, __raw__=None):
1145
+ """Order the :class:`~autonomous.db.queryset.QuerySet` by the given keys.
1146
+
1147
+ The order may be specified by prepending each of the keys by a "+" or
1148
+ a "-". Ascending order is assumed if there's no prefix.
1149
+
1150
+ If no keys are passed, existing ordering is cleared instead.
1151
+
1152
+ :param keys: fields to order the query results by; keys may be
1153
+ prefixed with "+" or a "-" to determine the ordering direction.
1154
+ :param __raw__: a raw pymongo "sort" argument (provided as a list of (key, direction))
1155
+ see 'key_or_list' in `pymongo.cursor.Cursor.sort doc <https://pymongo.readthedocs.io/en/stable/api/pymongo/cursor.html#pymongo.cursor.Cursor.sort>`.
1156
+ If both keys and __raw__ are provided, an exception is raised
1157
+ """
1158
+ if __raw__ and keys:
1159
+ raise OperationError("Can not use both keys and __raw__ with order_by() ")
1160
+
1161
+ queryset = self.clone()
1162
+ old_ordering = queryset._ordering
1163
+ if __raw__:
1164
+ new_ordering = __raw__
1165
+ else:
1166
+ new_ordering = queryset._get_order_by(keys)
1167
+
1168
+ if queryset._cursor_obj:
1169
+ # If a cursor object has already been created, apply the sort to it
1170
+ if new_ordering:
1171
+ queryset._cursor_obj.sort(new_ordering)
1172
+
1173
+ # If we're trying to clear a previous explicit ordering, we need
1174
+ # to clear the cursor entirely (because PyMongo doesn't allow
1175
+ # clearing an existing sort on a cursor).
1176
+ elif old_ordering:
1177
+ queryset._cursor_obj = None
1178
+
1179
+ queryset._ordering = new_ordering
1180
+
1181
+ return queryset
1182
+
1183
+ def clear_cls_query(self):
1184
+ """Clear the default "_cls" query.
1185
+
1186
+ By default, all queries generated for documents that allow inheritance
1187
+ include an extra "_cls" clause. In most cases this is desirable, but
1188
+ sometimes you might achieve better performance if you clear that
1189
+ default query.
1190
+
1191
+ Scan the code for `_cls_query` to get more details.
1192
+ """
1193
+ queryset = self.clone()
1194
+ queryset._cls_query = {}
1195
+ return queryset
1196
+
1197
+ def comment(self, text):
1198
+ """Add a comment to the query.
1199
+
1200
+ See https://www.mongodb.com/docs/manual/reference/method/cursor.comment/
1201
+ for details.
1202
+ """
1203
+ return self._chainable_method("comment", text)
1204
+
1205
+ def explain(self):
1206
+ """Return an explain plan record for the
1207
+ :class:`~autonomous.db.queryset.QuerySet` cursor.
1208
+ """
1209
+ return self._cursor.explain()
1210
+
1211
+ # DEPRECATED. Has no more impact on PyMongo 3+
1212
+ def snapshot(self, enabled):
1213
+ """Enable or disable snapshot mode when querying.
1214
+
1215
+ :param enabled: whether or not snapshot mode is enabled
1216
+ """
1217
+ msg = "snapshot is deprecated as it has no impact when using PyMongo 3+."
1218
+ warnings.warn(msg, DeprecationWarning)
1219
+ queryset = self.clone()
1220
+ queryset._snapshot = enabled
1221
+ return queryset
1222
+
1223
+ def allow_disk_use(self, enabled):
1224
+ """Enable or disable the use of temporary files on disk while processing a blocking sort operation.
1225
+ (To store data exceeding the 100 megabyte system memory limit)
1226
+
1227
+ :param enabled: whether or not temporary files on disk are used
1228
+ """
1229
+ queryset = self.clone()
1230
+ queryset._allow_disk_use = enabled
1231
+ return queryset
1232
+
1233
+ def timeout(self, enabled):
1234
+ """Enable or disable the default mongod timeout when querying. (no_cursor_timeout option)
1235
+
1236
+ :param enabled: whether or not the timeout is used
1237
+ """
1238
+ queryset = self.clone()
1239
+ queryset._timeout = enabled
1240
+ return queryset
1241
+
1242
+ def read_preference(self, read_preference):
1243
+ """Change the read_preference when querying.
1244
+
1245
+ :param read_preference: override ReplicaSetConnection-level
1246
+ preference.
1247
+ """
1248
+ validate_read_preference("read_preference", read_preference)
1249
+ queryset = self.clone()
1250
+ queryset._read_preference = read_preference
1251
+ queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_preference
1252
+ return queryset
1253
+
1254
+ def read_concern(self, read_concern):
1255
+ """Change the read_concern when querying.
1256
+
1257
+ :param read_concern: override ReplicaSetConnection-level
1258
+ preference.
1259
+ """
1260
+ if read_concern is not None and not isinstance(read_concern, Mapping):
1261
+ raise TypeError(f"{read_concern!r} is not a valid read concern.")
1262
+
1263
+ queryset = self.clone()
1264
+ queryset._read_concern = (
1265
+ ReadConcern(**read_concern) if read_concern is not None else None
1266
+ )
1267
+ queryset._cursor_obj = None # we need to re-create the cursor object whenever we apply read_concern
1268
+ return queryset
1269
+
1270
+ def scalar(self, *fields):
1271
+ """Instead of returning Document instances, return either a specific
1272
+ value or a tuple of values in order.
1273
+
1274
+ Can be used along with
1275
+ :func:`~autonomous.db.queryset.QuerySet.no_dereference` to turn off
1276
+ dereferencing.
1277
+
1278
+ .. note:: This effects all results and can be unset by calling
1279
+ ``scalar`` without arguments. Calls ``only`` automatically.
1280
+
1281
+ :param fields: One or more fields to return instead of a Document.
1282
+ """
1283
+ queryset = self.clone()
1284
+ queryset._scalar = list(fields)
1285
+
1286
+ if fields:
1287
+ queryset = queryset.only(*fields)
1288
+ else:
1289
+ queryset = queryset.all_fields()
1290
+
1291
+ return queryset
1292
+
1293
+ def values_list(self, *fields):
1294
+ """An alias for scalar"""
1295
+ return self.scalar(*fields)
1296
+
1297
+ def as_pymongo(self):
1298
+ """Instead of returning Document instances, return raw values from
1299
+ pymongo.
1300
+
1301
+ This method is particularly useful if you don't need dereferencing
1302
+ and care primarily about the speed of data retrieval.
1303
+ """
1304
+ queryset = self.clone()
1305
+ queryset._as_pymongo = True
1306
+ return queryset
1307
+
1308
+ def max_time_ms(self, ms):
1309
+ """Wait `ms` milliseconds before killing the query on the server
1310
+
1311
+ :param ms: the number of milliseconds before killing the query on the server
1312
+ """
1313
+ return self._chainable_method("max_time_ms", ms)
1314
+
1315
+ # JSON Helpers
1316
+
1317
+ def to_json(self, *args, **kwargs):
1318
+ """Converts a queryset to JSON"""
1319
+ if "json_options" not in kwargs:
1320
+ warnings.warn(
1321
+ "No 'json_options' are specified! Falling back to "
1322
+ "LEGACY_JSON_OPTIONS with uuid_representation=PYTHON_LEGACY. "
1323
+ "For use with other MongoDB drivers specify the UUID "
1324
+ "representation to use. This will be changed to "
1325
+ "uuid_representation=UNSPECIFIED in a future release.",
1326
+ DeprecationWarning,
1327
+ )
1328
+ kwargs["json_options"] = LEGACY_JSON_OPTIONS
1329
+ return json_util.dumps(self.as_pymongo(), *args, **kwargs)
1330
+
1331
+ def from_json(self, json_data):
1332
+ """Converts json data to unsaved objects"""
1333
+ son_data = json_util.loads(json_data)
1334
+ return [self._document._from_son(data) for data in son_data]
1335
+
1336
+ def aggregate(self, pipeline, *suppl_pipeline, **kwargs):
1337
+ """Perform an aggregate function based on your queryset params
1338
+
1339
+ :param pipeline: list of aggregation commands,
1340
+ see: https://www.mongodb.com/docs/manual/core/aggregation-pipeline/
1341
+ :param suppl_pipeline: unpacked list of pipeline (added to support deprecation of the old interface)
1342
+ parameter will be removed shortly
1343
+ :param kwargs: (optional) kwargs dictionary to be passed to pymongo's aggregate call
1344
+ See https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.aggregate
1345
+ """
1346
+ using_deprecated_interface = isinstance(pipeline, dict) or bool(suppl_pipeline)
1347
+ user_pipeline = [pipeline] if isinstance(pipeline, dict) else list(pipeline)
1348
+
1349
+ if using_deprecated_interface:
1350
+ msg = "Calling .aggregate() with un unpacked list (*pipeline) is deprecated, it will soon change and will expect a list (similar to pymongo.Collection.aggregate interface), see documentation"
1351
+ warnings.warn(msg, DeprecationWarning)
1352
+
1353
+ user_pipeline += suppl_pipeline
1354
+
1355
+ initial_pipeline = []
1356
+ if self._none or self._empty:
1357
+ initial_pipeline.append({"$limit": 1})
1358
+ initial_pipeline.append({"$match": {"$expr": False}})
1359
+
1360
+ if self._query:
1361
+ initial_pipeline.append({"$match": self._query})
1362
+
1363
+ if self._ordering:
1364
+ initial_pipeline.append({"$sort": dict(self._ordering)})
1365
+
1366
+ if self._limit is not None:
1367
+ # As per MongoDB Documentation (https://www.mongodb.com/docs/manual/reference/operator/aggregation/limit/),
1368
+ # keeping limit stage right after sort stage is more efficient. But this leads to wrong set of documents
1369
+ # for a skip stage that might succeed these. So we need to maintain more documents in memory in such a
1370
+ # case (https://stackoverflow.com/a/24161461).
1371
+ initial_pipeline.append({"$limit": self._limit + (self._skip or 0)})
1372
+
1373
+ if self._skip is not None:
1374
+ initial_pipeline.append({"$skip": self._skip})
1375
+
1376
+ final_pipeline = initial_pipeline + user_pipeline
1377
+
1378
+ collection = self._collection
1379
+ if self._read_preference is not None or self._read_concern is not None:
1380
+ collection = self._collection.with_options(
1381
+ read_preference=self._read_preference, read_concern=self._read_concern
1382
+ )
1383
+
1384
+ return collection.aggregate(final_pipeline, cursor={}, **kwargs)
1385
+
1386
+ # JS functionality
1387
+ def map_reduce(
1388
+ self, map_f, reduce_f, output, finalize_f=None, limit=None, scope=None
1389
+ ):
1390
+ """Perform a map/reduce query using the current query spec
1391
+ and ordering. While ``map_reduce`` respects ``QuerySet`` chaining,
1392
+ it must be the last call made, as it does not return a maleable
1393
+ ``QuerySet``.
1394
+
1395
+ See the :meth:`~autonomous.db.tests.QuerySetTest.test_map_reduce`
1396
+ and :meth:`~autonomous.db.tests.QuerySetTest.test_map_advanced`
1397
+ tests in ``tests.queryset.QuerySetTest`` for usage examples.
1398
+
1399
+ :param map_f: map function, as :class:`~bson.code.Code` or string
1400
+ :param reduce_f: reduce function, as
1401
+ :class:`~bson.code.Code` or string
1402
+ :param output: output collection name, if set to 'inline' will return
1403
+ the results inline. This can also be a dictionary containing output options
1404
+ see: https://www.mongodb.com/docs/manual/reference/command/mapReduce/#mongodb-dbcommand-dbcmd.mapReduce
1405
+ :param finalize_f: finalize function, an optional function that
1406
+ performs any post-reduction processing.
1407
+ :param scope: values to insert into map/reduce global scope. Optional.
1408
+ :param limit: number of objects from current query to provide
1409
+ to map/reduce method
1410
+
1411
+ Returns an iterator yielding
1412
+ :class:`~autonomous.db.document.MapReduceDocument`.
1413
+ """
1414
+ queryset = self.clone()
1415
+
1416
+ MapReduceDocument = _import_class("MapReduceDocument")
1417
+
1418
+ map_f_scope = {}
1419
+ if isinstance(map_f, Code):
1420
+ map_f_scope = map_f.scope
1421
+ map_f = str(map_f)
1422
+ map_f = Code(queryset._sub_js_fields(map_f), map_f_scope or None)
1423
+
1424
+ reduce_f_scope = {}
1425
+ if isinstance(reduce_f, Code):
1426
+ reduce_f_scope = reduce_f.scope
1427
+ reduce_f = str(reduce_f)
1428
+ reduce_f_code = queryset._sub_js_fields(reduce_f)
1429
+ reduce_f = Code(reduce_f_code, reduce_f_scope or None)
1430
+
1431
+ mr_args = {"query": queryset._query}
1432
+
1433
+ if finalize_f:
1434
+ finalize_f_scope = {}
1435
+ if isinstance(finalize_f, Code):
1436
+ finalize_f_scope = finalize_f.scope
1437
+ finalize_f = str(finalize_f)
1438
+ finalize_f_code = queryset._sub_js_fields(finalize_f)
1439
+ finalize_f = Code(finalize_f_code, finalize_f_scope or None)
1440
+ mr_args["finalize"] = finalize_f
1441
+
1442
+ if scope:
1443
+ mr_args["scope"] = scope
1444
+
1445
+ if limit:
1446
+ mr_args["limit"] = limit
1447
+
1448
+ if output == "inline" and not queryset._ordering:
1449
+ inline = True
1450
+ mr_args["out"] = {"inline": 1}
1451
+ else:
1452
+ inline = False
1453
+ if isinstance(output, str):
1454
+ mr_args["out"] = output
1455
+
1456
+ elif isinstance(output, dict):
1457
+ ordered_output = []
1458
+
1459
+ for part in ("replace", "merge", "reduce"):
1460
+ value = output.get(part)
1461
+ if value:
1462
+ ordered_output.append((part, value))
1463
+ break
1464
+
1465
+ else:
1466
+ raise OperationError("actionData not specified for output")
1467
+
1468
+ db_alias = output.get("db_alias")
1469
+ remaing_args = ["db", "sharded", "nonAtomic"]
1470
+
1471
+ if db_alias:
1472
+ ordered_output.append(("db", get_db(db_alias).name))
1473
+ del remaing_args[0]
1474
+
1475
+ for part in remaing_args:
1476
+ value = output.get(part)
1477
+ if value:
1478
+ ordered_output.append((part, value))
1479
+
1480
+ mr_args["out"] = SON(ordered_output)
1481
+
1482
+ db = queryset._document._get_db()
1483
+ result = db.command(
1484
+ {
1485
+ "mapReduce": queryset._document._get_collection_name(),
1486
+ "map": map_f,
1487
+ "reduce": reduce_f,
1488
+ **mr_args,
1489
+ }
1490
+ )
1491
+
1492
+ if inline:
1493
+ docs = result["results"]
1494
+ else:
1495
+ if isinstance(result["result"], str):
1496
+ docs = db[result["result"]].find()
1497
+ else:
1498
+ info = result["result"]
1499
+ docs = db.client[info["db"]][info["collection"]].find()
1500
+
1501
+ if queryset._ordering:
1502
+ docs = docs.sort(queryset._ordering)
1503
+
1504
+ for doc in docs:
1505
+ yield MapReduceDocument(
1506
+ queryset._document, queryset._collection, doc["_id"], doc["value"]
1507
+ )
1508
+
1509
+ def exec_js(self, code, *fields, **options):
1510
+ """Execute a Javascript function on the server. A list of fields may be
1511
+ provided, which will be translated to their correct names and supplied
1512
+ as the arguments to the function. A few extra variables are added to
1513
+ the function's scope: ``collection``, which is the name of the
1514
+ collection in use; ``query``, which is an object representing the
1515
+ current query; and ``options``, which is an object containing any
1516
+ options specified as keyword arguments.
1517
+
1518
+ As fields in MongoEngine may use different names in the database (set
1519
+ using the :attr:`db_field` keyword argument to a :class:`Field`
1520
+ constructor), a mechanism exists for replacing MongoEngine field names
1521
+ with the database field names in Javascript code. When accessing a
1522
+ field, use square-bracket notation, and prefix the MongoEngine field
1523
+ name with a tilde (~).
1524
+
1525
+ :param code: a string of Javascript code to execute
1526
+ :param fields: fields that you will be using in your function, which
1527
+ will be passed in to your function as arguments
1528
+ :param options: options that you want available to the function
1529
+ (accessed in Javascript through the ``options`` object)
1530
+ """
1531
+ queryset = self.clone()
1532
+
1533
+ code = queryset._sub_js_fields(code)
1534
+
1535
+ fields = [queryset._document._translate_field_name(f) for f in fields]
1536
+ collection = queryset._document._get_collection_name()
1537
+
1538
+ scope = {"collection": collection, "options": options or {}}
1539
+
1540
+ query = queryset._query
1541
+ if queryset._where_clause:
1542
+ query["$where"] = queryset._where_clause
1543
+
1544
+ scope["query"] = query
1545
+ code = Code(code, scope=scope)
1546
+
1547
+ db = queryset._document._get_db()
1548
+ return db.command("eval", code, args=fields).get("retval")
1549
+
1550
+ def where(self, where_clause):
1551
+ """Filter ``QuerySet`` results with a ``$where`` clause (a Javascript
1552
+ expression). Performs automatic field name substitution like
1553
+ :meth:`autonomous.db.queryset.Queryset.exec_js`.
1554
+
1555
+ .. note:: When using this mode of query, the database will call your
1556
+ function, or evaluate your predicate clause, for each object
1557
+ in the collection.
1558
+ """
1559
+ queryset = self.clone()
1560
+ where_clause = queryset._sub_js_fields(where_clause)
1561
+ queryset._where_clause = where_clause
1562
+ return queryset
1563
+
1564
+ def sum(self, field):
1565
+ """Sum over the values of the specified field.
1566
+
1567
+ :param field: the field to sum over; use dot notation to refer to
1568
+ embedded document fields
1569
+ """
1570
+ db_field = self._fields_to_dbfields([field]).pop()
1571
+ pipeline = [
1572
+ {"$match": self._query},
1573
+ {"$group": {"_id": "sum", "total": {"$sum": "$" + db_field}}},
1574
+ ]
1575
+
1576
+ # if we're performing a sum over a list field, we sum up all the
1577
+ # elements in the list, hence we need to $unwind the arrays first
1578
+ ListField = _import_class("ListField")
1579
+ field_parts = field.split(".")
1580
+ field_instances = self._document._lookup_field(field_parts)
1581
+ if isinstance(field_instances[-1], ListField):
1582
+ pipeline.insert(1, {"$unwind": "$" + field})
1583
+
1584
+ result = tuple(self._document._get_collection().aggregate(pipeline))
1585
+
1586
+ if result:
1587
+ return result[0]["total"]
1588
+ return 0
1589
+
1590
+ def average(self, field):
1591
+ """Average over the values of the specified field.
1592
+
1593
+ :param field: the field to average over; use dot notation to refer to
1594
+ embedded document fields
1595
+ """
1596
+ db_field = self._fields_to_dbfields([field]).pop()
1597
+ pipeline = [
1598
+ {"$match": self._query},
1599
+ {"$group": {"_id": "avg", "total": {"$avg": "$" + db_field}}},
1600
+ ]
1601
+
1602
+ # if we're performing an average over a list field, we average out
1603
+ # all the elements in the list, hence we need to $unwind the arrays
1604
+ # first
1605
+ ListField = _import_class("ListField")
1606
+ field_parts = field.split(".")
1607
+ field_instances = self._document._lookup_field(field_parts)
1608
+ if isinstance(field_instances[-1], ListField):
1609
+ pipeline.insert(1, {"$unwind": "$" + field})
1610
+
1611
+ result = tuple(self._document._get_collection().aggregate(pipeline))
1612
+ if result:
1613
+ return result[0]["total"]
1614
+ return 0
1615
+
1616
+ def item_frequencies(self, field, normalize=False, map_reduce=True):
1617
+ """Returns a dictionary of all items present in a field across
1618
+ the whole queried set of documents, and their corresponding frequency.
1619
+ This is useful for generating tag clouds, or searching documents.
1620
+
1621
+ .. note::
1622
+
1623
+ Can only do direct simple mappings and cannot map across
1624
+ :class:`~autonomous.db.fields.ReferenceField` or
1625
+ :class:`~autonomous.db.fields.GenericReferenceField` for more complex
1626
+ counting a manual map reduce call is required.
1627
+
1628
+ If the field is a :class:`~autonomous.db.fields.ListField`, the items within
1629
+ each list will be counted individually.
1630
+
1631
+ :param field: the field to use
1632
+ :param normalize: normalize the results so they add to 1.0
1633
+ :param map_reduce: Use map_reduce over exec_js
1634
+ """
1635
+ if map_reduce:
1636
+ return self._item_frequencies_map_reduce(field, normalize=normalize)
1637
+ return self._item_frequencies_exec_js(field, normalize=normalize)
1638
+
1639
+ # Iterator helpers
1640
+
1641
+ def __next__(self):
1642
+ """Wrap the result in a :class:`~autonomous.db.Document` object."""
1643
+ if self._none or self._empty:
1644
+ raise StopIteration
1645
+
1646
+ raw_doc = next(self._cursor)
1647
+
1648
+ if self._as_pymongo:
1649
+ return raw_doc
1650
+
1651
+ doc = self._document._from_son(
1652
+ raw_doc,
1653
+ _auto_dereference=self._auto_dereference,
1654
+ )
1655
+
1656
+ if self._scalar:
1657
+ return self._get_scalar(doc)
1658
+
1659
+ return doc
1660
+
1661
+ def rewind(self):
1662
+ """Rewind the cursor to its unevaluated state."""
1663
+ self._iter = False
1664
+ self._cursor.rewind()
1665
+
1666
+ # Properties
1667
+
1668
+ @property
1669
+ def _collection(self):
1670
+ """Property that returns the collection object. This allows us to
1671
+ perform operations only if the collection is accessed.
1672
+ """
1673
+ return self._collection_obj
1674
+
1675
+ @property
1676
+ def _cursor_args(self):
1677
+ fields_name = "projection"
1678
+ # snapshot is not handled at all by PyMongo 3+
1679
+ # TODO: evaluate similar possibilities using modifiers
1680
+ if self._snapshot:
1681
+ msg = "The snapshot option is not anymore available with PyMongo 3+"
1682
+ warnings.warn(msg, DeprecationWarning)
1683
+
1684
+ cursor_args = {}
1685
+ if not self._timeout:
1686
+ cursor_args["no_cursor_timeout"] = True
1687
+
1688
+ if self._allow_disk_use:
1689
+ cursor_args["allow_disk_use"] = True
1690
+
1691
+ if self._loaded_fields:
1692
+ cursor_args[fields_name] = self._loaded_fields.as_dict()
1693
+
1694
+ if self._search_text:
1695
+ if fields_name not in cursor_args:
1696
+ cursor_args[fields_name] = {}
1697
+
1698
+ if self._search_text_score:
1699
+ cursor_args[fields_name]["_text_score"] = {"$meta": "textScore"}
1700
+
1701
+ return cursor_args
1702
+
1703
+ @property
1704
+ def _cursor(self):
1705
+ """Return a PyMongo cursor object corresponding to this queryset."""
1706
+
1707
+ # If _cursor_obj already exists, return it immediately.
1708
+ if self._cursor_obj is not None:
1709
+ return self._cursor_obj
1710
+
1711
+ # Create a new PyMongo cursor.
1712
+ # XXX In PyMongo 3+, we define the read preference on a collection
1713
+ # level, not a cursor level. Thus, we need to get a cloned collection
1714
+ # object using `with_options` first.
1715
+ if self._read_preference is not None or self._read_concern is not None:
1716
+ self._cursor_obj = self._collection.with_options(
1717
+ read_preference=self._read_preference, read_concern=self._read_concern
1718
+ ).find(self._query, **self._cursor_args)
1719
+ else:
1720
+ self._cursor_obj = self._collection.find(self._query, **self._cursor_args)
1721
+
1722
+ # Apply "where" clauses to cursor
1723
+ if self._where_clause:
1724
+ where_clause = self._sub_js_fields(self._where_clause)
1725
+ self._cursor_obj.where(where_clause)
1726
+
1727
+ # Apply ordering to the cursor.
1728
+ # XXX self._ordering can be equal to:
1729
+ # * None if we didn't explicitly call order_by on this queryset.
1730
+ # * A list of PyMongo-style sorting tuples.
1731
+ # * An empty list if we explicitly called order_by() without any
1732
+ # arguments. This indicates that we want to clear the default
1733
+ # ordering.
1734
+ if self._ordering:
1735
+ # explicit ordering
1736
+ self._cursor_obj.sort(self._ordering)
1737
+ elif self._ordering is None and self._document._meta["ordering"]:
1738
+ # default ordering
1739
+ order = self._get_order_by(self._document._meta["ordering"])
1740
+ self._cursor_obj.sort(order)
1741
+
1742
+ if self._limit is not None:
1743
+ self._cursor_obj.limit(self._limit)
1744
+
1745
+ if self._skip is not None:
1746
+ self._cursor_obj.skip(self._skip)
1747
+
1748
+ if self._hint != -1:
1749
+ self._cursor_obj.hint(self._hint)
1750
+
1751
+ if self._collation is not None:
1752
+ self._cursor_obj.collation(self._collation)
1753
+
1754
+ if self._batch_size is not None:
1755
+ self._cursor_obj.batch_size(self._batch_size)
1756
+
1757
+ if self._comment is not None:
1758
+ self._cursor_obj.comment(self._comment)
1759
+
1760
+ return self._cursor_obj
1761
+
1762
+ def __deepcopy__(self, memo):
1763
+ """Essential for chained queries with ReferenceFields involved"""
1764
+ return self.clone()
1765
+
1766
+ @property
1767
+ def _query(self):
1768
+ if self._mongo_query is None:
1769
+ self._mongo_query = self._query_obj.to_query(self._document)
1770
+ if self._cls_query:
1771
+ if "_cls" in self._mongo_query:
1772
+ self._mongo_query = {"$and": [self._cls_query, self._mongo_query]}
1773
+ else:
1774
+ self._mongo_query.update(self._cls_query)
1775
+ return self._mongo_query
1776
+
1777
+ @property
1778
+ def _dereference(self):
1779
+ if not self.__dereference:
1780
+ self.__dereference = _import_class("DeReference")()
1781
+ return self.__dereference
1782
+
1783
+ @property
1784
+ def _auto_dereference(self):
1785
+ should_deref = not no_dereferencing_active_for_class(self._document)
1786
+ return should_deref and self.__auto_dereference
1787
+
1788
+ def no_dereference(self):
1789
+ """Turn off any dereferencing for the results of this queryset."""
1790
+ queryset = self.clone()
1791
+ queryset.__auto_dereference = False
1792
+ return queryset
1793
+
1794
+ # Helper Functions
1795
+
1796
+ def _item_frequencies_map_reduce(self, field, normalize=False):
1797
+ map_func = """
1798
+ function() {{
1799
+ var path = '{{{{~{field}}}}}'.split('.');
1800
+ var field = this;
1801
+
1802
+ for (p in path) {{
1803
+ if (typeof field != 'undefined')
1804
+ field = field[path[p]];
1805
+ else
1806
+ break;
1807
+ }}
1808
+ if (field && field.constructor == Array) {{
1809
+ field.forEach(function(item) {{
1810
+ emit(item, 1);
1811
+ }});
1812
+ }} else if (typeof field != 'undefined') {{
1813
+ emit(field, 1);
1814
+ }} else {{
1815
+ emit(null, 1);
1816
+ }}
1817
+ }}
1818
+ """.format(field=field)
1819
+ reduce_func = """
1820
+ function(key, values) {
1821
+ var total = 0;
1822
+ var valuesSize = values.length;
1823
+ for (var i=0; i < valuesSize; i++) {
1824
+ total += parseInt(values[i], 10);
1825
+ }
1826
+ return total;
1827
+ }
1828
+ """
1829
+ values = self.map_reduce(map_func, reduce_func, "inline")
1830
+ frequencies = {}
1831
+ for f in values:
1832
+ key = f.key
1833
+ if isinstance(key, float):
1834
+ if int(key) == key:
1835
+ key = int(key)
1836
+ frequencies[key] = int(f.value)
1837
+
1838
+ if normalize:
1839
+ count = sum(frequencies.values())
1840
+ frequencies = {k: float(v) / count for k, v in frequencies.items()}
1841
+
1842
+ return frequencies
1843
+
1844
+ def _item_frequencies_exec_js(self, field, normalize=False):
1845
+ """Uses exec_js to execute"""
1846
+ freq_func = """
1847
+ function(path) {
1848
+ var path = path.split('.');
1849
+
1850
+ var total = 0.0;
1851
+ db[collection].find(query).forEach(function(doc) {
1852
+ var field = doc;
1853
+ for (p in path) {
1854
+ if (field)
1855
+ field = field[path[p]];
1856
+ else
1857
+ break;
1858
+ }
1859
+ if (field && field.constructor == Array) {
1860
+ total += field.length;
1861
+ } else {
1862
+ total++;
1863
+ }
1864
+ });
1865
+
1866
+ var frequencies = {};
1867
+ var types = {};
1868
+ var inc = 1.0;
1869
+
1870
+ db[collection].find(query).forEach(function(doc) {
1871
+ field = doc;
1872
+ for (p in path) {
1873
+ if (field)
1874
+ field = field[path[p]];
1875
+ else
1876
+ break;
1877
+ }
1878
+ if (field && field.constructor == Array) {
1879
+ field.forEach(function(item) {
1880
+ frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]);
1881
+ });
1882
+ } else {
1883
+ var item = field;
1884
+ types[item] = item;
1885
+ frequencies[item] = inc + (isNaN(frequencies[item]) ? 0: frequencies[item]);
1886
+ }
1887
+ });
1888
+ return [total, frequencies, types];
1889
+ }
1890
+ """
1891
+ total, data, types = self.exec_js(freq_func, field)
1892
+ values = {types.get(k): int(v) for k, v in data.items()}
1893
+
1894
+ if normalize:
1895
+ values = {k: float(v) / total for k, v in values.items()}
1896
+
1897
+ frequencies = {}
1898
+ for k, v in values.items():
1899
+ if isinstance(k, float):
1900
+ if int(k) == k:
1901
+ k = int(k)
1902
+
1903
+ frequencies[k] = v
1904
+
1905
+ return frequencies
1906
+
1907
+ def _fields_to_dbfields(self, fields):
1908
+ """Translate fields' paths to their db equivalents."""
1909
+ subclasses = []
1910
+ if self._document._meta["allow_inheritance"]:
1911
+ subclasses = [get_document(x) for x in self._document._subclasses][1:]
1912
+
1913
+ db_field_paths = []
1914
+ for field in fields:
1915
+ field_parts = field.split(".")
1916
+ try:
1917
+ field = ".".join(
1918
+ f if isinstance(f, str) else f.db_field
1919
+ for f in self._document._lookup_field(field_parts)
1920
+ )
1921
+ db_field_paths.append(field)
1922
+ except LookUpError as err:
1923
+ found = False
1924
+
1925
+ # If a field path wasn't found on the main document, go
1926
+ # through its subclasses and see if it exists on any of them.
1927
+ for subdoc in subclasses:
1928
+ try:
1929
+ subfield = ".".join(
1930
+ f if isinstance(f, str) else f.db_field
1931
+ for f in subdoc._lookup_field(field_parts)
1932
+ )
1933
+ db_field_paths.append(subfield)
1934
+ found = True
1935
+ break
1936
+ except LookUpError:
1937
+ pass
1938
+
1939
+ if not found:
1940
+ raise err
1941
+
1942
+ return db_field_paths
1943
+
1944
+ def _get_order_by(self, keys):
1945
+ """Given a list of MongoEngine-style sort keys, return a list
1946
+ of sorting tuples that can be applied to a PyMongo cursor. For
1947
+ example:
1948
+
1949
+ >>> qs._get_order_by(['-last_name', 'first_name'])
1950
+ [('last_name', -1), ('first_name', 1)]
1951
+ """
1952
+ key_list = []
1953
+ for key in keys:
1954
+ if not key:
1955
+ continue
1956
+
1957
+ if key == "$text_score":
1958
+ key_list.append(("_text_score", {"$meta": "textScore"}))
1959
+ continue
1960
+
1961
+ direction = pymongo.ASCENDING
1962
+ if key[0] == "-":
1963
+ direction = pymongo.DESCENDING
1964
+
1965
+ if key[0] in ("-", "+"):
1966
+ key = key[1:]
1967
+
1968
+ key = key.replace("__", ".")
1969
+ try:
1970
+ key = self._document._translate_field_name(key)
1971
+ except Exception:
1972
+ # TODO this exception should be more specific
1973
+ pass
1974
+
1975
+ key_list.append((key, direction))
1976
+
1977
+ return key_list
1978
+
1979
+ def _get_scalar(self, doc):
1980
+ def lookup(obj, name):
1981
+ chunks = name.split("__")
1982
+ for chunk in chunks:
1983
+ obj = getattr(obj, chunk)
1984
+ return obj
1985
+
1986
+ data = [lookup(doc, n) for n in self._scalar]
1987
+ if len(data) == 1:
1988
+ return data[0]
1989
+
1990
+ return tuple(data)
1991
+
1992
+ def _sub_js_fields(self, code):
1993
+ """When fields are specified with [~fieldname] syntax, where
1994
+ *fieldname* is the Python name of a field, *fieldname* will be
1995
+ substituted for the MongoDB name of the field (specified using the
1996
+ :attr:`name` keyword argument in a field's constructor).
1997
+ """
1998
+
1999
+ def field_sub(match):
2000
+ # Extract just the field name, and look up the field objects
2001
+ field_name = match.group(1).split(".")
2002
+ fields = self._document._lookup_field(field_name)
2003
+ # Substitute the correct name for the field into the javascript
2004
+ return '["%s"]' % fields[-1].db_field
2005
+
2006
+ def field_path_sub(match):
2007
+ # Extract just the field name, and look up the field objects
2008
+ field_name = match.group(1).split(".")
2009
+ fields = self._document._lookup_field(field_name)
2010
+ # Substitute the correct name for the field into the javascript
2011
+ return ".".join([f.db_field for f in fields])
2012
+
2013
+ code = re.sub(r"\[\s*~([A-z_][A-z_0-9.]+?)\s*\]", field_sub, code)
2014
+ code = re.sub(r"\{\{\s*~([A-z_][A-z_0-9.]+?)\s*\}\}", field_path_sub, code)
2015
+ return code
2016
+
2017
+ def _chainable_method(self, method_name, val):
2018
+ """Call a particular method on the PyMongo cursor call a particular chainable method
2019
+ with the provided value.
2020
+ """
2021
+ queryset = self.clone()
2022
+
2023
+ # Get an existing cursor object or create a new one
2024
+ cursor = queryset._cursor
2025
+
2026
+ # Find the requested method on the cursor and call it with the
2027
+ # provided value
2028
+ getattr(cursor, method_name)(val)
2029
+
2030
+ # Cache the value on the queryset._{method_name}
2031
+ setattr(queryset, "_" + method_name, val)
2032
+
2033
+ return queryset