aggify 0.2.9__tar.gz → 0.3.0__tar.gz
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.
- {aggify-0.2.9 → aggify-0.3.0}/PKG-INFO +1 -2
- {aggify-0.2.9 → aggify-0.3.0}/README.md +0 -1
- {aggify-0.2.9 → aggify-0.3.0}/aggify/aggify.py +73 -68
- {aggify-0.2.9 → aggify-0.3.0}/aggify/compiler.py +13 -13
- {aggify-0.2.9 → aggify-0.3.0}/aggify/types.py +1 -2
- {aggify-0.2.9 → aggify-0.3.0}/aggify/utilty.py +1 -13
- {aggify-0.2.9 → aggify-0.3.0}/pyproject.toml +1 -1
- {aggify-0.2.9 → aggify-0.3.0}/LICENSE +0 -0
- {aggify-0.2.9 → aggify-0.3.0}/aggify/__init__.py +0 -0
- {aggify-0.2.9 → aggify-0.3.0}/aggify/exceptions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: aggify
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A MongoDB aggregation generator for Mongoengine
|
|
5
5
|
Home-page: https://github.com/Aggify/aggify
|
|
6
6
|
Author: SeYeD.Dev
|
|
@@ -181,4 +181,3 @@ In the sample usage above, you can see how Aggify simplifies the construction of
|
|
|
181
181
|
allowing you to chain filters, lookups, and other operations to build complex queries.
|
|
182
182
|
For more details and examples, please refer to the documentation and codebase.
|
|
183
183
|
|
|
184
|
-
|
|
@@ -161,4 +161,3 @@ Mongoengine equivalent query:
|
|
|
161
161
|
In the sample usage above, you can see how Aggify simplifies the construction of MongoDB aggregation pipelines by
|
|
162
162
|
allowing you to chain filters, lookups, and other operations to build complex queries.
|
|
163
163
|
For more details and examples, please refer to the documentation and codebase.
|
|
164
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import functools
|
|
2
|
-
from typing import Any, Dict, Type, Union, List,
|
|
2
|
+
from typing import Any, Dict, Type, Union, List, TypeVar, Callable
|
|
3
3
|
|
|
4
|
-
from mongoengine import Document, EmbeddedDocument, fields
|
|
4
|
+
from mongoengine import Document, EmbeddedDocument, fields as mongoengine_fields
|
|
5
5
|
from mongoengine.base import TopLevelDocumentMetaclass
|
|
6
6
|
|
|
7
7
|
from aggify.compiler import F, Match, Q, Operators, Cond # noqa keep
|
|
@@ -85,25 +85,23 @@ class Aggify:
|
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
87
|
# Extract fields to keep and check if _id should be deleted
|
|
88
|
-
to_keep_values =
|
|
89
|
-
delete_id = kwargs.get("id")
|
|
88
|
+
to_keep_values = {"id"}
|
|
89
|
+
delete_id = kwargs.get("id") is not None
|
|
90
90
|
projection = {}
|
|
91
91
|
|
|
92
92
|
# Add missing fields to the base model
|
|
93
93
|
for key, value in kwargs.items():
|
|
94
94
|
if value == 1:
|
|
95
|
-
to_keep_values.
|
|
95
|
+
to_keep_values.add(key)
|
|
96
96
|
elif key not in self.base_model._fields and isinstance( # noqa
|
|
97
97
|
kwargs[key], (str, dict)
|
|
98
|
-
):
|
|
99
|
-
to_keep_values.
|
|
100
|
-
self.base_model._fields[key] =
|
|
98
|
+
):
|
|
99
|
+
to_keep_values.add(key)
|
|
100
|
+
self.base_model._fields[key] = mongoengine_fields.IntField() # noqa
|
|
101
101
|
projection[get_db_field(self.base_model, key)] = value # noqa
|
|
102
102
|
|
|
103
103
|
# Remove fields from the base model, except the ones in to_keep_values and possibly _id
|
|
104
|
-
keys_for_deletion =
|
|
105
|
-
to_keep_values
|
|
106
|
-
) # noqa
|
|
104
|
+
keys_for_deletion = self.base_model._fields.keys() - to_keep_values # noqa
|
|
107
105
|
if delete_id:
|
|
108
106
|
keys_for_deletion.add("id")
|
|
109
107
|
for key in keys_for_deletion:
|
|
@@ -144,19 +142,18 @@ class Aggify:
|
|
|
144
142
|
return self
|
|
145
143
|
|
|
146
144
|
@last_out_stage_check
|
|
147
|
-
def add_fields(self, **
|
|
148
|
-
"""
|
|
149
|
-
Generates a MongoDB addFields pipeline stage.
|
|
145
|
+
def add_fields(self, **fields) -> "Aggify": # noqa
|
|
146
|
+
"""Generates a MongoDB addFields pipeline stage.
|
|
150
147
|
|
|
151
148
|
Args:
|
|
152
|
-
|
|
149
|
+
fields: A dictionary of field expressions and values.
|
|
153
150
|
|
|
154
151
|
Returns:
|
|
155
152
|
A MongoDB add_fields pipeline stage.
|
|
156
153
|
"""
|
|
157
154
|
add_fields_stage = {"$addFields": {}}
|
|
158
155
|
|
|
159
|
-
for field, expression in
|
|
156
|
+
for field, expression in fields.items():
|
|
160
157
|
field = field.replace("__", ".")
|
|
161
158
|
if isinstance(expression, str):
|
|
162
159
|
add_fields_stage["$addFields"][field] = {"$literal": expression}
|
|
@@ -169,7 +166,9 @@ class Aggify:
|
|
|
169
166
|
else:
|
|
170
167
|
raise AggifyValueError([str, F, list], type(expression))
|
|
171
168
|
# TODO: Should be checked if new field is embedded, create embedded field.
|
|
172
|
-
self.base_model._fields[
|
|
169
|
+
self.base_model._fields[ # noqa
|
|
170
|
+
field.replace("$", "")
|
|
171
|
+
] = mongoengine_fields.IntField()
|
|
173
172
|
|
|
174
173
|
self.pipelines.append(add_fields_stage)
|
|
175
174
|
return self
|
|
@@ -238,12 +237,8 @@ class Aggify:
|
|
|
238
237
|
"""
|
|
239
238
|
Builds the pipelines list based on the query parameters.
|
|
240
239
|
"""
|
|
241
|
-
skip_list = []
|
|
242
240
|
|
|
243
241
|
for key, value in query.items():
|
|
244
|
-
if key in skip_list:
|
|
245
|
-
continue
|
|
246
|
-
|
|
247
242
|
# Split the key to access the field information.
|
|
248
243
|
split_query = key.split("__")
|
|
249
244
|
|
|
@@ -257,8 +252,8 @@ class Aggify:
|
|
|
257
252
|
or "document_type_obj"
|
|
258
253
|
not in join_field.__dict__ # Check whether this field is a join field or not.
|
|
259
254
|
or issubclass(
|
|
260
|
-
|
|
261
|
-
|
|
255
|
+
join_field.document_type, EmbeddedDocument # noqa
|
|
256
|
+
) # Check whether this field is embedded field or not
|
|
262
257
|
or len(split_query) == 1
|
|
263
258
|
or (len(split_query) == 2 and split_query[1] in Operators.ALL_OPERATORS)
|
|
264
259
|
):
|
|
@@ -276,7 +271,6 @@ class Aggify:
|
|
|
276
271
|
matches = []
|
|
277
272
|
for k, v in query.items():
|
|
278
273
|
if k.split("__")[0] == split_query[0]:
|
|
279
|
-
skip_list.append(k)
|
|
280
274
|
_key = k.replace("__", ".", 1)
|
|
281
275
|
match = self.__match({_key: v}).get("$match")
|
|
282
276
|
if match != {}:
|
|
@@ -357,15 +351,6 @@ class Aggify:
|
|
|
357
351
|
self.pipelines.append(unwind_stage)
|
|
358
352
|
return self
|
|
359
353
|
|
|
360
|
-
def aggregate(self):
|
|
361
|
-
"""
|
|
362
|
-
Returns the aggregated results.
|
|
363
|
-
|
|
364
|
-
Returns:
|
|
365
|
-
The aggregated results.
|
|
366
|
-
"""
|
|
367
|
-
return self.base_model.objects.aggregate(*self.pipelines) # type: ignore
|
|
368
|
-
|
|
369
354
|
def annotate(
|
|
370
355
|
self, annotate_name: str, accumulator: str, f: Union[Union[str, Dict], F, int]
|
|
371
356
|
) -> "Aggify":
|
|
@@ -389,31 +374,49 @@ class Aggify:
|
|
|
389
374
|
"""
|
|
390
375
|
|
|
391
376
|
# Some of the accumulator fields might be false and should be checked.
|
|
377
|
+
# noinspection SpellCheckingInspection
|
|
392
378
|
aggregation_mapping: Dict[str, Type] = {
|
|
393
|
-
"sum": (
|
|
394
|
-
"avg": (
|
|
395
|
-
"stdDevPop": (
|
|
396
|
-
"stdDevSamp": (
|
|
397
|
-
"push": (
|
|
398
|
-
"addToSet": (
|
|
399
|
-
"count": (
|
|
400
|
-
"first": (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
"
|
|
407
|
-
|
|
379
|
+
"sum": (mongoengine_fields.FloatField(), "$sum"),
|
|
380
|
+
"avg": (mongoengine_fields.FloatField(), "$avg"),
|
|
381
|
+
"stdDevPop": (mongoengine_fields.FloatField(), "$stdDevPop"),
|
|
382
|
+
"stdDevSamp": (mongoengine_fields.FloatField(), "$stdDevSamp"),
|
|
383
|
+
"push": (mongoengine_fields.ListField(), "$push"),
|
|
384
|
+
"addToSet": (mongoengine_fields.ListField(), "$addToSet"),
|
|
385
|
+
"count": (mongoengine_fields.IntField(), "$count"),
|
|
386
|
+
"first": (
|
|
387
|
+
mongoengine_fields.EmbeddedDocumentField(
|
|
388
|
+
mongoengine_fields.EmbeddedDocument
|
|
389
|
+
),
|
|
390
|
+
"$first",
|
|
391
|
+
),
|
|
392
|
+
"last": (
|
|
393
|
+
mongoengine_fields.EmbeddedDocumentField(
|
|
394
|
+
mongoengine_fields.EmbeddedDocument
|
|
395
|
+
),
|
|
396
|
+
"$last",
|
|
397
|
+
),
|
|
398
|
+
"max": (mongoengine_fields.DynamicField(), "$max"),
|
|
399
|
+
"accumulator": (mongoengine_fields.DynamicField(), "$accumulator"),
|
|
400
|
+
"min": (mongoengine_fields.DynamicField(), "$min"),
|
|
401
|
+
"median": (mongoengine_fields.DynamicField(), "$median"),
|
|
402
|
+
"mergeObjects": (mongoengine_fields.DictField(), "$mergeObjects"),
|
|
403
|
+
"top": (
|
|
404
|
+
mongoengine_fields.EmbeddedDocumentField(
|
|
405
|
+
mongoengine_fields.EmbeddedDocument
|
|
406
|
+
),
|
|
407
|
+
"$top",
|
|
408
|
+
),
|
|
408
409
|
"bottom": (
|
|
409
|
-
|
|
410
|
+
mongoengine_fields.EmbeddedDocumentField(
|
|
411
|
+
mongoengine_fields.EmbeddedDocument
|
|
412
|
+
),
|
|
410
413
|
"$bottom",
|
|
411
414
|
),
|
|
412
|
-
"topN": (
|
|
413
|
-
"bottomN": (
|
|
414
|
-
"firstN": (
|
|
415
|
-
"lastN": (
|
|
416
|
-
"maxN": (
|
|
415
|
+
"topN": (mongoengine_fields.ListField(), "$topN"),
|
|
416
|
+
"bottomN": (mongoengine_fields.ListField(), "$bottomN"),
|
|
417
|
+
"firstN": (mongoengine_fields.ListField(), "$firstN"),
|
|
418
|
+
"lastN": (mongoengine_fields.ListField(), "$lastN"),
|
|
419
|
+
"maxN": (mongoengine_fields.ListField(), "$maxN"),
|
|
417
420
|
}
|
|
418
421
|
|
|
419
422
|
try:
|
|
@@ -552,19 +555,23 @@ class Aggify:
|
|
|
552
555
|
foreign_field: Union[str, None] = None,
|
|
553
556
|
) -> "Aggify":
|
|
554
557
|
"""
|
|
555
|
-
|
|
558
|
+
Generates a MongoDB lookup pipeline stage.
|
|
556
559
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
560
|
+
Args:
|
|
561
|
+
from_collection (Document): The document representing the collection to perform the lookup on.
|
|
562
|
+
as_name (str): The name of the new field to create.
|
|
563
|
+
query (list[Q] | Union[Q, None], optional): List of desired queries with Q function or a single query.
|
|
564
|
+
let (Union[List[str], None], optional): The local field(s) to join on. If provided,
|
|
565
|
+
localField and foreignField are not used.
|
|
566
|
+
local_field (Union[str, None], optional): The local field to join on when `let` is not provided.
|
|
567
|
+
foreign_field (Union[str, None], optional): The foreign field to join on when `let` is not provided.
|
|
568
|
+
let (Union[List[str], None], optional): The local field(s) to join on. If provided,
|
|
569
|
+
localField and foreignField are not used.
|
|
570
|
+
local_field (Union[str, None], optional): The local field to join on when let not provided.
|
|
571
|
+
foreign_field (Union[str, None], optional): The foreign field to join on when let not provided.
|
|
565
572
|
|
|
566
|
-
|
|
567
|
-
|
|
573
|
+
Returns:
|
|
574
|
+
Aggify: An instance of the Aggify class representing a MongoDB lookup pipeline stage.
|
|
568
575
|
"""
|
|
569
576
|
|
|
570
577
|
lookup_stages = []
|
|
@@ -576,8 +583,6 @@ class Aggify:
|
|
|
576
583
|
expected_list=[["local_field", "foreign_field"], "let"]
|
|
577
584
|
)
|
|
578
585
|
elif not let:
|
|
579
|
-
if not (local_field and foreign_field):
|
|
580
|
-
raise InvalidArgument(expected_list=["local_field", "foreign_field"])
|
|
581
586
|
lookup_stage = {
|
|
582
587
|
"$lookup": {
|
|
583
588
|
"from": from_collection_name,
|
|
@@ -632,7 +637,7 @@ class Aggify:
|
|
|
632
637
|
return self
|
|
633
638
|
|
|
634
639
|
@staticmethod
|
|
635
|
-
def get_model_field(model:
|
|
640
|
+
def get_model_field(model: Type[Document], field: str) -> mongoengine_fields:
|
|
636
641
|
"""
|
|
637
642
|
Get the field definition of a specified field in a MongoDB model.
|
|
638
643
|
|
|
@@ -82,11 +82,6 @@ class Operators:
|
|
|
82
82
|
}
|
|
83
83
|
else:
|
|
84
84
|
self.match_query[field] = {Operators.ALL_OPERATORS[operator]: value}
|
|
85
|
-
else:
|
|
86
|
-
# Default behavior
|
|
87
|
-
self.match_query[field] = {
|
|
88
|
-
Operators.ALL_OPERATORS.get(operator, operator): value
|
|
89
|
-
}
|
|
90
85
|
|
|
91
86
|
return self.match_query
|
|
92
87
|
|
|
@@ -107,7 +102,7 @@ class Q:
|
|
|
107
102
|
yield "$match", self.conditions
|
|
108
103
|
|
|
109
104
|
def __or__(self, other):
|
|
110
|
-
if self.conditions.get("$or"
|
|
105
|
+
if self.conditions.get("$or"):
|
|
111
106
|
self.conditions["$or"].append(dict(other)["$match"])
|
|
112
107
|
combined_conditions = self.conditions
|
|
113
108
|
|
|
@@ -116,7 +111,7 @@ class Q:
|
|
|
116
111
|
return Q(**combined_conditions)
|
|
117
112
|
|
|
118
113
|
def __and__(self, other):
|
|
119
|
-
if self.conditions.get("$and"
|
|
114
|
+
if self.conditions.get("$and"):
|
|
120
115
|
self.conditions["$and"].append(dict(other)["$match"])
|
|
121
116
|
combined_conditions = self.conditions
|
|
122
117
|
else:
|
|
@@ -138,7 +133,7 @@ class F:
|
|
|
138
133
|
def to_dict(self):
|
|
139
134
|
return self.field
|
|
140
135
|
|
|
141
|
-
def __add__(self, other):
|
|
136
|
+
def __add__(self, other):
|
|
142
137
|
if isinstance(other, F):
|
|
143
138
|
other = other.field
|
|
144
139
|
|
|
@@ -248,7 +243,12 @@ class Match:
|
|
|
248
243
|
|
|
249
244
|
@staticmethod
|
|
250
245
|
def validate_operator(key: str):
|
|
251
|
-
|
|
246
|
+
_op = key.rsplit("__", 1)
|
|
247
|
+
try:
|
|
248
|
+
operator = _op[1]
|
|
249
|
+
except IndexError:
|
|
250
|
+
raise InvalidOperator(str(_op)) from None
|
|
251
|
+
|
|
252
252
|
if operator not in Operators.COMPARISON_OPERATORS:
|
|
253
253
|
raise InvalidOperator(operator)
|
|
254
254
|
|
|
@@ -275,15 +275,15 @@ class Match:
|
|
|
275
275
|
def compile(self, pipelines: list) -> Dict[str, Dict[str, list]]:
|
|
276
276
|
match_query = {}
|
|
277
277
|
for key, value in self.matches.items():
|
|
278
|
+
if isinstance(value, F):
|
|
279
|
+
if F.is_suitable_for_match(key) is False:
|
|
280
|
+
raise InvalidOperator(key)
|
|
281
|
+
|
|
278
282
|
if "__" not in key:
|
|
279
283
|
key = get_db_field(self.base_model, key)
|
|
280
284
|
match_query[key] = value
|
|
281
285
|
continue
|
|
282
286
|
|
|
283
|
-
if isinstance(value, F):
|
|
284
|
-
if F.is_suitable_for_match(key) is False:
|
|
285
|
-
raise InvalidOperator(key)
|
|
286
|
-
|
|
287
287
|
field, operator, *_ = key.split("__")
|
|
288
288
|
if (
|
|
289
289
|
self.is_base_model_field(field)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import Union, Dict, TypeVar, Callable
|
|
2
|
-
from mongoengine import Document
|
|
3
2
|
from bson import ObjectId
|
|
3
|
+
from mongoengine import Document
|
|
4
4
|
|
|
5
5
|
QueryParams = Union[int, None, str, bool, float, Dict, ObjectId]
|
|
6
|
-
|
|
7
6
|
CollectionType = TypeVar("CollectionType", bound=Callable[..., Document])
|
|
@@ -1,20 +1,8 @@
|
|
|
1
1
|
from typing import Any, Union, List, Dict
|
|
2
2
|
|
|
3
3
|
from mongoengine import Document
|
|
4
|
-
|
|
5
|
-
from aggify.exceptions import MongoIndexError, InvalidField, AlreadyExistsField
|
|
6
4
|
from aggify.types import CollectionType
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def int_to_slice(final_index: int) -> slice:
|
|
10
|
-
"""
|
|
11
|
-
Converts an integer to a slice, assuming that the start index is 0.
|
|
12
|
-
|
|
13
|
-
Examples:
|
|
14
|
-
>>> int_to_slice(3)
|
|
15
|
-
slice(0, 2)
|
|
16
|
-
"""
|
|
17
|
-
return slice(0, final_index)
|
|
5
|
+
from aggify.exceptions import MongoIndexError, InvalidField, AlreadyExistsField
|
|
18
6
|
|
|
19
7
|
|
|
20
8
|
def to_mongo_positive_index(index: Union[int, slice]) -> slice:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|