aggify 0.2.5__tar.gz → 0.2.7__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.5 → aggify-0.2.7}/PKG-INFO +3 -4
- {aggify-0.2.5 → aggify-0.2.7}/README.md +2 -3
- {aggify-0.2.5 → aggify-0.2.7}/aggify/aggify.py +95 -3
- {aggify-0.2.5 → aggify-0.2.7}/aggify/compiler.py +35 -23
- {aggify-0.2.5 → aggify-0.2.7}/pyproject.toml +1 -1
- {aggify-0.2.5 → aggify-0.2.7}/LICENSE +0 -0
- {aggify-0.2.5 → aggify-0.2.7}/aggify/__init__.py +0 -0
- {aggify-0.2.5 → aggify-0.2.7}/aggify/exceptions.py +0 -0
- {aggify-0.2.5 → aggify-0.2.7}/aggify/types.py +0 -0
- {aggify-0.2.5 → aggify-0.2.7}/aggify/utilty.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: aggify
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: A MongoDB aggregation generator for Mongoengine
|
|
5
5
|
Home-page: https://github.com/Aggify/aggify
|
|
6
6
|
Author: SeYeD.Dev
|
|
@@ -30,6 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
[](https://coverage-badge.samuelcolvin.workers.dev/redirect/Aggify/aggify)
|
|
31
31
|
[](https://github.com/Aggify/aggify/blob/main/LICENSE)
|
|
32
32
|
[](https://github.com/Aggify/aggify/graphs/contributors)
|
|
33
|
+
[](https://t.me/Aggify)
|
|
33
34
|
|
|
34
35
|
# Aggify
|
|
35
36
|
|
|
@@ -47,9 +48,7 @@ organized interface.
|
|
|
47
48
|
|
|
48
49
|
## TODO
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
- [ ] `$project` (with expressions): Allows you to use expressions to reshape and calculate values.
|
|
52
|
-
- [ ] `$redact`: Controls document inclusion during the aggregation pipeline.
|
|
51
|
+
|
|
53
52
|
|
|
54
53
|
## Installation
|
|
55
54
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
[](https://coverage-badge.samuelcolvin.workers.dev/redirect/Aggify/aggify)
|
|
12
12
|
[](https://github.com/Aggify/aggify/blob/main/LICENSE)
|
|
13
13
|
[](https://github.com/Aggify/aggify/graphs/contributors)
|
|
14
|
+
[](https://t.me/Aggify)
|
|
14
15
|
|
|
15
16
|
# Aggify
|
|
16
17
|
|
|
@@ -28,9 +29,7 @@ organized interface.
|
|
|
28
29
|
|
|
29
30
|
## TODO
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
- [ ] `$project` (with expressions): Allows you to use expressions to reshape and calculate values.
|
|
33
|
-
- [ ] `$redact`: Controls document inclusion during the aggregation pipeline.
|
|
32
|
+
|
|
34
33
|
|
|
35
34
|
## Installation
|
|
36
35
|
|
|
@@ -495,6 +495,39 @@ class Aggify:
|
|
|
495
495
|
|
|
496
496
|
return merged_pipeline
|
|
497
497
|
|
|
498
|
+
# check_fields_exist(self.base_model, let) # noqa
|
|
499
|
+
def get_field_name_recursively(self, field: str) -> str:
|
|
500
|
+
"""
|
|
501
|
+
Recursively fetch the field name by following the hierarchy indicated by the field parameter.
|
|
502
|
+
|
|
503
|
+
The function traverses the field hierarchy indicated by double underscores in the field parameter.
|
|
504
|
+
At each level, it checks if the field exists and then fetches the database field name for it.
|
|
505
|
+
The entire hierarchy is then joined using a dot (.) separator.
|
|
506
|
+
|
|
507
|
+
Parameters:
|
|
508
|
+
- field (str): A string indicating the hierarchy of fields, separated by double underscores.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
- str: A dot-separated string representing the full path to the field in the database.
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
field_name = []
|
|
515
|
+
prev_base = self.base_model
|
|
516
|
+
|
|
517
|
+
# Split the field based on double underscores and process each item
|
|
518
|
+
for index, item in enumerate(field.split("__")):
|
|
519
|
+
# Ensure the field exists at the current level of hierarchy
|
|
520
|
+
check_fields_exist(prev_base, [item]) # noqa
|
|
521
|
+
|
|
522
|
+
# Append the database field name to the field_name list
|
|
523
|
+
field_name.append(get_db_field(prev_base, item))
|
|
524
|
+
|
|
525
|
+
# Move to the next level in the model hierarchy
|
|
526
|
+
prev_base = self.get_model_field(prev_base, item)
|
|
527
|
+
|
|
528
|
+
# Join the entire hierarchy using dots and return
|
|
529
|
+
return ".".join(field_name)
|
|
530
|
+
|
|
498
531
|
@last_out_stage_check
|
|
499
532
|
def lookup(
|
|
500
533
|
self,
|
|
@@ -544,11 +577,12 @@ class Aggify:
|
|
|
544
577
|
else:
|
|
545
578
|
if not query:
|
|
546
579
|
raise InvalidArgument(expected_list=["query"])
|
|
547
|
-
|
|
580
|
+
|
|
548
581
|
let_dict = {
|
|
549
|
-
field: f"${get_db_field(self.base_model, field)}" # noqa
|
|
550
|
-
for field in let
|
|
582
|
+
field: f"${get_db_field(self.base_model, self.get_field_name_recursively(field))}" # noqa
|
|
583
|
+
for field in let
|
|
551
584
|
}
|
|
585
|
+
|
|
552
586
|
for q in query:
|
|
553
587
|
# Construct the match stage for each query
|
|
554
588
|
if isinstance(q, Q):
|
|
@@ -680,3 +714,61 @@ class Aggify:
|
|
|
680
714
|
self.pipelines.append(new_root)
|
|
681
715
|
|
|
682
716
|
return self
|
|
717
|
+
|
|
718
|
+
@last_out_stage_check
|
|
719
|
+
def redact(self, value1, condition, value2, then_value, else_value):
|
|
720
|
+
"""
|
|
721
|
+
Add a $redact stage to the pipeline based on the provided conditions.
|
|
722
|
+
|
|
723
|
+
The $redact stage restricts the contents of the returned documents
|
|
724
|
+
based on the condition specified. This method assists in building
|
|
725
|
+
the $redact stage for a MongoDB aggregation pipeline.
|
|
726
|
+
|
|
727
|
+
Parameters:
|
|
728
|
+
- value1 (str): The first value to compare in the condition.
|
|
729
|
+
- condition (str): The MongoDB comparison operator (e.g., "$eq", "$lt").
|
|
730
|
+
- value2 (str): The second value to compare in the condition.
|
|
731
|
+
- then_value (str): The action to take if the condition is True.
|
|
732
|
+
Expected values: "$DESCEND", "$PRUNE", "$KEEP".
|
|
733
|
+
- else_value (str): The action to take if the condition is False.
|
|
734
|
+
Expected values: "$DESCEND", "$PRUNE", "$KEEP".
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
- self: Returns the instance of the class to allow chaining.
|
|
738
|
+
|
|
739
|
+
Raises:
|
|
740
|
+
- InvalidArgument: If then_value or else_value are not in the expected list.
|
|
741
|
+
"""
|
|
742
|
+
|
|
743
|
+
# List of valid redaction values
|
|
744
|
+
redact_values = ["DESCEND", "PRUNE", "KEEP"]
|
|
745
|
+
|
|
746
|
+
def clean_then_else(then_value, else_value):
|
|
747
|
+
"""
|
|
748
|
+
Helper function to sanitize then_value and else_value.
|
|
749
|
+
|
|
750
|
+
Strips the dollar sign and converts values to uppercase.
|
|
751
|
+
"""
|
|
752
|
+
return (
|
|
753
|
+
then_value.replace("$", "").upper(),
|
|
754
|
+
else_value.replace("$", "").upper(),
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Clean the provided then_value and else_value
|
|
758
|
+
then_value, else_value = clean_then_else(then_value, else_value)
|
|
759
|
+
|
|
760
|
+
# Check if the cleaned values are in the valid list
|
|
761
|
+
if then_value not in redact_values or else_value not in redact_values:
|
|
762
|
+
raise InvalidArgument(expected_list=redact_values)
|
|
763
|
+
|
|
764
|
+
# Construct the $redact stage with the provided condition
|
|
765
|
+
stage = {
|
|
766
|
+
"$redact": dict(
|
|
767
|
+
Cond(value1, condition, value2, f"$${then_value}", f"$${else_value}")
|
|
768
|
+
)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
# Append the constructed stage to the pipelines list
|
|
772
|
+
self.pipelines.append(stage)
|
|
773
|
+
|
|
774
|
+
return self
|
|
@@ -10,7 +10,7 @@ from aggify.utilty import get_db_field
|
|
|
10
10
|
class Operators:
|
|
11
11
|
QUERY_OPERATORS = {
|
|
12
12
|
"exact": "$eq",
|
|
13
|
-
"iexact": "$
|
|
13
|
+
"iexact": "$regex",
|
|
14
14
|
"contains": "$regex",
|
|
15
15
|
"icontains": "$regex", # noqa
|
|
16
16
|
"startswith": "$regex",
|
|
@@ -34,6 +34,23 @@ class Operators:
|
|
|
34
34
|
**COMPARISON_OPERATORS,
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
REGEX_PATTERNS = {
|
|
38
|
+
"iexact": "^{value}$",
|
|
39
|
+
"contains": "{value}",
|
|
40
|
+
"icontains": "{value}",
|
|
41
|
+
"startswith": "^{value}",
|
|
42
|
+
"istartswith": "^{value}",
|
|
43
|
+
"endswith": "{value}$",
|
|
44
|
+
"iendswith": "{value}$",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
REGEX_OPTIONS = {
|
|
48
|
+
"iexact": "i",
|
|
49
|
+
"icontains": "i",
|
|
50
|
+
"istartswith": "i",
|
|
51
|
+
"iendswith": "i",
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
def __init__(self, match_query: Dict[str, Any]):
|
|
38
55
|
self.match_query = match_query
|
|
39
56
|
|
|
@@ -42,35 +59,30 @@ class Operators:
|
|
|
42
59
|
# I think there should be easier way to inject comparison operators to be defined per each
|
|
43
60
|
# like map an existing template to each operator
|
|
44
61
|
|
|
45
|
-
if operator in
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
]: # noqa
|
|
56
|
-
self.match_query[field] = {
|
|
57
|
-
Operators.ALL_OPERATORS[operator]: f".*{value}.*",
|
|
58
|
-
"$options": "i",
|
|
59
|
-
}
|
|
62
|
+
if operator in Operators.REGEX_PATTERNS:
|
|
63
|
+
if isinstance(value, F):
|
|
64
|
+
raise ValueError("Not implemented yet")
|
|
65
|
+
pattern = Operators.REGEX_PATTERNS[operator].format(value=value)
|
|
66
|
+
# Create the base query with the pattern
|
|
67
|
+
query = {Operators.ALL_OPERATORS[operator]: pattern}
|
|
68
|
+
|
|
69
|
+
# If there's an option for the operator, add it to the query
|
|
70
|
+
if operator in Operators.REGEX_OPTIONS:
|
|
71
|
+
query["$options"] = Operators.REGEX_OPTIONS[operator]
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
self.match_query[field] = query
|
|
74
|
+
elif operator in Operators.ALL_OPERATORS:
|
|
62
75
|
if isinstance(value, F):
|
|
63
76
|
self.match_query["$expr"] = {
|
|
64
|
-
Operators.ALL_OPERATORS[operator]: [
|
|
65
|
-
f"${field}",
|
|
66
|
-
value.to_dict(),
|
|
67
|
-
]
|
|
77
|
+
Operators.ALL_OPERATORS[operator]: [f"${field}", value.to_dict()]
|
|
68
78
|
}
|
|
69
|
-
|
|
70
79
|
else:
|
|
71
80
|
self.match_query[field] = {Operators.ALL_OPERATORS[operator]: value}
|
|
72
81
|
else:
|
|
73
|
-
|
|
82
|
+
# Default behavior
|
|
83
|
+
self.match_query[field] = {
|
|
84
|
+
Operators.ALL_OPERATORS.get(operator, operator): value
|
|
85
|
+
}
|
|
74
86
|
|
|
75
87
|
return self.match_query
|
|
76
88
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|