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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aggify
3
- Version: 0.2.5
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
  [![Coverage](https://img.shields.io/codecov/c/github/Aggify/aggify)](https://coverage-badge.samuelcolvin.workers.dev/redirect/Aggify/aggify)
31
31
  [![License](https://img.shields.io/github/license/Aggify/aggify.svg)](https://github.com/Aggify/aggify/blob/main/LICENSE)
32
32
  [![Contributors](https://img.shields.io/github/contributors/Aggify/aggify.svg)](https://github.com/Aggify/aggify/graphs/contributors)
33
+ [![Telegram](https://img.shields.io/badge/-telegram-red?color=white&logo=telegram&logoColor=blue)](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
- - [ ] `$replaceRoot`: Replaces the document structure with a new one.
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
  [![Coverage](https://img.shields.io/codecov/c/github/Aggify/aggify)](https://coverage-badge.samuelcolvin.workers.dev/redirect/Aggify/aggify)
12
12
  [![License](https://img.shields.io/github/license/Aggify/aggify.svg)](https://github.com/Aggify/aggify/blob/main/LICENSE)
13
13
  [![Contributors](https://img.shields.io/github/contributors/Aggify/aggify.svg)](https://github.com/Aggify/aggify/graphs/contributors)
14
+ [![Telegram](https://img.shields.io/badge/-telegram-red?color=white&logo=telegram&logoColor=blue)](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
- - [ ] `$replaceRoot`: Replaces the document structure with a new one.
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
- check_fields_exist(self.base_model, let) # noqa
580
+
548
581
  let_dict = {
549
- field: f"${get_db_field(self.base_model, field)}" # noqa
550
- for field in let # noqa
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": "$eq",
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 ["exact", "iexact"]:
46
- self.match_query[field] = {Operators.ALL_OPERATORS[operator]: value}
47
-
48
- elif operator in [
49
- "contains",
50
- "startswith",
51
- "endswith",
52
- "icontains",
53
- "istartswith",
54
- "iendswith",
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
- elif operator in Operators.ALL_OPERATORS[operator]:
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
- self.match_query[field] = {Operators.ALL_OPERATORS[operator]: value}
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
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aggify"
3
- version = "0.2.5"
3
+ version = "0.2.7"
4
4
  authors = ["SeYeD.Dev <me@seyed.dev>"]
5
5
  repository = "https://github.com/Aggify/aggify"
6
6
  documentation = "https://github.com/Aggify/aggify/docs"
File without changes
File without changes
File without changes
File without changes
File without changes