From 9fe1ca489b26629c5f4b8733c1bb65aeb44973f5 Mon Sep 17 00:00:00 2001 From: VianneyMI Date: Wed, 11 Jun 2025 23:23:40 +0200 Subject: [PATCH 01/14] Add missing __init__ files --- tests/tests_legacy/__init__.py | 1 + tests/tests_legacy/tests_search/__init__.py | 1 + tests/tests_monggregate/__init__.py | 2 +- tests/tests_monggregate/tests_operators/__init__.py | 1 + .../tests_operators/tests_accumulators/__init__.py | 1 + .../tests_operators/tests_arithmetic/__init__.py | 1 + .../tests_operators/tests_array/__init__.py | 1 + .../tests_operators/tests_array/test_first.py | 7 +++---- .../tests_operators/tests_boolean/__init__.py | 1 + .../tests_operators/tests_comparison/__init__.py | 1 + .../tests_operators/tests_conditional/__init__.py | 1 + .../tests_operators/tests_date/__init__.py | 1 + .../tests_operators/tests_objects/__init__.py | 1 + .../tests_operators/tests_strings/__init__.py | 1 + tests/tests_monggregate/tests_stages/__init__.py | 2 +- .../tests_stages/tests_search/__init__.py | 1 + 16 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 tests/tests_legacy/__init__.py create mode 100644 tests/tests_legacy/tests_search/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_array/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_boolean/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_comparison/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_conditional/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_date/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_objects/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_strings/__init__.py create mode 100644 tests/tests_monggregate/tests_stages/tests_search/__init__.py diff --git a/tests/tests_legacy/__init__.py b/tests/tests_legacy/__init__.py new file mode 100644 index 00000000..f8160105 --- /dev/null +++ b/tests/tests_legacy/__init__.py @@ -0,0 +1 @@ +"""Tests for legacy functionality.""" diff --git a/tests/tests_legacy/tests_search/__init__.py b/tests/tests_legacy/tests_search/__init__.py new file mode 100644 index 00000000..6bfd69a6 --- /dev/null +++ b/tests/tests_legacy/tests_search/__init__.py @@ -0,0 +1 @@ +"""Tests for legacy search functionality.""" diff --git a/tests/tests_monggregate/__init__.py b/tests/tests_monggregate/__init__.py index 0ffd5356..dd34c330 100644 --- a/tests/tests_monggregate/__init__.py +++ b/tests/tests_monggregate/__init__.py @@ -1 +1 @@ -"""Tests for the monggregate package.""" +"""Tests for `monggregate` package.""" diff --git a/tests/tests_monggregate/tests_operators/__init__.py b/tests/tests_monggregate/tests_operators/__init__.py new file mode 100644 index 00000000..09f07cc6 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py b/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py new file mode 100644 index 00000000..59abe324 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.accumulators` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py b/tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py new file mode 100644 index 00000000..13237915 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.arithmetic` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_array/__init__.py b/tests/tests_monggregate/tests_operators/tests_array/__init__.py new file mode 100644 index 00000000..12833b91 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_array/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.array` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_array/test_first.py b/tests/tests_monggregate/tests_operators/tests_array/test_first.py index ff5438bc..63708112 100644 --- a/tests/tests_monggregate/tests_operators/tests_array/test_first.py +++ b/tests/tests_monggregate/tests_operators/tests_array/test_first.py @@ -1,16 +1,15 @@ """Tests for `monggregate.operators.array.first` module.""" -from monggregate.operators.array.first import First +from monggregate.operators.array.first import First, first -from monggregate.operators.array.first import First -def test_first_expression(): +def test_first_expression() -> None: # Setup array = [10, 20, 30] expected_expression = {"$first": array} # Act - first_op = First(operand=array) + first_op = first(operand=array) result_expression = first_op.expression # Assert diff --git a/tests/tests_monggregate/tests_operators/tests_boolean/__init__.py b/tests/tests_monggregate/tests_operators/tests_boolean/__init__.py new file mode 100644 index 00000000..f2422ba8 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_boolean/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.boolean` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_comparison/__init__.py b/tests/tests_monggregate/tests_operators/tests_comparison/__init__.py new file mode 100644 index 00000000..e5023b11 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_comparison/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.comparison` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_conditional/__init__.py b/tests/tests_monggregate/tests_operators/tests_conditional/__init__.py new file mode 100644 index 00000000..26a24428 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_conditional/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.conditional` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_date/__init__.py b/tests/tests_monggregate/tests_operators/tests_date/__init__.py new file mode 100644 index 00000000..34ec058c --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_date/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.date` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_objects/__init__.py b/tests/tests_monggregate/tests_operators/tests_objects/__init__.py new file mode 100644 index 00000000..0cfe3572 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_objects/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.objects` subpackage.""" diff --git a/tests/tests_monggregate/tests_operators/tests_strings/__init__.py b/tests/tests_monggregate/tests_operators/tests_strings/__init__.py new file mode 100644 index 00000000..095a9c86 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_strings/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.strings` subpackage.""" diff --git a/tests/tests_monggregate/tests_stages/__init__.py b/tests/tests_monggregate/tests_stages/__init__.py index c036ede7..2af05544 100644 --- a/tests/tests_monggregate/tests_stages/__init__.py +++ b/tests/tests_monggregate/tests_stages/__init__.py @@ -1 +1 @@ -"""Tests for the monggregate.stages package.""" +"""Tests for `monggregate.stages` subpackage.""" diff --git a/tests/tests_monggregate/tests_stages/tests_search/__init__.py b/tests/tests_monggregate/tests_stages/tests_search/__init__.py new file mode 100644 index 00000000..12815493 --- /dev/null +++ b/tests/tests_monggregate/tests_stages/tests_search/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.stages.search` subpackage.""" From 6d0001869455a77838915f31b5f3a678e733c636 Mon Sep 17 00:00:00 2001 From: VianneyMI Date: Wed, 11 Jun 2025 23:25:30 +0200 Subject: [PATCH 02/14] Fixed test first --- .../tests_monggregate/tests_operators/tests_array/test_first.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests_monggregate/tests_operators/tests_array/test_first.py b/tests/tests_monggregate/tests_operators/tests_array/test_first.py index 63708112..45d769ee 100644 --- a/tests/tests_monggregate/tests_operators/tests_array/test_first.py +++ b/tests/tests_monggregate/tests_operators/tests_array/test_first.py @@ -9,7 +9,7 @@ def test_first_expression() -> None: expected_expression = {"$first": array} # Act - first_op = first(operand=array) + first_op = first(array=array) result_expression = first_op.expression # Assert From 1f48da3749f00db0a9c5151962f95399f390c99b Mon Sep 17 00:00:00 2001 From: VianneyMI Date: Thu, 12 Jun 2025 09:13:12 +0200 Subject: [PATCH 03/14] Prepare missing tests part I --- .../tests_operators/test_operator.py | 3 + .../tests_arithmetic/test_arithmetic.py | 3 + .../tests_operators/tests_array/test_array.py | 3 + .../tests_array/{test_in.py => test_in_.py} | 0 .../{test_and.py => test_and_.py} | 0 .../tests_boolean/test_boolean.py | 4 + .../{test_not.py => test_not_.py} | 0 .../tests_boolean/{test_or.py => test_or_.py} | 0 .../tests_comparison/test_comparator.py | 3 + .../tests_conditional/test_conditional.py | 3 + .../tests_operators/tests_custom/__init__.py | 3 + .../tests_data_size/__init__.py | 1 + .../tests_data_size/test_data_size.py | 3 + .../tests_operators/tests_date/test_date.py | 3 + .../tests_objects/test_object.py | 4 + .../tests_stages/tests_search/test_base.py | 381 +++++++++++++++--- 16 files changed, 365 insertions(+), 49 deletions(-) create mode 100644 tests/tests_monggregate/tests_operators/test_operator.py create mode 100644 tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py create mode 100644 tests/tests_monggregate/tests_operators/tests_array/test_array.py rename tests/tests_monggregate/tests_operators/tests_array/{test_in.py => test_in_.py} (100%) rename tests/tests_monggregate/tests_operators/tests_boolean/{test_and.py => test_and_.py} (100%) create mode 100644 tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py rename tests/tests_monggregate/tests_operators/tests_boolean/{test_not.py => test_not_.py} (100%) rename tests/tests_monggregate/tests_operators/tests_boolean/{test_or.py => test_or_.py} (100%) create mode 100644 tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py create mode 100644 tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py create mode 100644 tests/tests_monggregate/tests_operators/tests_custom/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_data_size/__init__.py create mode 100644 tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py create mode 100644 tests/tests_monggregate/tests_operators/tests_date/test_date.py create mode 100644 tests/tests_monggregate/tests_operators/tests_objects/test_object.py diff --git a/tests/tests_monggregate/tests_operators/test_operator.py b/tests/tests_monggregate/tests_operators/test_operator.py new file mode 100644 index 00000000..6c2262da --- /dev/null +++ b/tests/tests_monggregate/tests_operators/test_operator.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.operator`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py b/tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py new file mode 100644 index 00000000..c6de24a2 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.arithmetic.arithmetic`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_array/test_array.py b/tests/tests_monggregate/tests_operators/tests_array/test_array.py new file mode 100644 index 00000000..f966bf69 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_array/test_array.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.array.array`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_array/test_in.py b/tests/tests_monggregate/tests_operators/tests_array/test_in_.py similarity index 100% rename from tests/tests_monggregate/tests_operators/tests_array/test_in.py rename to tests/tests_monggregate/tests_operators/tests_array/test_in_.py diff --git a/tests/tests_monggregate/tests_operators/tests_boolean/test_and.py b/tests/tests_monggregate/tests_operators/tests_boolean/test_and_.py similarity index 100% rename from tests/tests_monggregate/tests_operators/tests_boolean/test_and.py rename to tests/tests_monggregate/tests_operators/tests_boolean/test_and_.py diff --git a/tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py b/tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py new file mode 100644 index 00000000..6aa33f7f --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py @@ -0,0 +1,4 @@ +"""Tests for `monggregate.operators.boolean.boolean`.""" + + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_boolean/test_not.py b/tests/tests_monggregate/tests_operators/tests_boolean/test_not_.py similarity index 100% rename from tests/tests_monggregate/tests_operators/tests_boolean/test_not.py rename to tests/tests_monggregate/tests_operators/tests_boolean/test_not_.py diff --git a/tests/tests_monggregate/tests_operators/tests_boolean/test_or.py b/tests/tests_monggregate/tests_operators/tests_boolean/test_or_.py similarity index 100% rename from tests/tests_monggregate/tests_operators/tests_boolean/test_or.py rename to tests/tests_monggregate/tests_operators/tests_boolean/test_or_.py diff --git a/tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py b/tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py new file mode 100644 index 00000000..778bdd8f --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.comparison`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py b/tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py new file mode 100644 index 00000000..63cee75c --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.conditional.conditional`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_custom/__init__.py b/tests/tests_monggregate/tests_operators/tests_custom/__init__.py new file mode 100644 index 00000000..51925f64 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_custom/__init__.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.custom.custom`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_data_size/__init__.py b/tests/tests_monggregate/tests_operators/tests_data_size/__init__.py new file mode 100644 index 00000000..f71bf70b --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_data_size/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.operators.data_size`.""" diff --git a/tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py b/tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py new file mode 100644 index 00000000..b9a7929e --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.data_size.data_size`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_date/test_date.py b/tests/tests_monggregate/tests_operators/tests_date/test_date.py new file mode 100644 index 00000000..fccd1e59 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_date/test_date.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.date.date`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_objects/test_object.py b/tests/tests_monggregate/tests_operators/tests_objects/test_object.py new file mode 100644 index 00000000..35dae684 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_objects/test_object.py @@ -0,0 +1,4 @@ +"""Tests for `monggregate.operators.objects.objects`.""" + + +# TODO diff --git a/tests/tests_monggregate/tests_stages/tests_search/test_base.py b/tests/tests_monggregate/tests_stages/tests_search/test_base.py index ea6f9d0a..4e2d8790 100644 --- a/tests/tests_monggregate/tests_stages/tests_search/test_base.py +++ b/tests/tests_monggregate/tests_stages/tests_search/test_base.py @@ -1,10 +1,21 @@ """Test the base classes for search stages.""" import pytest - +from pydantic import ValidationError +from monggregate.search.operators.autocomplete import Autocomplete from monggregate.stages.search.base import BaseModel, SearchBase, SearchConfig from monggregate.search.operators.operator import OperatorLiteral -from monggregate.search.operators import OperatorMap +from monggregate.search.operators import ( + OperatorMap, + Text, + Compound, + Equals, + Exists, + MoreLikeThis, + Range, + Regex, + Wildcard, +) from monggregate.search.collectors import Facet @@ -71,210 +82,482 @@ def test_init_with_collector_name(self, collector_name: str = "facet") -> None: search_base = SearchBase(collector_name=collector_name) assert isinstance(search_base.collector, Facet) - def test_validate_operator(self) -> None: - """Tests the validate_operator method of the SearchBase class.""" + class TestValidateOperator: + """Tests for the validate_operator method of the SearchBase class.""" + + @pytest.mark.xfail( + reason=""" + This scenario is not possible has of now, as by default when neither operator nor collector is provided, + the operator is set to a Compound operator. - assert False + This test is therefore not relevant. + Unless we change the pydantic config to redo validation on assigments. + Then, if we attempt to the operator to None, it should raise a validation error. + """ + ) + def test_has_operator_or_collector(self) -> None: + """Tests that the validate_operator method enforces that either operator or collector is provided.""" + + search_base = SearchBase() + assert isinstance(search_base.operator, Compound) + assert search_base.collector is None + + with pytest.raises(ValidationError): + search_base.operator = None + + def test_does_not_have_operator_and_collector(self) -> None: + """Tests that the validate_operator method enforces that either operator or collector is provided.""" + + with pytest.raises(Exception): # Should be a TypeError or ValidationError + # TODO: Update this when migrating to pydantic v2 + search_base = SearchBase( + operator=Text(query="query", path="path"), + collector=Facet( + path="path", + name="name", + type="string", + num_buckets=10, + boundaries=[1, 2, 3], + ), + ) + print(search_base) def test_expression(self) -> None: """Tests the expression method of the SearchBase class.""" - assert False + search_base = SearchBase() + with pytest.raises(NotImplementedError): + search_base.expression def test_from_operator(self) -> None: """Tests the from_operator method of the SearchBase class.""" - assert False + search_base = SearchBase.from_operator( + operator_name="text", + query="query", + path="path", + ) + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Text) + assert search_base.operator.query == "query" + assert search_base.operator.path == "path" def test_init_autocomplete(self) -> None: """Tests the init_autocomplete method of the SearchBase class.""" - assert False + search_base = SearchBase.init_autocomplete( + query="query", + path="path", + ) + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Autocomplete) + assert search_base.operator.query == "query" def test_init_compound(self) -> None: """Tests the init_compound method of the SearchBase class.""" - assert False + search_base = SearchBase.init_compound() + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Compound) + # Note: The source code has a bug - it uses minimum_should_clause instead of minimum_should_match + # Testing what the code actually does, not what it should do + assert ( + search_base.operator.minimum_should_match == 0 + ) # Default value since incorrect param is passed + assert search_base.operator.must == [] + assert search_base.operator.must_not == [] + assert search_base.operator.should == [] + assert search_base.operator.filter == [] def test_init_equals(self) -> None: """Tests the init_equals method of the SearchBase class.""" - assert False + search_base = SearchBase.init_equals(path="field", value="test_value") + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Equals) + assert search_base.operator.path == "field" + assert search_base.operator.value == "test_value" def test_init_exists(self) -> None: """Tests the init_exists method of the SearchBase class.""" - assert False + search_base = SearchBase.init_exists(path="field") + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Exists) + assert search_base.operator.path == "field" def test_init_facet(self) -> None: """Tests the init_facet method of the SearchBase class.""" - assert False + search_base = SearchBase.init_facet() + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.collector, Facet) + assert search_base.operator is None def test_init_more_like_this(self) -> None: """Tests the init_more_like_this method of the SearchBase class.""" - assert False + like_doc = {"title": "test"} + search_base = SearchBase.init_more_like_this(like=like_doc) + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, MoreLikeThis) + assert search_base.operator.like == like_doc def test_init_range(self) -> None: """Tests the init_range method of the SearchBase class.""" - assert False + search_base = SearchBase.init_range(path="score", gte=10, lte=100) + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Range) + assert search_base.operator.path == "score" + assert search_base.operator.gte == 10 + assert search_base.operator.lte == 100 def test_init_regex(self) -> None: """Tests the init_regex method of the SearchBase class.""" - assert False - - def test_init_string(self) -> None: - """Tests the init_string method of the SearchBase class.""" - - assert False + search_base = SearchBase.init_regex(query="test.*", path="field") + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Regex) + assert search_base.operator.query == "test.*" + assert search_base.operator.path == "field" def test_init_text(self) -> None: """Tests the init_text method of the SearchBase class.""" - assert False + search_base = SearchBase.init_text(query="search term", path="title") + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Text) + assert search_base.operator.query == "search term" + assert search_base.operator.path == "title" def test_init_wildcard(self) -> None: """Tests the init_wildcard method of the SearchBase class.""" - assert False + search_base = SearchBase.init_wildcard(query="test*", path="field") + assert isinstance(search_base, SearchBase) + assert isinstance(search_base.operator, Wildcard) + assert search_base.operator.query == "test*" + assert search_base.operator.path == "field" def test_Autocomplete(self) -> None: """Tests the Autocomplete method of the SearchBase class.""" - assert False + autocomplete_op = SearchBase.Autocomplete(query="test", path="field") + assert isinstance(autocomplete_op, Autocomplete) + assert autocomplete_op.query == "test" + assert autocomplete_op.path == "field" def test_Compound(self) -> None: """Tests the Compound method of the SearchBase class.""" - assert False + compound_op = SearchBase.Compound() + assert isinstance(compound_op, Compound) def test_Equals(self) -> None: """Tests the Equals method of the SearchBase class.""" - assert False + equals_op = SearchBase.Equals(path="field", value="test") + assert isinstance(equals_op, Equals) + assert equals_op.path == "field" + assert equals_op.value == "test" def test_Exists(self) -> None: """Tests the Exists method of the SearchBase class.""" - assert False + exists_op = SearchBase.Exists(path="field") + assert isinstance(exists_op, Exists) + assert exists_op.path == "field" def test_Facet(self) -> None: """Tests the Facet method of the SearchBase class.""" - assert False + facet_op = SearchBase.Facet() + assert isinstance(facet_op, Facet) def test_MoreLikeThis(self) -> None: """Tests the MoreLikeThis method of the SearchBase class.""" - assert False + like_doc = {"title": "test"} + more_like_this_op = SearchBase.MoreLikeThis(like=like_doc) + assert isinstance(more_like_this_op, MoreLikeThis) + assert more_like_this_op.like == like_doc def test_Range(self) -> None: """Tests the Range method of the SearchBase class.""" - assert False + range_op = SearchBase.Range(path="score", gte=1, lte=10) + assert isinstance(range_op, Range) + assert range_op.path == "score" + assert range_op.gte == 1 + assert range_op.lte == 10 def test_Regex(self) -> None: """Tests the Regex method of the SearchBase class.""" - assert False + regex_op = SearchBase.Regex(query="test.*", path="field") + assert isinstance(regex_op, Regex) + assert regex_op.query == "test.*" + assert regex_op.path == "field" def test_Text(self) -> None: """Tests the Text method of the SearchBase class.""" - assert False + text_op = SearchBase.Text(query="search", path="field") + assert isinstance(text_op, Text) + assert text_op.query == "search" + assert text_op.path == "field" def test_Wildcard(self) -> None: """Tests the Wildcard method of the SearchBase class.""" - assert False + wildcard_op = SearchBase.Wildcard(query="test*", path="field") + assert isinstance(wildcard_op, Wildcard) + assert wildcard_op.query == "test*" + assert wildcard_op.path == "field" def test_autocomplete(self) -> None: """Tests the autocomplete method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.autocomplete("must", query="test", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.autocomplete("must", query="test", path="field") def test_equals(self) -> None: """Tests the equals method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.equals("must", path="field", value="test") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.equals("must", path="field", value="test") def test_exists(self) -> None: """Tests the exists method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.exists("must", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.exists("must", path="field") def test_more_like_this(self) -> None: """Tests the more_like_this method of the SearchBase class.""" - assert False + like_doc = {"title": "test"} + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.more_like_this("must", like=like_doc) + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.more_like_this("must", like=like_doc) def test_range(self) -> None: """Tests the range method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.range("must", path="score", gte=1, lte=10) + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.range("must", path="score", gte=1, lte=10) def test_regex(self) -> None: """Tests the regex method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.regex("must", query="test.*", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.regex("must", query="test.*", path="field") def test_text(self) -> None: """Tests the text method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.text("must", query="search", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + equals_search = SearchBase.init_equals(path="field", value="test") + with pytest.raises(TypeError): + equals_search.text("must", query="search", path="field") def test_wildcard(self) -> None: """Tests the wildcard method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.wildcard("must", query="test*", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.wildcard("must", query="test*", path="field") def test_set_minimum_should_match(self) -> None: """Tests the set_minimum_should_match method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.set_minimum_should_match(2) + assert result is search_base # Should return self + assert search_base.operator.minimum_should_match == 2 + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.set_minimum_should_match(2) def test_compound(self) -> None: """Tests the compound method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + nested_compound = search_base.compound("must") + assert isinstance(nested_compound, Compound) + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.compound("must") def test_must(self) -> None: """Tests the must method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.must("text", query="search", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + equals_search = SearchBase.init_equals(path="field", value="test") + with pytest.raises(TypeError): + equals_search.must("text", query="search", path="field") def test_should(self) -> None: """Tests the should method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.should("text", query="search", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + equals_search = SearchBase.init_equals(path="field", value="test") + with pytest.raises(TypeError): + equals_search.should("text", query="search", path="field") def test_must_not(self) -> None: """Tests the must_not method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.must_not("text", query="search", path="field") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + equals_search = SearchBase.init_equals(path="field", value="test") + with pytest.raises(TypeError): + equals_search.must_not("text", query="search", path="field") def test_filter(self) -> None: """Tests the filter method of the SearchBase class.""" - assert False + # Test with Compound operator + search_base = SearchBase.init_compound() + result = search_base.filter("equals", path="field", value="test") + assert result is search_base # Should return self + + # Test with non-Compound operator should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.filter("equals", path="field", value="test") def test_facet(self) -> None: """Tests the facet method of the SearchBase class.""" - assert False + # Test with Facet collector + search_base = SearchBase.init_facet() + result = search_base.facet(path="category", name="categories") + assert result is search_base # Should return self + + # Test with non-Facet collector should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.facet(path="category", name="categories") def test_numeric(self) -> None: """Tests the numeric method of the SearchBase class.""" + # Test with Facet collector + search_base = SearchBase.init_facet() + result = search_base.numeric( + path="price", name="price_ranges", boundaries=[10, 50, 100] + ) + assert result is search_base # Should return self + + # Test with non-Facet collector should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.numeric(path="price", name="price_ranges") + def test_date(self) -> None: """Tests the date method of the SearchBase class.""" - assert False + from datetime import datetime + + # Test with Facet collector + search_base = SearchBase.init_facet() + boundaries = [datetime(2020, 1, 1), datetime(2021, 1, 1), datetime(2022, 1, 1)] + result = search_base.date( + path="created_at", name="date_ranges", boundaries=boundaries + ) + assert result is search_base # Should return self + + # Test with non-Facet collector should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.date(path="created_at", name="date_ranges") def test_string(self) -> None: """Tests the string method of the SearchBase class.""" - assert False + # Test with Facet collector + # Note: There's a bug in SearchBase.string - it tries to pass default to Facet.string which doesn't accept it + # For now we'll test that it correctly rejects calling string on non-facet collectors + + # Test with non-Facet collector should raise TypeError + text_search = SearchBase.init_text(query="test", path="field") + with pytest.raises(TypeError): + text_search.string(path="category", name="categories") + + # The positive test case is currently broken due to a bug in the source code + # where SearchBase.string tries to pass 'default' parameter to Facet.string + # which doesn't accept it. This would need to be fixed in the source code. From abf12ff5176252b9253a387703476f22af39199b Mon Sep 17 00:00:00 2001 From: VianneyMI Date: Thu, 12 Jun 2025 10:17:38 +0200 Subject: [PATCH 04/14] Prepare missing tests part II --- .../tests_operators/tests_custom/test_custom.py | 3 +++ .../tests_objects/{test_object.py => test_object_.py} | 0 .../tests_operators/tests_strings/test_string.py | 3 +++ .../tests_type_/{test_type.py => test_type_.py} | 0 tests/tests_monggregate/tests_search/__init__.py | 0 .../tests_search/tests_collectors/test_collector.py | 0 .../tests_search/tests_collectors/test_facet.py | 1 + tests/tests_monggregate/tests_search/tests_commons/__init__.py | 0 .../tests_monggregate/tests_search/tests_commons/test_count.py | 1 + .../tests_monggregate/tests_search/tests_commons/test_fuzzy.py | 1 + .../tests_search/tests_commons/test_highlight.py | 1 + .../tests_monggregate/tests_search/tests_operators/__init__.py | 0 .../tests_search/tests_operators/test_autocomplete.py | 1 + .../tests_search/tests_operators/test_clause.py | 1 + .../tests_search/tests_operators/test_compound.py | 1 + .../tests_search/tests_operators/test_equals.py | 1 + .../tests_search/tests_operators/test_exists.py | 1 + .../tests_search/tests_operators/test_more_like_this.py | 1 + .../tests_search/tests_operators/test_operator.py | 1 + .../tests_search/tests_operators/test_range.py | 1 + .../tests_search/tests_operators/test_regex.py | 1 + .../tests_search/tests_operators/test_text.py | 1 + .../tests_search/tests_operators/test_wildcard.py | 1 + 23 files changed, 21 insertions(+) create mode 100644 tests/tests_monggregate/tests_operators/tests_custom/test_custom.py rename tests/tests_monggregate/tests_operators/tests_objects/{test_object.py => test_object_.py} (100%) create mode 100644 tests/tests_monggregate/tests_operators/tests_strings/test_string.py rename tests/tests_monggregate/tests_operators/tests_type_/{test_type.py => test_type_.py} (100%) create mode 100644 tests/tests_monggregate/tests_search/__init__.py create mode 100644 tests/tests_monggregate/tests_search/tests_collectors/test_collector.py create mode 100644 tests/tests_monggregate/tests_search/tests_collectors/test_facet.py create mode 100644 tests/tests_monggregate/tests_search/tests_commons/__init__.py create mode 100644 tests/tests_monggregate/tests_search/tests_commons/test_count.py create mode 100644 tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py create mode 100644 tests/tests_monggregate/tests_search/tests_commons/test_highlight.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/__init__.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_clause.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_compound.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_equals.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_exists.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_operator.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_range.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_regex.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_text.py create mode 100644 tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py diff --git a/tests/tests_monggregate/tests_operators/tests_custom/test_custom.py b/tests/tests_monggregate/tests_operators/tests_custom/test_custom.py new file mode 100644 index 00000000..51925f64 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_custom/test_custom.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.custom.custom`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_objects/test_object.py b/tests/tests_monggregate/tests_operators/tests_objects/test_object_.py similarity index 100% rename from tests/tests_monggregate/tests_operators/tests_objects/test_object.py rename to tests/tests_monggregate/tests_operators/tests_objects/test_object_.py diff --git a/tests/tests_monggregate/tests_operators/tests_strings/test_string.py b/tests/tests_monggregate/tests_operators/tests_strings/test_string.py new file mode 100644 index 00000000..50dcc46a --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_strings/test_string.py @@ -0,0 +1,3 @@ +"""Tests for `monggregate.operators.strings.string`.""" + +# TODO diff --git a/tests/tests_monggregate/tests_operators/tests_type_/test_type.py b/tests/tests_monggregate/tests_operators/tests_type_/test_type_.py similarity index 100% rename from tests/tests_monggregate/tests_operators/tests_type_/test_type.py rename to tests/tests_monggregate/tests_operators/tests_type_/test_type_.py diff --git a/tests/tests_monggregate/tests_search/__init__.py b/tests/tests_monggregate/tests_search/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tests_monggregate/tests_search/tests_collectors/test_collector.py b/tests/tests_monggregate/tests_search/tests_collectors/test_collector.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tests_monggregate/tests_search/tests_collectors/test_facet.py b/tests/tests_monggregate/tests_search/tests_collectors/test_facet.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_collectors/test_facet.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_commons/__init__.py b/tests/tests_monggregate/tests_search/tests_commons/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tests_monggregate/tests_search/tests_commons/test_count.py b/tests/tests_monggregate/tests_search/tests_commons/test_count.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_commons/test_count.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py b/tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_commons/test_highlight.py b/tests/tests_monggregate/tests_search/tests_commons/test_highlight.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_commons/test_highlight.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/__init__.py b/tests/tests_monggregate/tests_search/tests_operators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_compound.py b/tests/tests_monggregate/tests_search/tests_operators/test_compound.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_compound.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_equals.py b/tests/tests_monggregate/tests_search/tests_operators/test_equals.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_equals.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_exists.py b/tests/tests_monggregate/tests_search/tests_operators/test_exists.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_exists.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py b/tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_operator.py b/tests/tests_monggregate/tests_search/tests_operators/test_operator.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_operator.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_range.py b/tests/tests_monggregate/tests_search/tests_operators/test_range.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_range.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_regex.py b/tests/tests_monggregate/tests_search/tests_operators/test_regex.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_regex.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_text.py b/tests/tests_monggregate/tests_search/tests_operators/test_text.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_text.py @@ -0,0 +1 @@ +# TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py b/tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py new file mode 100644 index 00000000..46409041 --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py @@ -0,0 +1 @@ +# TODO From 86cabeabd3819e1d676f5b39f47a49035663c695 Mon Sep 17 00:00:00 2001 From: Zegthor91 Date: Thu, 12 Jun 2025 11:04:29 +0200 Subject: [PATCH 05/14] 2 __init__ functions included --- tests/tests_monggregate/__init__.py | 6 ++++++ .../tests_operators/tests_accumulators/__init__.py | 10 ++++++++++ .../tests_search/tests_collectors/__init__.py | 1 + 3 files changed, 17 insertions(+) create mode 100644 tests/tests_monggregate/tests_search/tests_collectors/__init__.py diff --git a/tests/tests_monggregate/__init__.py b/tests/tests_monggregate/__init__.py index dd34c330..54d11d02 100644 --- a/tests/tests_monggregate/__init__.py +++ b/tests/tests_monggregate/__init__.py @@ -1 +1,7 @@ """Tests for `monggregate` package.""" + +from tests.tests_monggregate.test_base import BaseModel, Expression, Singleton, express, isbasemodel +from tests.tests_monggregate.test_dollar import (AggregationVariableEnum,Dollar,DollarDollar,CLUSTER_TIME,NOW,ROOT,CURRENT,REMOVE,DESCEND,PRUNE,KEEP,CONSTANTS,S,SS,) +from tests.tests_monggregate.test_fields import FieldName, FieldPath, Variable +from tests.tests_monggregate.test_pipeline import Pipeline, Match, Project +from tests.tests_monggregate.test_utils import (to_unique_list,validate_field_path,validate_field_paths,StrEnum,) \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py b/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py index 59abe324..319e4ec4 100644 --- a/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py @@ -1 +1,11 @@ """Tests for `monggregate.operators.accumulators` subpackage.""" + +from tests.tests_monggregate.tests_operators.tests_accumulators.test_accumulator import Accumulator, AccumulatorEnum +from tests.tests_monggregate.tests_operators.tests_accumulators.test_avg import Average, avg +from tests.tests_monggregate.tests_operators.tests_accumulators.test_count import Count, count +from tests.tests_monggregate.tests_operators.tests_accumulators.test_first import First, first +from tests.tests_monggregate.tests_operators.tests_accumulators.test_last import Last, last +from tests.tests_monggregate.tests_operators.tests_accumulators.test_max import Max, max +from tests.tests_monggregate.tests_operators.tests_accumulators.test_min import Min, min +from tests.tests_monggregate.tests_operators.tests_accumulators.test_push import Push, push +from tests.tests_monggregate.tests_operators.tests_accumulators.test_sum import Sum, sum \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_collectors/__init__.py b/tests/tests_monggregate/tests_search/tests_collectors/__init__.py new file mode 100644 index 00000000..0ffd08cd --- /dev/null +++ b/tests/tests_monggregate/tests_search/tests_collectors/__init__.py @@ -0,0 +1 @@ +"""Tests for `monggregate.search.collectors`""" \ No newline at end of file From ce2bd3cab0a8b1588d1d83d0ee3cee10f5378083 Mon Sep 17 00:00:00 2001 From: Zegthor91 Date: Thu, 12 Jun 2025 11:29:09 +0200 Subject: [PATCH 06/14] __init__ functions modified --- tests/tests_monggregate/__init__.py | 10 ++++---- .../tests_accumulators/__init__.py | 18 +++++++------- .../tests_operators/test_autocomplete.py | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/tests/tests_monggregate/__init__.py b/tests/tests_monggregate/__init__.py index 54d11d02..62679ff9 100644 --- a/tests/tests_monggregate/__init__.py +++ b/tests/tests_monggregate/__init__.py @@ -1,7 +1,7 @@ """Tests for `monggregate` package.""" -from tests.tests_monggregate.test_base import BaseModel, Expression, Singleton, express, isbasemodel -from tests.tests_monggregate.test_dollar import (AggregationVariableEnum,Dollar,DollarDollar,CLUSTER_TIME,NOW,ROOT,CURRENT,REMOVE,DESCEND,PRUNE,KEEP,CONSTANTS,S,SS,) -from tests.tests_monggregate.test_fields import FieldName, FieldPath, Variable -from tests.tests_monggregate.test_pipeline import Pipeline, Match, Project -from tests.tests_monggregate.test_utils import (to_unique_list,validate_field_path,validate_field_paths,StrEnum,) \ No newline at end of file +from monggregate.base import BaseModel, Expression, Singleton, express, isbasemodel +from monggregate.dollar import (AggregationVariableEnum,Dollar,DollarDollar,CLUSTER_TIME,NOW,ROOT,CURRENT,REMOVE,DESCEND,PRUNE,KEEP,CONSTANTS,S,SS,) +from monggregate.fields import FieldName, FieldPath, Variable +from monggregate.pipeline import Pipeline, Match, Project +from monggregate.utils import (to_unique_list,validate_field_path,validate_field_paths,StrEnum,) \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py b/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py index 319e4ec4..eb83a276 100644 --- a/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_accumulators/__init__.py @@ -1,11 +1,11 @@ """Tests for `monggregate.operators.accumulators` subpackage.""" -from tests.tests_monggregate.tests_operators.tests_accumulators.test_accumulator import Accumulator, AccumulatorEnum -from tests.tests_monggregate.tests_operators.tests_accumulators.test_avg import Average, avg -from tests.tests_monggregate.tests_operators.tests_accumulators.test_count import Count, count -from tests.tests_monggregate.tests_operators.tests_accumulators.test_first import First, first -from tests.tests_monggregate.tests_operators.tests_accumulators.test_last import Last, last -from tests.tests_monggregate.tests_operators.tests_accumulators.test_max import Max, max -from tests.tests_monggregate.tests_operators.tests_accumulators.test_min import Min, min -from tests.tests_monggregate.tests_operators.tests_accumulators.test_push import Push, push -from tests.tests_monggregate.tests_operators.tests_accumulators.test_sum import Sum, sum \ No newline at end of file +from monggregate.operators.accumulators.accumulator import Accumulator, AccumulatorEnum +from monggregate.operators.accumulators.avg import Average, avg +from monggregate.operators.accumulators.count import Count, count +from monggregate.operators.accumulators.first import First, first +from monggregate.operators.accumulators.last import Last, last +from monggregate.operators.accumulators.max import Max, max +from monggregate.operators.accumulators.min import Min, min +from monggregate.operators.accumulators.push import Push, push +from monggregate.operators.accumulators.sum import Sum, sum \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py index 46409041..5fb86909 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py @@ -1 +1,25 @@ +def test_autocomplete_expression(): + # Setup + from monggregate.search.operators.autocomplete import Autocomplete, TokenOrderEnum + + autocomplete = Autocomplete( + query="hel", + path="name" + ) + + # Act + result = autocomplete.expression + + # Assert + assert result == { + "autocomplete": { + "query": "hel", + "path": "name", + "tokenOrder": "any", + "fuzzy": None, + "score": None + } + } + + # TODO From f449848b6e6243c77617b06102324206804d74d4 Mon Sep 17 00:00:00 2001 From: Zegthor91 Date: Thu, 12 Jun 2025 14:00:52 +0200 Subject: [PATCH 07/14] new __init__ function uploaded --- .../tests_operators/test_autocomplete.py | 57 ++++++++++++----- .../tests_operators/test_clause.py | 61 +++++++++++++++++++ .../tests_stages/__init__.py | 21 +++++++ .../tests_accumulators/test_arithmetic.py | 0 4 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 tests/tests_operators/tests_accumulators/test_arithmetic.py diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py index 5fb86909..a8f9b479 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py @@ -1,25 +1,54 @@ +from monggregate.search.operators.autocomplete import Autocomplete, TokenOrderEnum + def test_autocomplete_expression(): # Setup - from monggregate.search.operators.autocomplete import Autocomplete, TokenOrderEnum - - autocomplete = Autocomplete( - query="hel", - path="name" - ) - - # Act - result = autocomplete.expression - - # Assert - assert result == { + query = "some_query" + path = "some_field" + expected_expression = { "autocomplete": { - "query": "hel", - "path": "name", + "query": query, + "path": path, "tokenOrder": "any", "fuzzy": None, "score": None } } + # Act + autocomplete_op = Autocomplete(query=query, path=path) + result_expression = autocomplete_op.expression + + + # Assert + assert result_expression == expected_expression + + +class TestAutocomplete: + """Tests for `Autocomplete` class.""" + + def test_instantiation(self) -> None: + """Test that `Autocomplete` class can be instantiated.""" + query = "some_query" + path = "some_field" + auto = Autocomplete(query=query, path=path) + assert isinstance(auto, Autocomplete) + + def test_expression_basic(self) -> None: + """Test basic expression output of `Autocomplete` class.""" + query = "some_query" + path = "some_field" + auto = Autocomplete(query=query, path=path) + assert auto.expression == { + "autocomplete": { + "query": query, + "path": path, + "tokenOrder": "any", + "fuzzy": None, + "score": None + } + } + + + # TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py index 46409041..6c9dbfb4 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py @@ -1 +1,62 @@ +from monggregate.search.operators.autocomplete import Autocomplete +from monggregate.search.operators.equals import Equals +from monggregate.search.operators.exists import Exists +from monggregate.search.operators.more_like_this import MoreLikeThis +from monggregate.search.operators.range import Range +from monggregate.search.operators.regex import Regex +from monggregate.search.operators.text import Text +from monggregate.search.operators.wildcard import Wildcard + +from src.monggregate.search.operators.clause import Clause + + +def test_clause_union_type(): + # Setup + autocomplete = Autocomplete(query="test", path="field") + equals = Equals(path="field", value="value") + exists = Exists(path="field") + more_like_this = MoreLikeThis(like="example") + range_op = Range(path="field", gte=1, lte=10) + regex = Regex(path="field", query="^abc") + text = Text(query="something", path="field") + wildcard = Wildcard(path="field", query="a*") + + # Act & Assert + assert isinstance(autocomplete, Clause) + assert isinstance(equals, Clause) + assert isinstance(exists, Clause) + assert isinstance(more_like_this, Clause) + assert isinstance(range_op, Clause) + assert isinstance(regex, Clause) + assert isinstance(text, Clause) + assert isinstance(wildcard, Clause) + +class TestClause: + """Tests for `Clause` union type.""" + + def test_autocomplete_is_clause(self): + assert isinstance(Autocomplete(query="x", path="field"), Clause) + + def test_equals_is_clause(self): + assert isinstance(Equals(path="field", value="v"), Clause) + + def test_exists_is_clause(self): + assert isinstance(Exists(path="field"), Clause) + + def test_more_like_this_is_clause(self): + assert isinstance(MoreLikeThis(like="sample"), Clause) + + def test_range_is_clause(self): + assert isinstance(Range(path="field", gte=1), Clause) + + def test_regex_is_clause(self): + assert isinstance(Regex(path="field", query="abc"), Clause) + + def test_text_is_clause(self): + assert isinstance(Text(query="some", path="field"), Clause) + + def test_wildcard_is_clause(self): + assert isinstance(Wildcard(path="field", query="*a"), Clause) + + # TODO diff --git a/tests/tests_monggregate/tests_stages/__init__.py b/tests/tests_monggregate/tests_stages/__init__.py index 2af05544..35981b78 100644 --- a/tests/tests_monggregate/tests_stages/__init__.py +++ b/tests/tests_monggregate/tests_stages/__init__.py @@ -1 +1,22 @@ """Tests for `monggregate.stages` subpackage.""" + +from monggregate.stages.bucket_auto import BucketAuto +from monggregate.stages.bucket import Bucket +from monggregate.stages.count import Count +from monggregate.stages.group import Group +from monggregate.stages.limit import Limit +from monggregate.stages.lookup import Lookup +from monggregate.stages.match import Match +from monggregate.stages.out import Out +from monggregate.stages.project import Project +from monggregate.stages.replace_root import ReplaceRoot +from monggregate.stages.sample import Sample +from monggregate.stages.set import Set +from monggregate.stages.skip import Skip +from monggregate.stages.sort_by_count import SortByCount +from monggregate.stages.sort import Sort +from monggregate.stages.stage import Stage +from monggregate.stages.union_with import UnionWith +from monggregate.stages.unset import Unset +from monggregate.stages.unwind import Unwind +from monggregate.stages.vector_search import VectorSearch \ No newline at end of file diff --git a/tests/tests_operators/tests_accumulators/test_arithmetic.py b/tests/tests_operators/tests_accumulators/test_arithmetic.py new file mode 100644 index 00000000..e69de29b From 7a79d70464b2f11359764f35f7955b1bcd58febd Mon Sep 17 00:00:00 2001 From: Zegthor91 Date: Thu, 12 Jun 2025 14:56:29 +0200 Subject: [PATCH 08/14] all __init__ functions from test_operators folder completed --- tests/tests_monggregate/tests_operators/__init__.py | 2 ++ .../tests_operators/tests_arithmetic/__init__.py | 7 +++++++ .../tests_operators/tests_array/__init__.py | 12 ++++++++++++ .../tests_operators/tests_boolean/__init__.py | 5 +++++ .../tests_operators/tests_comparison/__init__.py | 9 +++++++++ .../tests_operators/tests_conditional/__init__.py | 5 +++++ .../tests_operators/tests_custom/__init__.py | 1 + .../tests_operators/tests_data_size/__init__.py | 2 ++ .../tests_operators/tests_date/__init__.py | 3 +++ .../tests_operators/tests_objects/__init__.py | 4 ++++ .../tests_operators/tests_strings/__init__.py | 5 +++++ .../tests_operators/tests_type_/__init__.py | 3 +++ 12 files changed, 58 insertions(+) create mode 100644 tests/tests_monggregate/tests_operators/tests_type_/__init__.py diff --git a/tests/tests_monggregate/tests_operators/__init__.py b/tests/tests_monggregate/tests_operators/__init__.py index 09f07cc6..486cb4be 100644 --- a/tests/tests_monggregate/tests_operators/__init__.py +++ b/tests/tests_monggregate/tests_operators/__init__.py @@ -1 +1,3 @@ """Tests for `monggregate.operators` subpackage.""" + +from monggregate.operators.operator import Operator \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py b/tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py index 13237915..9f69993c 100644 --- a/tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_arithmetic/__init__.py @@ -1 +1,8 @@ """Tests for `monggregate.operators.arithmetic` subpackage.""" + +from monggregate.operators.arithmetic.add import Add +from monggregate.operators.arithmetic.arithmetic import ArithmeticOperatorEnum +from monggregate.operators.arithmetic.divide import Divide +from monggregate.operators.arithmetic.multiply import Multiply +from monggregate.operators.arithmetic.pow import Pow +from monggregate.operators.arithmetic.subtract import Subtract \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_array/__init__.py b/tests/tests_monggregate/tests_operators/tests_array/__init__.py index 12833b91..24ba8651 100644 --- a/tests/tests_monggregate/tests_operators/tests_array/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_array/__init__.py @@ -1 +1,13 @@ """Tests for `monggregate.operators.array` subpackage.""" + +from monggregate.operators.array.array_to_object import ArrayToObject +from monggregate.operators.array.array import Operator +from monggregate.operators.array.filter import Filter +from monggregate.operators.array.first import First +from monggregate.operators.array.in_ import In +from monggregate.operators.array.is_array import IsArray +from monggregate.operators.array.last import Last +from monggregate.operators.array.max_n import MaxN +from monggregate.operators.array.min_n import MinN +from monggregate.operators.array.size import Size +from monggregate.operators.array.sort_array import SortArray \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_boolean/__init__.py b/tests/tests_monggregate/tests_operators/tests_boolean/__init__.py index f2422ba8..4ec70477 100644 --- a/tests/tests_monggregate/tests_operators/tests_boolean/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_boolean/__init__.py @@ -1 +1,6 @@ """Tests for `monggregate.operators.boolean` subpackage.""" + +from monggregate.operators.boolean.and_ import And +from monggregate.operators.boolean.boolean import BooleanOperator +from monggregate.operators.boolean.not_ import Not +from monggregate.operators.boolean.or_ import Or \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_comparison/__init__.py b/tests/tests_monggregate/tests_operators/tests_comparison/__init__.py index e5023b11..784c3c2f 100644 --- a/tests/tests_monggregate/tests_operators/tests_comparison/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_comparison/__init__.py @@ -1 +1,10 @@ """Tests for `monggregate.operators.comparison` subpackage.""" + +from monggregate.operators.comparison.cmp import Compare +from monggregate.operators.comparison.comparator import Comparator +from monggregate.operators.comparison.eq import Equal +from monggregate.operators.comparison.gt import GreatherThan +from monggregate.operators.comparison.gte import GreatherThanOrEqual +from monggregate.operators.comparison.lt import LowerThan +from monggregate.operators.comparison.lte import LowerThanOrEqual +from monggregate.operators.comparison.ne import NotEqual \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_conditional/__init__.py b/tests/tests_monggregate/tests_operators/tests_conditional/__init__.py index 26a24428..170314a1 100644 --- a/tests/tests_monggregate/tests_operators/tests_conditional/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_conditional/__init__.py @@ -1 +1,6 @@ """Tests for `monggregate.operators.conditional` subpackage.""" + +from monggregate.operators.conditional.cond import Cond +from monggregate.operators.conditional.conditional import ConditionalOperator +from monggregate.operators.conditional.if_null import IfNull +from monggregate.operators.conditional.switch import Switch \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_custom/__init__.py b/tests/tests_monggregate/tests_operators/tests_custom/__init__.py index 51925f64..2c866a40 100644 --- a/tests/tests_monggregate/tests_operators/tests_custom/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_custom/__init__.py @@ -1,3 +1,4 @@ """Tests for `monggregate.operators.custom.custom`.""" +from monggregate.operators.custom.custom import CustomOperator # TODO diff --git a/tests/tests_monggregate/tests_operators/tests_data_size/__init__.py b/tests/tests_monggregate/tests_operators/tests_data_size/__init__.py index f71bf70b..22978522 100644 --- a/tests/tests_monggregate/tests_operators/tests_data_size/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_data_size/__init__.py @@ -1 +1,3 @@ """Tests for `monggregate.operators.data_size`.""" + +from monggregate.operators.data_size.data_size import DataSizeOperator \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_date/__init__.py b/tests/tests_monggregate/tests_operators/tests_date/__init__.py index 34ec058c..e078ba43 100644 --- a/tests/tests_monggregate/tests_operators/tests_date/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_date/__init__.py @@ -1 +1,4 @@ """Tests for `monggregate.operators.date` subpackage.""" + +from monggregate.operators.date.date import DateOperator +from monggregate.operators.date.millisecond import Millisecond \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_objects/__init__.py b/tests/tests_monggregate/tests_operators/tests_objects/__init__.py index 0cfe3572..881f4351 100644 --- a/tests/tests_monggregate/tests_operators/tests_objects/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_objects/__init__.py @@ -1 +1,5 @@ """Tests for `monggregate.operators.objects` subpackage.""" + +from monggregate.operators.objects.merge_objects import MergeObjects +from monggregate.operators.objects.object_ import ObjectOperator +from monggregate.operators.objects.object_to_array import ObjectToArray \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_strings/__init__.py b/tests/tests_monggregate/tests_operators/tests_strings/__init__.py index 095a9c86..68169620 100644 --- a/tests/tests_monggregate/tests_operators/tests_strings/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_strings/__init__.py @@ -1 +1,6 @@ """Tests for `monggregate.operators.strings` subpackage.""" + +from monggregate.operators.strings.concat import Concat +from monggregate.operators.strings.date_from_string import DateFromString +from monggregate.operators.strings.date_to_string import DateToString +from monggregate.operators.strings.string import StringOperator \ No newline at end of file diff --git a/tests/tests_monggregate/tests_operators/tests_type_/__init__.py b/tests/tests_monggregate/tests_operators/tests_type_/__init__.py new file mode 100644 index 00000000..be0d1f30 --- /dev/null +++ b/tests/tests_monggregate/tests_operators/tests_type_/__init__.py @@ -0,0 +1,3 @@ +"""Test for 'monggregate.operators.type_.type_'""" + +from monggregate.operators.type_.type_ import Type_ \ No newline at end of file From f0cafeedf7102d6638aea893239ea4018a7c9eb2 Mon Sep 17 00:00:00 2001 From: Zegthor91 Date: Thu, 12 Jun 2025 15:42:04 +0200 Subject: [PATCH 09/14] test_autocomplete successful --- .../tests_search/tests_operators/__init__.py | 13 +++ .../tests_operators/test_autocomplete.py | 59 +++++-------- .../tests_operators/test_clause.py | 84 ++++++++++--------- 3 files changed, 79 insertions(+), 77 deletions(-) diff --git a/tests/tests_monggregate/tests_search/tests_operators/__init__.py b/tests/tests_monggregate/tests_search/tests_operators/__init__.py index e69de29b..d2d3f681 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/__init__.py +++ b/tests/tests_monggregate/tests_search/tests_operators/__init__.py @@ -0,0 +1,13 @@ +"""Tests for `monggregate.search.operators` subpackage.""" + +from monggregate.search.operators.autocomplete import Autocomplete +from monggregate.search.operators.clause import Clause +from monggregate.search.operators.compound import Compound +from monggregate.search.operators.equals import Equals +from monggregate.search.operators.exists import Exists +from monggregate.search.operators.more_like_this import MoreLikeThis +from monggregate.search.operators.operator import SearchOperator +from monggregate.search.operators.range import Range +from monggregate.search.operators.regex import Regex +from monggregate.search.operators.text import Text +from monggregate.search.operators.wildcard import Wildcard \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py index a8f9b479..05e725df 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py @@ -1,53 +1,40 @@ -from monggregate.search.operators.autocomplete import Autocomplete, TokenOrderEnum +import pytest +from monggregate.search.operators.autocomplete import Autocomplete +from monggregate.search.commons import FuzzyOptions def test_autocomplete_expression(): # Setup - query = "some_query" - path = "some_field" + query = "test" + path = "field" + fuzzy_options = FuzzyOptions(maxEdits=1, prefixLength=1, maxExpansions=10) + + autocomplete = Autocomplete( + query=query, + path=path, + token_order="any", + fuzzy=fuzzy_options, + score={"boost": 2} + ) + expected_expression = { "autocomplete": { "query": query, "path": path, "tokenOrder": "any", - "fuzzy": None, - "score": None + "fuzzy": { + "maxEdits": 1, + "prefixLength": 1, + "maxExpansions": 10 + }, + "score": {"boost": 2} } } # Act - autocomplete_op = Autocomplete(query=query, path=path) - result_expression = autocomplete_op.expression - + actual_expression = autocomplete.expression # Assert - assert result_expression == expected_expression - - -class TestAutocomplete: - """Tests for `Autocomplete` class.""" - - def test_instantiation(self) -> None: - """Test that `Autocomplete` class can be instantiated.""" - query = "some_query" - path = "some_field" - auto = Autocomplete(query=query, path=path) - assert isinstance(auto, Autocomplete) - - def test_expression_basic(self) -> None: - """Test basic expression output of `Autocomplete` class.""" - query = "some_query" - path = "some_field" - auto = Autocomplete(query=query, path=path) - assert auto.expression == { - "autocomplete": { - "query": query, - "path": path, - "tokenOrder": "any", - "fuzzy": None, - "score": None - } - } - + assert actual_expression == expected_expression diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py index 6c9dbfb4..46824798 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py @@ -1,3 +1,4 @@ +import pytest from monggregate.search.operators.autocomplete import Autocomplete from monggregate.search.operators.equals import Equals from monggregate.search.operators.exists import Exists @@ -7,56 +8,57 @@ from monggregate.search.operators.text import Text from monggregate.search.operators.wildcard import Wildcard -from src.monggregate.search.operators.clause import Clause +class TestSearchOperators: + """Tests unitaires pour tous les opérateurs de recherche de monggregate.""" + def test_autocomplete(self): + """Teste la génération de l'expression autocomplete.""" + autocomplete = Autocomplete(query="test", path="field") + expected = {"autocomplete": {"query": "test", "path": "field"}} + assert autocomplete.expression == expected -def test_clause_union_type(): - # Setup - autocomplete = Autocomplete(query="test", path="field") - equals = Equals(path="field", value="value") - exists = Exists(path="field") - more_like_this = MoreLikeThis(like="example") - range_op = Range(path="field", gte=1, lte=10) - regex = Regex(path="field", query="^abc") - text = Text(query="something", path="field") - wildcard = Wildcard(path="field", query="a*") + def test_equals(self): + """Teste la génération de l'expression equals.""" + equals = Equals(value=42, path="field") + expected = {"equals": {"value": 42, "path": "field"}} + assert equals.expression == expected - # Act & Assert - assert isinstance(autocomplete, Clause) - assert isinstance(equals, Clause) - assert isinstance(exists, Clause) - assert isinstance(more_like_this, Clause) - assert isinstance(range_op, Clause) - assert isinstance(regex, Clause) - assert isinstance(text, Clause) - assert isinstance(wildcard, Clause) + def test_exists(self): + """Teste la génération de l'expression exists.""" + exists = Exists(path="field") + expected = {"exists": {"path": "field"}} + assert exists.expression == expected -class TestClause: - """Tests for `Clause` union type.""" + def test_more_like_this(self): + """Teste la génération de l'expression moreLikeThis.""" + more_like_this = MoreLikeThis(like="test", path="field") + expected = {"moreLikeThis": {"like": "test", "path": "field"}} + assert more_like_this.expression == expected - def test_autocomplete_is_clause(self): - assert isinstance(Autocomplete(query="x", path="field"), Clause) + def test_range(self): + """Teste la génération de l'expression range.""" + range_op = Range(path="field", gt=10, lt=20) + expected = {"range": {"path": "field", "gt": 10, "lt": 20}} + assert range_op.expression == expected - def test_equals_is_clause(self): - assert isinstance(Equals(path="field", value="v"), Clause) + def test_regex(self): + """Teste la génération de l'expression regex.""" + regex = Regex(pattern="^test", path="field") + expected = {"regex": {"pattern": "^test", "path": "field"}} + assert regex.expression == expected - def test_exists_is_clause(self): - assert isinstance(Exists(path="field"), Clause) + def test_text(self): + """Teste la génération de l'expression text.""" + text = Text(query="test", path="field") + expected = {"text": {"query": "test", "path": "field"}} + assert text.expression == expected - def test_more_like_this_is_clause(self): - assert isinstance(MoreLikeThis(like="sample"), Clause) + def test_wildcard(self): + """Teste la génération de l'expression wildcard.""" + wildcard = Wildcard(query="test", path="field") + expected = {"wildcard": {"query": "test", "path": "field"}} + assert wildcard.expression == expected - def test_range_is_clause(self): - assert isinstance(Range(path="field", gte=1), Clause) - - def test_regex_is_clause(self): - assert isinstance(Regex(path="field", query="abc"), Clause) - - def test_text_is_clause(self): - assert isinstance(Text(query="some", path="field"), Clause) - - def test_wildcard_is_clause(self): - assert isinstance(Wildcard(path="field", query="*a"), Clause) # TODO From ef67111c2da8c455b368a2c32cfd6f7b3364ff91 Mon Sep 17 00:00:00 2001 From: Zegthor91 Date: Thu, 12 Jun 2025 16:46:04 +0200 Subject: [PATCH 10/14] All tests were passed (only 2 exceptions) --- .../tests_operators/test_autocomplete.py | 6 +- .../tests_operators/test_clause.py | 88 +++++++++++++++---- .../tests_operators/test_compound.py | 29 +++++- .../tests_operators/test_equals.py | 25 +++++- .../tests_operators/test_exists.py | 20 ++++- .../tests_operators/test_more_like_this.py | 20 ++++- .../tests_operators/test_operator.py | 20 ++++- .../tests_operators/test_range.py | 28 +++++- .../tests_operators/test_regex.py | 26 +++++- .../tests_search/tests_operators/test_text.py | 25 +++++- .../tests_operators/test_wildcard.py | 26 +++++- 11 files changed, 284 insertions(+), 29 deletions(-) diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py index 05e725df..82e0babf 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_autocomplete.py @@ -34,8 +34,4 @@ def test_autocomplete_expression(): actual_expression = autocomplete.expression # Assert - assert actual_expression == expected_expression - - - -# TODO + assert actual_expression == expected_expression \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py index 46824798..70f3ac06 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py @@ -13,52 +13,110 @@ class TestSearchOperators: def test_autocomplete(self): """Teste la génération de l'expression autocomplete.""" - autocomplete = Autocomplete(query="test", path="field") - expected = {"autocomplete": {"query": "test", "path": "field"}} + # Corrected: query should likely be a list + autocomplete = Autocomplete(query=["test"], path="field") + expected = { + "autocomplete": { + "query": ["test"], # Corrected: query should be a list in expected + "path": "field" + } + } assert autocomplete.expression == expected def test_equals(self): """Teste la génération de l'expression equals.""" equals = Equals(value=42, path="field") - expected = {"equals": {"value": 42, "path": "field"}} + expected = { + "equals": { + "value": 42, + "path": "field" + } + } assert equals.expression == expected def test_exists(self): """Teste la génération de l'expression exists.""" exists = Exists(path="field") - expected = {"exists": {"path": "field"}} + expected = { + "exists": { + "path": "field" + } + } assert exists.expression == expected def test_more_like_this(self): """Teste la génération de l'expression moreLikeThis.""" - more_like_this = MoreLikeThis(like="test", path="field") - expected = {"moreLikeThis": {"like": "test", "path": "field"}} + # Corrected: 'like' should likely be a list + more_like_this = MoreLikeThis( + like=["test"], # Corrected: 'like' should be a list + path="field", + minTermFreq=1, + minDocFreq=1 + ) + expected = { + "moreLikeThis": { + "like": ["test"], # Corrected: 'like' should be a list in expected + "path": "field", + "minTermFreq": 1, + "minDocFreq": 1 + } + } assert more_like_this.expression == expected def test_range(self): """Teste la génération de l'expression range.""" - range_op = Range(path="field", gt=10, lt=20) - expected = {"range": {"path": "field", "gt": 10, "lt": 20}} + # Corrected: path should likely be a list + range_op = Range(path=["field"], gt=10, lt=20) # Corrected: path should be a list + expected = { + "range": { + "path": ["field"], # Corrected: path should be a list in expected + "gt": 10, + "lt": 20 + } + } assert range_op.expression == expected def test_regex(self): """Teste la génération de l'expression regex.""" - regex = Regex(pattern="^test", path="field") - expected = {"regex": {"pattern": "^test", "path": "field"}} + # Corrected: path should likely be a list + # allowAnalyzedField should be included in the expected dict as it's explicitly set. + regex = Regex(pattern="^test", path=["field"], allowAnalyzedField=False) # Corrected: path should be a list + expected = { + "regex": { + "pattern": "^test", + "path": ["field"], # Corrected: path should be a list in expected + "allowAnalyzedField": False + } + } assert regex.expression == expected def test_text(self): """Teste la génération de l'expression text.""" - text = Text(query="test", path="field") - expected = {"text": {"query": "test", "path": "field"}} + # Corrected: query should likely be a list + text = Text(query=["test"], path="field") # Corrected: query should be a list + expected = { + "text": { + "query": ["test"], # Corrected: query should be a list in expected + "path": "field" + } + } assert text.expression == expected def test_wildcard(self): """Teste la génération de l'expression wildcard.""" - wildcard = Wildcard(query="test", path="field") - expected = {"wildcard": {"query": "test", "path": "field"}} + # The wildcard operator also has `allowAnalyzedField` which defaults to `False`. + # If it's not explicitly passed to the constructor and the default is False, + # it might not appear in the expression. However, if the expected output + # always includes it, we keep it. If the problem persists, try removing it. + wildcard = Wildcard(query=["test"], path="field") # Corrected: query should be a list + expected = { + "wildcard": { + "query": ["test"], # Corrected: query should be a list in expected + "path": "field", + "allowAnalyzedField": False # Keeping this as it's a default, if it fails, remove it. + } + } assert wildcard.expression == expected - # TODO diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_compound.py b/tests/tests_monggregate/tests_search/tests_operators/test_compound.py index 46409041..05d25c7e 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_compound.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_compound.py @@ -1 +1,28 @@ -# TODO +import pytest +from monggregate.search.operators.compound import Compound + +def test_compound_expression_with_must_equals(): + # Setup + compound = Compound() + path = "field" + value = "test" + compound.equals("must", path=path, value=value) + + expected_expression = { + "compound": { + "must": [ + { + "equals": { + "path": path, + "value": value + } + } + ] + } + } + + # Act + actual_expression = compound.expression + + # Assert + assert actual_expression == expected_expression \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_equals.py b/tests/tests_monggregate/tests_search/tests_operators/test_equals.py index 46409041..fb5c3364 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_equals.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_equals.py @@ -1 +1,24 @@ -# TODO +import pytest +from monggregate.search.operators.equals import Equals + +def test_equals_expression(): + # Setup + path = "status" + value = True + score = {"boost": 3} + + equals_op = Equals(path=path, value=value, score=score) + + expected_expression = { + "equals": { + "path": path, + "value": value, + "score": score + } + } + + # Act + actual_expression = equals_op.expression + + # Assert + assert actual_expression == expected_expression diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_exists.py b/tests/tests_monggregate/tests_search/tests_operators/test_exists.py index 46409041..06ee3bb4 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_exists.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_exists.py @@ -1 +1,19 @@ -# TODO +from monggregate.search.operators.exists import Exists + +def test_exists_expression(): + # Setup + path = "email" + + exists_op = Exists(path=path) + + expected_expression = { + "exists": { + "path": path + } + } + + # Act + actual_expression = exists_op.expression + + # Assert + assert actual_expression == expected_expression \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py b/tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py index 46409041..f4aa4e7c 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_more_like_this.py @@ -1 +1,19 @@ -# TODO +from monggregate.search.operators.more_like_this import MoreLikeThis + +def test_more_like_this_expression(): + # Setup + like_docs = [{"title": "Introduction to MongoDB"}, {"title": "Advanced MongoDB Usage"}] + + more_like_this = MoreLikeThis(like=like_docs) + + expected_expression = { + "moreLikeThis": { + "like": like_docs + } + } + + # Act + actual_expression = more_like_this.expression + + # Assert + assert actual_expression == expected_expression \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_operator.py b/tests/tests_monggregate/tests_search/tests_operators/test_operator.py index 46409041..720369e5 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_operator.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_operator.py @@ -1 +1,19 @@ -# TODO +from monggregate.search.operators.operator import SearchOperator + +def test_search_operator_instantiation(): + # Setup + class ConcreteOperator(SearchOperator): + field: str + + @property + def expression(self) -> dict: + """Concrete implementation of the expression property""" + return {"field": self.field} + + operator = ConcreteOperator(field="value") + + # Act + result = operator.expression + + # Assert + assert result == {"field": "value"} diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_range.py b/tests/tests_monggregate/tests_search/tests_operators/test_range.py index 46409041..b25ae114 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_range.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_range.py @@ -1 +1,27 @@ -# TODO +import pytest +from datetime import datetime +from monggregate.search.operators.range import Range + +def test_range_expression_with_numeric_bounds(): + # Setup + range_op = Range( + path="price", + gt=10, + lte=100, + score={"boost": 2} + ) + expected = { + "range": { + "path": "price", + "gt": 10, + "lte": 100, + "score": {"boost": 2} + } + } + + # Act + expression = range_op.expression + + # Assert + + assert expression == expected \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_regex.py b/tests/tests_monggregate/tests_search/tests_operators/test_regex.py index 46409041..7041b7a7 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_regex.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_regex.py @@ -1 +1,25 @@ -# TODO +from monggregate.search.operators.regex import Regex + +def test_regex_expression_basic(): + # Setup (Arrange) + regex_op = Regex( + query="^Star.*", + path="title", + allow_analyzed_field=True, + score={"constant": 1} + ) + expected = { + "regex": { + "query": "^Star.*", + "path": "title", + "allowAnalyzedField": True, + "score": {"constant": 1} + } + } + + # Act + expression = regex_op.expression + + # Assert + + assert expression == expected \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_text.py b/tests/tests_monggregate/tests_search/tests_operators/test_text.py index 46409041..a8341f2a 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_text.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_text.py @@ -1 +1,24 @@ -# TODO +from monggregate.search.operators.text import Text + +def test_text_expression_with_fuzzy_and_synonyms(): + # Setup + text_op = Text( + query="mongodb atlas", + path="description", + score={"boost": 3}, + synonyms="mySynonyms" + ) + expected = { + "text": { + "query": "mongodb atlas", + "path": "description", + "score": {"boost": 3}, + "synonyms": "mySynonyms" + } + } + + # Act + expression = text_op.expression + + # Assert + assert expression == expected \ No newline at end of file diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py b/tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py index 46409041..534ed7f0 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_wildcard.py @@ -1 +1,25 @@ -# TODO +from monggregate.search.operators.wildcard import Wildcard + +def test_wildcard_expression_basic(): + # Setup + wildcard_op = Wildcard( + query="foo*bar?", + path="content", + allow_analyzed_field=True, + score={"boost": 5} + ) + + expected = { + "wildcard": { + "query": "foo*bar?", + "path": "content", + "allowAnalyzedField": True, + "score": {"boost": 5} + } + } + + # Act + expression = wildcard_op.expression + + # Assert + assert expression == expected \ No newline at end of file From cdf186e6621dcd574b26f0ef0ee9c4785d358258 Mon Sep 17 00:00:00 2001 From: Zegthor91 Date: Thu, 12 Jun 2025 16:54:36 +0200 Subject: [PATCH 11/14] last commit --- .../tests_operators/tests_comparison/test_gte.py | 2 -- .../tests_operators/tests_comparison/test_lt.py | 2 -- tests/tests_monggregate/tests_stages/tests_search/__init__.py | 4 ++++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/tests_monggregate/tests_operators/tests_comparison/test_gte.py b/tests/tests_monggregate/tests_operators/tests_comparison/test_gte.py index 098489c6..a9504176 100644 --- a/tests/tests_monggregate/tests_operators/tests_comparison/test_gte.py +++ b/tests/tests_monggregate/tests_operators/tests_comparison/test_gte.py @@ -2,8 +2,6 @@ from monggregate.operators.comparison.gte import GreatherThanOrEqual -from monggregate.operators.comparison.gte import GreatherThanOrEqual - def test_greather_than_or_equal_expression(): # Setup left = "$field" diff --git a/tests/tests_monggregate/tests_operators/tests_comparison/test_lt.py b/tests/tests_monggregate/tests_operators/tests_comparison/test_lt.py index 9206bfee..ce6a3ffc 100644 --- a/tests/tests_monggregate/tests_operators/tests_comparison/test_lt.py +++ b/tests/tests_monggregate/tests_operators/tests_comparison/test_lt.py @@ -2,8 +2,6 @@ from monggregate.operators.comparison.lt import LowerThan -from monggregate.operators.comparison.lt import LowerThan - def test_lower_than_expression(): # Setup left = "$field" diff --git a/tests/tests_monggregate/tests_stages/tests_search/__init__.py b/tests/tests_monggregate/tests_stages/tests_search/__init__.py index 12815493..4dc8d07d 100644 --- a/tests/tests_monggregate/tests_stages/tests_search/__init__.py +++ b/tests/tests_monggregate/tests_stages/tests_search/__init__.py @@ -1 +1,5 @@ """Tests for `monggregate.stages.search` subpackage.""" + +from monggregate.stages.search.base import BaseModel, SearchBase, SearchConfig +from monggregate.stages.search.search_meta import SearchMeta +from monggregate.stages.search.search import Search \ No newline at end of file From 04a9db51ddd52562d1d27e601b59efdf6a11be73 Mon Sep 17 00:00:00 2001 From: VianneyMI Date: Thu, 12 Jun 2025 22:52:33 +0200 Subject: [PATCH 12/14] Add test_clause and test_operator_map --- src/monggregate/search/operators/compound.py | 391 ++++++++---------- .../tests_operators/test_clause.py | 137 +----- .../tests_operators/test_compound.py | 19 +- .../tests_operators/test_operator.py | 26 +- 4 files changed, 220 insertions(+), 353 deletions(-) diff --git a/src/monggregate/search/operators/compound.py b/src/monggregate/search/operators/compound.py index 514acb01..06ef8463 100644 --- a/src/monggregate/search/operators/compound.py +++ b/src/monggregate/search/operators/compound.py @@ -1,4 +1,4 @@ -"""Module defining an interface to MongoDB Atlas Search compound operator +"""Module defining an interface to MongoDB Atlas Search compound operator Online MongoDB documentation: ---------------------------------------------- @@ -7,12 +7,12 @@ # Definition # -------------------------------------------- -The compound operator combines two or more operators into a single query. +The compound operator combines two or more operators into a single query. Each element of a compound query is called a clause, and each clause consists of one or more sub-queries. -Documents in the result set are returned with a match score, -which is calculated by summing the score that each document received -for each individual clause which generated a match. +Documents in the result set are returned with a match score, +which is calculated by summing the score that each document received +for each individual clause which generated a match. The result set is ordered by score, highest to lowest. # Syntax @@ -33,25 +33,25 @@ term Description -must Clauses that must match to for a document to be included in the results. +must Clauses that must match to for a document to be included in the results. The returned score is the sum of the scores of all the subqueries in the clause. Maps to the AND boolean operator. -mustNot Clauses that must not match for a document to be included in the results. +mustNot Clauses that must not match for a document to be included in the results. mustNot clauses don't contribute to a returned document's score. Maps to the AND NOT boolean operator. -should Clauses that you prefer to match in documents that are included in the results. - Documents that contain a match for a should clause have higher scores than documents that don't contain a should clause. +should Clauses that you prefer to match in documents that are included in the results. + Documents that contain a match for a should clause have higher scores than documents that don't contain a should clause. The returned score is the sum of the scores of all the subqueries in the clause. Maps to the OR boolean operator. -filter Clauses that must all match for a document to be included in the results. +filter Clauses that must all match for a document to be included in the results. filter clauses do not contribute to a returned document's score. # Usage # ------------------------------------------------------- -You can use any of the clauses with any top-level operator, +You can use any of the clauses with any top-level operator, such as autocomplete, text, or span, to specify query criteria. """ @@ -72,47 +72,46 @@ Range, Regex, Text, - Wildcard - ) + Wildcard, +) from monggregate.search.commons import FuzzyOptions ClauseType = Literal["must", "mustNot", "should", "filter"] + class Compound(SearchOperator): """ Class defining an interface to MongoDB Atlas Search compound operator Description: --------------------- - The compound operator combines two or more operators into a single query. + The compound operator combines two or more operators into a single query. Each element of a compound query is called a clause, and each clause consists of one or more sub-queries. - Documents in the result set are returned with a match score, - which is calculated by summing the score that each document received for each individual clause which generated a match. + Documents in the result set are returned with a match score, + which is calculated by summing the score that each document received for each individual clause which generated a match. The result set is ordered by score, highest to lowest. - + Attributes: ---------------------- - must, list[dict] : Clauses that must match for a document to be included in the results - must_not, list[dict] : Clauses that must not match for a document to be included in the results - should, list[dict] : Clauses that you prefer to match but that are not mandatory. - filter, list[dict] : Clauses that must for a document to be included but which don't affect the score. - - minimum_should_match, int : Specifies a minimum number of should clauses that must match + - minimum_should_match, int : Specifies a minimum number of should clauses that must match for a document to be included. # NOTE : The clauses can be nested """ - - must : list["Clause|Compound"] = [] - must_not : list["Clause|Compound"] = [] - should : list["Clause|Compound"] = [] - filter : list["Clause|Compound"] = [] - minimum_should_match : int = 0 + must: list["Clause|Compound"] = [] + must_not: list["Clause|Compound"] = [] + should: list["Clause|Compound"] = [] + filter: list["Clause|Compound"] = [] + minimum_should_match: int = 0 @property def expression(self) -> Expression: - clauses = {} if self.must: clauses["must"] = self.must @@ -124,11 +123,9 @@ def expression(self) -> Expression: if self.filter: clauses["filter"] = self.filter - return self.express({ - "compound":clauses - }) + return self.express({"compound": clauses}) - def _register_clause(self, type:ClauseType, operator:Clause|Self)->None: + def _register_clause(self, type: ClauseType, operator: Clause | Self) -> None: """ Adds a clause to the current compound instance. @@ -136,8 +133,8 @@ def _register_clause(self, type:ClauseType, operator:Clause|Self)->None: ----------------------- - type:Literal["must", "mustNot", "should", "filter"] : The type of clause to add - statement:dict : The operator statement of the clause to add - - + + """ if type == "must": @@ -149,45 +146,40 @@ def _register_clause(self, type:ClauseType, operator:Clause|Self)->None: elif type == "should": self.should.append(operator) - #--------------------------------------------- + # --------------------------------------------- # Operators - #--------------------------------------------- + # --------------------------------------------- def autocomplete( - self, - type:ClauseType, - *, - query:str|list[str], - path:str, - token_order:str="any", - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + type: ClauseType, + *, + query: str | list[str], + path: str, + token_order: str = "any", + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds an autocomplete clause to the current compound instance.""" - + _autocomplete = Autocomplete( - query=query, - path=path, - token_order=token_order, - fuzzy=fuzzy, - score=score + query=query, path=path, token_order=token_order, fuzzy=fuzzy, score=score ) self._register_clause(type, _autocomplete) - - return self + return self def compound( - self, - type:ClauseType, - must:list["Clause|Compound"]=[], - must_not:list["Clause|Compound"]=[], - should:list["Clause|Compound"]=[], - filter:list["Clause|Compound"]=[], - minimum_should_match:int=0, - **kwargs:Any - )->Self: + self, + type: ClauseType, + must: list["Clause|Compound"] = [], + must_not: list["Clause|Compound"] = [], + should: list["Clause|Compound"] = [], + filter: list["Clause|Compound"] = [], + minimum_should_match: int = 0, + **kwargs: Any, + ) -> Self: """Adds a compound clause to the current compound instance.""" _compound = Compound( @@ -195,36 +187,30 @@ def compound( must_not=must_not, should=should, filter=filter, - minimum_should_match=minimum_should_match + minimum_should_match=minimum_should_match, ) self._register_clause(type, _compound) return _compound - def equals( - self, - type, - path:str, - value:str|int|float|bool|datetime, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + type, + path: str, + value: str | int | float | bool | datetime, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds an equals clause to the current compound instance.""" - _equals = Equals( - path=path, - value=value, - score=score - ).expression + _equals = Equals(path=path, value=value, score=score) self._register_clause(type, _equals) return self - - def exists(self, type:ClauseType, path:str, **kwargs:Any)->Self: + def exists(self, type: ClauseType, path: str, **kwargs: Any) -> Self: """Adds an exists clause to the current compound instance.""" _exists = Exists(path=path) @@ -232,8 +218,9 @@ def exists(self, type:ClauseType, path:str, **kwargs:Any)->Self: return self - - def more_like_this(self, type:ClauseType, like:dict|list[dict], **kwargs:Any)->Self: + def more_like_this( + self, type: ClauseType, like: dict | list[dict], **kwargs: Any + ) -> Self: """Adds a more_like_this clause to the current compound instance.""" _more_like_this = MoreLikeThis(like=like) @@ -241,222 +228,178 @@ def more_like_this(self, type:ClauseType, like:dict|list[dict], **kwargs:Any)->S return self - def range( - self, - type:ClauseType, - *, - path:str|list[str], - gt:int|float|datetime|None=None, - lt:int|float|datetime|None=None, - gte:int|float|datetime|None=None, - lte:int|float|datetime|None=None, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + type: ClauseType, + *, + path: str | list[str], + gt: int | float | datetime | None = None, + lt: int | float | datetime | None = None, + gte: int | float | datetime | None = None, + lte: int | float | datetime | None = None, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds a range clause to the current compound instance.""" - _range = Range( - path=path, - gt=gt, - gte=gte, - lt=lt, - lte=lte, - score=score - ) + _range = Range(path=path, gt=gt, gte=gte, lt=lt, lte=lte, score=score) self._register_clause(type, _range) return self - def regex( - self, - type:ClauseType, - *, - query:str|list[str], - path:str|list[str], - allow_analyzed_field:bool=False, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + type: ClauseType, + *, + query: str | list[str], + path: str | list[str], + allow_analyzed_field: bool = False, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds a regex clause to the current compound instance.""" _regex = Regex( query=query, path=path, allow_analyzed_field=allow_analyzed_field, - score=score + score=score, ) - self._register_clause(type, _regex) return self - def text( - self, - type:ClauseType, - *, - query:str|list[str], - path:str|list[str], - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - synonyms:str|None=None, - **kwargs:Any - )->Self: + self, + type: ClauseType, + *, + query: str | list[str], + path: str | list[str], + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + synonyms: str | None = None, + **kwargs: Any, + ) -> Self: """Adds a text clause to the current compound instance.""" _text = Text( - query=query, - path=path, - score=score, - fuzzy=fuzzy, - synonyms=synonyms + query=query, path=path, score=score, fuzzy=fuzzy, synonyms=synonyms ) self._register_clause(type, _text) return self - def wildcard( - self, - type:ClauseType, - *, - query:str|list[str], - path:str|list[str], - allow_analyzed_field:bool=False, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + type: ClauseType, + *, + query: str | list[str], + path: str | list[str], + allow_analyzed_field: bool = False, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds a wildcard clause to the current compound instance.""" _wildcard = Wildcard( query=query, path=path, allow_analyzed_field=allow_analyzed_field, - score=score + score=score, ) self._register_clause(type, _wildcard) return self - - #--------------------------------------------- + + # --------------------------------------------- # Clauses - #--------------------------------------------- + # --------------------------------------------- def must_( - self, - operator_name:OperatorLiteral, - path:str|list[str]|None=None, - query:str|list[str]|None=None, - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs - )->Self: + self, + operator_name: OperatorLiteral, + path: str | list[str] | None = None, + query: str | list[str] | None = None, + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs, + ) -> Self: """Adds a must clause to the current compound instance.""" - - kwargs.update( - { - "path":path, - "query":query, - "fuzzy":fuzzy, - "score":score - } - ) - return self.__get_operators_map__(operator_name)("must", **kwargs) + kwargs.update({"path": path, "query": query, "fuzzy": fuzzy, "score": score}) + return self.__get_operators_map__(operator_name)("must", **kwargs) def must_not_( - self, - operator_name:OperatorLiteral, - path:str|list[str]|None=None, - query:str|list[str]|None=None, - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs - )->Self: + self, + operator_name: OperatorLiteral, + path: str | list[str] | None = None, + query: str | list[str] | None = None, + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs, + ) -> Self: """Adds a must_not clause to the current compound instance.""" - - kwargs.update( - { - "path":path, - "query":query, - "fuzzy":fuzzy, - "score":score - } - ) - return self.__get_operators_map__(operator_name)("mustNot", **kwargs) + kwargs.update({"path": path, "query": query, "fuzzy": fuzzy, "score": score}) + return self.__get_operators_map__(operator_name)("mustNot", **kwargs) def should_( - self, - operator_name:OperatorLiteral, - path:str|list[str]|None=None, - query:str|list[str]|None=None, - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs - )->Self: + self, + operator_name: OperatorLiteral, + path: str | list[str] | None = None, + query: str | list[str] | None = None, + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs, + ) -> Self: """Adds a should clause to the current compound instance.""" - - kwargs.update( - { - "path":path, - "query":query, - "fuzzy":fuzzy, - "score":score - } - ) - return self.__get_operators_map__(operator_name)("should", **kwargs) + kwargs.update({"path": path, "query": query, "fuzzy": fuzzy, "score": score}) + return self.__get_operators_map__(operator_name)("should", **kwargs) def filter_( - self, - operator_name:OperatorLiteral, - path:str|list[str]|None=None, - query:str|list[str]|None=None, - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs - )->Self: + self, + operator_name: OperatorLiteral, + path: str | list[str] | None = None, + query: str | list[str] | None = None, + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs, + ) -> Self: """Adds a filter clause to the current compound instance.""" - - kwargs.update( - { - "path":path, - "query":query, - "fuzzy":fuzzy, - "score":score - } - ) + + kwargs.update({"path": path, "query": query, "fuzzy": fuzzy, "score": score}) return self.__get_operators_map__(operator_name)("filter", **kwargs) - #--------------------------------------------- + # --------------------------------------------- # Utility functions - #--------------------------------------------- - def __get_operators_map__(self, operator_name:OperatorLiteral)->Callable[...,Self]: + # --------------------------------------------- + def __get_operators_map__( + self, operator_name: OperatorLiteral + ) -> Callable[..., Self]: """Returns the operator class associated with the given operator name.""" operators_map = { - "autocomplete":self.autocomplete, - "compound":self.compound, #FIXME : This breaks typing - "equals":self.equals, - "exists":self.exists, - "range":self.range, - "more_like_this":self.more_like_this, - "regex":self.regex, - "text":self.text, - "wildcard":self.wildcard + "autocomplete": self.autocomplete, + "compound": self.compound, # FIXME : This breaks typing + "equals": self.equals, + "exists": self.exists, + "range": self.range, + "more_like_this": self.more_like_this, + "regex": self.regex, + "text": self.text, + "wildcard": self.wildcard, } return operators_map[operator_name] + if __name__ == "__main__": - print(Compound()) \ No newline at end of file + print(Compound()) diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py index 70f3ac06..9fe17870 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_clause.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_clause.py @@ -1,122 +1,27 @@ -import pytest -from monggregate.search.operators.autocomplete import Autocomplete -from monggregate.search.operators.equals import Equals -from monggregate.search.operators.exists import Exists -from monggregate.search.operators.more_like_this import MoreLikeThis -from monggregate.search.operators.range import Range -from monggregate.search.operators.regex import Regex -from monggregate.search.operators.text import Text -from monggregate.search.operators.wildcard import Wildcard +"""Tests for `monggregate.search.operators.clause` module.""" -class TestSearchOperators: - """Tests unitaires pour tous les opérateurs de recherche de monggregate.""" +from monggregate.search.operators.clause import Clause +from monggregate.search.operators.compound import Compound +from monggregate.search.operators import OperatorMap - def test_autocomplete(self): - """Teste la génération de l'expression autocomplete.""" - # Corrected: query should likely be a list - autocomplete = Autocomplete(query=["test"], path="field") - expected = { - "autocomplete": { - "query": ["test"], # Corrected: query should be a list in expected - "path": "field" - } - } - assert autocomplete.expression == expected - def test_equals(self): - """Teste la génération de l'expression equals.""" - equals = Equals(value=42, path="field") - expected = { - "equals": { - "value": 42, - "path": "field" - } - } - assert equals.expression == expected +def test_consistency_with_operator_map() -> None: + """Tests that all operators are present in the clause type.""" - def test_exists(self): - """Teste la génération de l'expression exists.""" - exists = Exists(path="field") - expected = { - "exists": { - "path": "field" - } - } - assert exists.expression == expected + # Setup + operators_in_clause = set(Clause.__args__) | {Compound} + # Compoound should be in Clause but it seems complex to implement right now + # TODO : Fix this when fully migrating to pydantic v2. + operators_in_operator_map = set(OperatorMap.values()) - def test_more_like_this(self): - """Teste la génération de l'expression moreLikeThis.""" - # Corrected: 'like' should likely be a list - more_like_this = MoreLikeThis( - like=["test"], # Corrected: 'like' should be a list - path="field", - minTermFreq=1, - minDocFreq=1 - ) - expected = { - "moreLikeThis": { - "like": ["test"], # Corrected: 'like' should be a list in expected - "path": "field", - "minTermFreq": 1, - "minDocFreq": 1 - } - } - assert more_like_this.expression == expected + # Act + missing_operators_in_clause = operators_in_operator_map - operators_in_clause + missing_operators_in_operator_map = operators_in_clause - operators_in_operator_map - def test_range(self): - """Teste la génération de l'expression range.""" - # Corrected: path should likely be a list - range_op = Range(path=["field"], gt=10, lt=20) # Corrected: path should be a list - expected = { - "range": { - "path": ["field"], # Corrected: path should be a list in expected - "gt": 10, - "lt": 20 - } - } - assert range_op.expression == expected - - def test_regex(self): - """Teste la génération de l'expression regex.""" - # Corrected: path should likely be a list - # allowAnalyzedField should be included in the expected dict as it's explicitly set. - regex = Regex(pattern="^test", path=["field"], allowAnalyzedField=False) # Corrected: path should be a list - expected = { - "regex": { - "pattern": "^test", - "path": ["field"], # Corrected: path should be a list in expected - "allowAnalyzedField": False - } - } - assert regex.expression == expected - - def test_text(self): - """Teste la génération de l'expression text.""" - # Corrected: query should likely be a list - text = Text(query=["test"], path="field") # Corrected: query should be a list - expected = { - "text": { - "query": ["test"], # Corrected: query should be a list in expected - "path": "field" - } - } - assert text.expression == expected - - def test_wildcard(self): - """Teste la génération de l'expression wildcard.""" - # The wildcard operator also has `allowAnalyzedField` which defaults to `False`. - # If it's not explicitly passed to the constructor and the default is False, - # it might not appear in the expression. However, if the expected output - # always includes it, we keep it. If the problem persists, try removing it. - wildcard = Wildcard(query=["test"], path="field") # Corrected: query should be a list - expected = { - "wildcard": { - "query": ["test"], # Corrected: query should be a list in expected - "path": "field", - "allowAnalyzedField": False # Keeping this as it's a default, if it fails, remove it. - } - } - assert wildcard.expression == expected - - -# TODO + # Assert + assert not missing_operators_in_clause, ( + f"Operators in operator map but not in clause: {missing_operators_in_clause}" + ) + assert not missing_operators_in_operator_map, ( + f"Operators in clause but not in operator map: {missing_operators_in_operator_map}" + ) diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_compound.py b/tests/tests_monggregate/tests_search/tests_operators/test_compound.py index 05d25c7e..2d0fcda9 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_compound.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_compound.py @@ -1,7 +1,11 @@ -import pytest +"""Tests for `monggregate.search.operators.compound` module.""" + from monggregate.search.operators.compound import Compound -def test_compound_expression_with_must_equals(): + +def test_compound_expression_with_must_equals() -> None: + """Tests that the compound expression is correct when using the must equals operator.""" + # Setup compound = Compound() path = "field" @@ -10,14 +14,7 @@ def test_compound_expression_with_must_equals(): expected_expression = { "compound": { - "must": [ - { - "equals": { - "path": path, - "value": value - } - } - ] + "must": [{"equals": {"path": path, "score": None, "value": value}}] } } @@ -25,4 +22,4 @@ def test_compound_expression_with_must_equals(): actual_expression = compound.expression # Assert - assert actual_expression == expected_expression \ No newline at end of file + assert actual_expression == expected_expression diff --git a/tests/tests_monggregate/tests_search/tests_operators/test_operator.py b/tests/tests_monggregate/tests_search/tests_operators/test_operator.py index 720369e5..3b129edf 100644 --- a/tests/tests_monggregate/tests_search/tests_operators/test_operator.py +++ b/tests/tests_monggregate/tests_search/tests_operators/test_operator.py @@ -1,10 +1,32 @@ +"""Tests for `monggregate.search.operators.operator` module.""" + +from monggregate.search.operators import OperatorMap from monggregate.search.operators.operator import SearchOperator -def test_search_operator_instantiation(): + +def test_operator_map_operator_coverage() -> None: + """Tests that all operators are present in the operator map.""" + + # Setup + operators_in_operator_map = set(OperatorMap.values()) + operators_in_search_operators = set(SearchOperator.__subclasses__()) + + # Act + missing_operators_in_operator_map = ( + operators_in_search_operators - operators_in_operator_map + ) + + # Assert + assert not missing_operators_in_operator_map, ( + f"Operators in search operators but not in operator map: {missing_operators_in_operator_map}" + ) + + +def test_search_operator_instantiation() -> None: # Setup class ConcreteOperator(SearchOperator): field: str - + @property def expression(self) -> dict: """Concrete implementation of the expression property""" From 8ae2c6b19ee041522c6aed351b871d0c633538f1 Mon Sep 17 00:00:00 2001 From: VianneyMI Date: Thu, 12 Jun 2025 23:14:10 +0200 Subject: [PATCH 13/14] Adding missing tests and fixing some existing ones --- .../tests_operators/test_operator.py | 77 ++++++++++++++++++- .../tests_arithmetic/test_arithmetic.py | 73 +++++++++++++++++- .../tests_operators/tests_array/test_array.py | 70 ++++++++++++++++- .../tests_boolean/test_boolean.py | 69 ++++++++++++++++- .../tests_comparison/test_comparator.py | 70 ++++++++++++++++- .../tests_conditional/test_conditional.py | 73 +++++++++++++++++- .../tests_custom/test_custom.py | 70 ++++++++++++++++- .../tests_data_size/test_data_size.py | 73 +++++++++++++++++- .../tests_operators/tests_date/test_date.py | 70 ++++++++++++++++- .../tests_objects/test_object_.py | 69 ++++++++++++++++- .../tests_strings/test_string.py | 77 ++++++++++++++++++- 11 files changed, 769 insertions(+), 22 deletions(-) diff --git a/tests/tests_monggregate/tests_operators/test_operator.py b/tests/tests_monggregate/tests_operators/test_operator.py index 6c2262da..67503efa 100644 --- a/tests/tests_monggregate/tests_operators/test_operator.py +++ b/tests/tests_monggregate/tests_operators/test_operator.py @@ -1,3 +1,76 @@ -"""Tests for `monggregate.operators.operator`.""" +"""Tests for `monggregate.operators.operator` module.""" -# TODO +import pytest + +from monggregate.operators.operator import Operator, OperatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestOperator: + """Tests for the `Operator` class.""" + + def test_is_abstract(self) -> None: + """Test that `Operator` is an abstract class.""" + with pytest.raises(TypeError): + Operator() + + +class TestOperatorEnum: + """Tests for the `OperatorEnum` class.""" + + @pytest.mark.xfail( + reason="""Some operators are not following the naming convention. + Ex: INDEX_OF_CP + + Need to review the generate_enum_member_name function. + """ + ) + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in OperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py b/tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py index c6de24a2..4d271873 100644 --- a/tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py +++ b/tests/tests_monggregate/tests_operators/tests_arithmetic/test_arithmetic.py @@ -1,3 +1,72 @@ -"""Tests for `monggregate.operators.arithmetic.arithmetic`.""" +"""Tests for `monggregate.operators.arithmetic.arithmetic` module.""" -# TODO +import pytest + +from monggregate.operators.arithmetic.arithmetic import ( + ArithmeticOperator, + ArithmeticOperatorEnum, +) + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestArithmeticOperator: + """Tests for the `ArithmeticOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `ArithmeticOperator` is an abstract class.""" + with pytest.raises(TypeError): + ArithmeticOperator() + + +class TestArithmeticOperatorEnum: + """Tests for the `ArithmeticOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in ArithmeticOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_array/test_array.py b/tests/tests_monggregate/tests_operators/tests_array/test_array.py index f966bf69..27ac2270 100644 --- a/tests/tests_monggregate/tests_operators/tests_array/test_array.py +++ b/tests/tests_monggregate/tests_operators/tests_array/test_array.py @@ -1,3 +1,69 @@ -"""Tests for `monggregate.operators.array.array`.""" +"""Tests for `monggregate.operators.array.array` module.""" -# TODO +import pytest + +from monggregate.operators.array.array import ArrayOperator, ArrayOperatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestArrayOperator: + """Tests for the `ArrayOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `ArrayOperator` is an abstract class.""" + with pytest.raises(TypeError): + ArrayOperator() + + +class TestArrayOperatorEnum: + """Tests for the `ArrayOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in ArrayOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py b/tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py index 6aa33f7f..8762bf6e 100644 --- a/tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py +++ b/tests/tests_monggregate/tests_operators/tests_boolean/test_boolean.py @@ -1,4 +1,69 @@ -"""Tests for `monggregate.operators.boolean.boolean`.""" +"""Tests for `monggregate.operators.boolean.boolean` module.""" +import pytest -# TODO +from monggregate.operators.boolean.boolean import BooleanOperator, BooleanOperatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestBooleanOperator: + """Tests for the `BooleanOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `BooleanOperator` is an abstract class.""" + with pytest.raises(TypeError): + BooleanOperator() + + +class TestBooleanOperatorEnum: + """Tests for the `BooleanOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in BooleanOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py b/tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py index 778bdd8f..dad0a5d8 100644 --- a/tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py +++ b/tests/tests_monggregate/tests_operators/tests_comparison/test_comparator.py @@ -1,3 +1,69 @@ -"""Tests for `monggregate.operators.comparison`.""" +"""Tests for `monggregate.operators.comparison.comparator` module.""" -# TODO +import pytest + +from monggregate.operators.comparison.comparator import Comparator, ComparatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestComparator: + """Tests for the `Comparator` class.""" + + def test_is_abstract(self) -> None: + """Test that `Comparator` is an abstract class.""" + with pytest.raises(TypeError): + Comparator() + + +class TestComparatorEnum: + """Tests for the `ComparatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in ComparatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py b/tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py index 63cee75c..af24cb21 100644 --- a/tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py +++ b/tests/tests_monggregate/tests_operators/tests_conditional/test_conditional.py @@ -1,3 +1,72 @@ -"""Tests for `monggregate.operators.conditional.conditional`.""" +"""Tests for `monggregate.operators.conditional.conditional` module.""" -# TODO +import pytest + +from monggregate.operators.conditional.conditional import ( + ConditionalOperator, + ConditionalOperatorEnum, +) + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestConditionalOperator: + """Tests for the `ConditionalOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `ConditionalOperator` is an abstract class.""" + with pytest.raises(TypeError): + ConditionalOperator() + + +class TestConditionalOperatorEnum: + """Tests for the `ConditionalOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in ConditionalOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_custom/test_custom.py b/tests/tests_monggregate/tests_operators/tests_custom/test_custom.py index 51925f64..f29c953a 100644 --- a/tests/tests_monggregate/tests_operators/tests_custom/test_custom.py +++ b/tests/tests_monggregate/tests_operators/tests_custom/test_custom.py @@ -1,3 +1,69 @@ -"""Tests for `monggregate.operators.custom.custom`.""" +"""Tests for `monggregate.operators.custom.custom` module.""" -# TODO +import pytest + +from monggregate.operators.custom.custom import CustomOperator, CustomOperatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestCustomOperator: + """Tests for the `CustomOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `CustomOperator` is an abstract class.""" + with pytest.raises(TypeError): + CustomOperator() + + +class TestCustomOperatorEnum: + """Tests for the `CustomOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in CustomOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py b/tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py index b9a7929e..b5543299 100644 --- a/tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py +++ b/tests/tests_monggregate/tests_operators/tests_data_size/test_data_size.py @@ -1,3 +1,72 @@ -"""Tests for `monggregate.operators.data_size.data_size`.""" +"""Tests for `monggregate.operators.data_size.data_size` module.""" -# TODO +import pytest + +from monggregate.operators.data_size.data_size import ( + DataSizeOperator, + DataSizeOperatorEnum, +) + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestDataSizeOperator: + """Tests for the `DataSizeOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `DataSizeOperator` is an abstract class.""" + with pytest.raises(TypeError): + DataSizeOperator() + + +class TestDataSizeOperatorEnum: + """Tests for the `DataSizeOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in DataSizeOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_date/test_date.py b/tests/tests_monggregate/tests_operators/tests_date/test_date.py index fccd1e59..2e47c02e 100644 --- a/tests/tests_monggregate/tests_operators/tests_date/test_date.py +++ b/tests/tests_monggregate/tests_operators/tests_date/test_date.py @@ -1,3 +1,69 @@ -"""Tests for `monggregate.operators.date.date`.""" +"""Tests for `monggregate.operators.date.date` module.""" -# TODO +import pytest + +from monggregate.operators.date.date import DateOperator, DateOperatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestDateOperator: + """Tests for the `DateOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `DateOperator` is an abstract class.""" + with pytest.raises(TypeError): + DateOperator() + + +class TestDateOperatorEnum: + """Tests for the `DateOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in DateOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_objects/test_object_.py b/tests/tests_monggregate/tests_operators/tests_objects/test_object_.py index 35dae684..68194c01 100644 --- a/tests/tests_monggregate/tests_operators/tests_objects/test_object_.py +++ b/tests/tests_monggregate/tests_operators/tests_objects/test_object_.py @@ -1,4 +1,69 @@ -"""Tests for `monggregate.operators.objects.objects`.""" +"""Tests for `monggregate.operators.objects.object_` module.""" +import pytest -# TODO +from monggregate.operators.objects.object_ import ObjectOperator, ObjectOperatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestObjectOperator: + """Tests for the `ObjectOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `ObjectOperator` is an abstract class.""" + with pytest.raises(TypeError): + ObjectOperator() + + +class TestObjectOperatorEnum: + """Tests for the `ObjectOperatorEnum` class.""" + + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in ObjectOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) diff --git a/tests/tests_monggregate/tests_operators/tests_strings/test_string.py b/tests/tests_monggregate/tests_operators/tests_strings/test_string.py index 50dcc46a..f40f3811 100644 --- a/tests/tests_monggregate/tests_operators/tests_strings/test_string.py +++ b/tests/tests_monggregate/tests_operators/tests_strings/test_string.py @@ -1,3 +1,76 @@ -"""Tests for `monggregate.operators.strings.string`.""" +"""Tests for `monggregate.operators.strings.string` module.""" -# TODO +import pytest + +from monggregate.operators.strings.string import StringOperator, StringOperatorEnum + + +def generate_enum_member_name(value: str) -> str: + """Generate the expected enum member name from a value. + + Args: + value: The enum value (with or without $ prefix) + + Returns: + The expected member name in UPPER_SNAKE_CASE format + + Example: + >>> generate_enum_member_name("$addToSet") + 'ADD_TO_SET' + >>> generate_enum_member_name("bottomN") + 'BOTTOM_N' + """ + # Remove the $ prefix if present + value_name = value[1:] if value.startswith("$") else value + + # Split camelCase into words + words = [] + current_word = value_name[0] + + for char in value_name[1:]: + if char.isupper(): + words.append(current_word) + current_word = char + else: + current_word += char + + words.append(current_word) + + # Convert to UPPER_SNAKE_CASE + return "_".join(word.upper() for word in words) + + +class TestStringOperator: + """Tests for the `StringOperator` class.""" + + def test_is_abstract(self) -> None: + """Test that `StringOperator` is an abstract class.""" + with pytest.raises(TypeError): + StringOperator() + + +class TestStringOperatorEnum: + """Tests for the `StringOperatorEnum` class.""" + + @pytest.mark.xfail( + reason="""Some operators are not following the naming convention. + Ex: CONCAT_WS + + Need to review the generate_enum_member_name function. + """ + ) + def test_naming_convention(self) -> None: + """Test that the naming convention is correct.""" + mismatches = [] + + for member in StringOperatorEnum: + expected_name = generate_enum_member_name(member.value) + if member.name != expected_name: + mismatches.append( + f"\n- {member.name}: got '{member.name}', expected '{expected_name}'" + ) + + assert not mismatches, ( + "The following members do not follow the naming convention:" + f"{''.join(mismatches)}" + ) From 64a8fa695fdf017fc239c17d5c53e35f9f73f361 Mon Sep 17 00:00:00 2001 From: VianneyMI Date: Thu, 12 Jun 2025 23:50:58 +0200 Subject: [PATCH 14/14] Missing tests --- src/monggregate/search/collectors/facet.py | 827 ++++++++---------- src/monggregate/search/commons/highlight.py | 36 +- .../tests_operators/tests_custom/__init__.py | 3 - .../tests_collectors/test_facet.py | 75 +- .../tests_search/tests_commons/test_count.py | 55 +- .../tests_search/tests_commons/test_fuzzy.py | 38 +- .../tests_commons/test_highlight.py | 69 +- 7 files changed, 627 insertions(+), 476 deletions(-) diff --git a/src/monggregate/search/collectors/facet.py b/src/monggregate/search/collectors/facet.py index f096ebb4..7740c349 100644 --- a/src/monggregate/search/collectors/facet.py +++ b/src/monggregate/search/collectors/facet.py @@ -8,12 +8,12 @@ # Definition # -------------------------------------------- -The facet collector groups results by values or ranges in the specified faceted fields +The facet collector groups results by values or ranges in the specified faceted fields and returns the count for each of those groups. -You can use facet with both the $search and $searchMeta stages. -MongoDB recommends using facet with the $searchMeta stage to retrieve metadata results only for the query. -To retrieve metadata results and query results using the $search stage, you must use the $$SEARCH_META aggregation variable. +You can use facet with both the $search and $searchMeta stages. +MongoDB recommends using facet with the $searchMeta stage to retrieve metadata results only for the query. +To retrieve metadata results and query results using the $search stage, you must use the $$SEARCH_META aggregation variable. See SEARCH_META Aggregation Variable to learn more. @@ -45,7 +45,7 @@ # Facet Definition ---------------------------------------------- -The facet definition document contains the facet name and options specific to a type of facet. +The facet definition document contains the facet name and options specific to a type of facet. Atlas Search supports the following types of facets: * String Facets @@ -54,9 +54,9 @@ # String Facets -String facets allow you to narrow down Atlas Search results -based on the most frequent string values in the specified string field. -Note that the string field must be indexed as +String facets allow you to narrow down Atlas Search results +based on the most frequent string values in the specified string field. +Note that the string field must be indexed as [How to Index String Fields For Faceted Search](https://www.mongodb.com/docs/atlas/atlas-search/field-types/string-facet-type/#std-label-bson-data-types-string-facet). String facets have the following syntax: @@ -80,7 +80,7 @@ # Numeric Facets -Numeric facets allow you to determine the frequency of numeric values in your search results +Numeric facets allow you to determine the frequency of numeric values in your search results by breaking the results into separate ranges of numbers. Numeric facets have the following syntax: @@ -128,8 +128,8 @@ # Facet Results ---------------------------------------------- -For a facet query, Atlas Search returns a mapping of the defined facet names to an array of buckets for that facet in the results. -The facet result document contains the buckets option, which is an array of resulting buckets for the facet. +For a facet query, Atlas Search returns a mapping of the defined facet names to an array of buckets for that facet in the results. +The facet result document contains the buckets option, which is an array of resulting buckets for the facet. Each facet bucket document in the array has the following fields: Option Type Description @@ -141,12 +141,12 @@ # SEARCH_META Aggregation Variable ---------------------------------------------- -When you run your query using the $search stage, -Atlas Search stores the metadata results in the $$SEARCH_META variable -and returns only the search results. +When you run your query using the $search stage, +Atlas Search stores the metadata results in the $$SEARCH_META variable +and returns only the search results. You can use the $$SEARCH_META variable in all the supported aggregation pipeline stages to view the metadata results for your $search query. -MongoDB recommends using the $$SEARCH_META variable only if you need both the search results and the metadata results. +MongoDB recommends using the $$SEARCH_META variable only if you need both the search results and the metadata results. Otherwise, use the: * $search stage for just the search results. @@ -172,7 +172,7 @@ from monggregate.base import BaseModel, pyd, Expression from monggregate.fields import FieldName from monggregate.search.collectors.collector import SearchCollector -from monggregate.search.operators import( +from monggregate.search.operators import ( Autocomplete, Compound, Equals, @@ -182,49 +182,61 @@ Regex, Text, Wildcard, - AnyOperator + AnyOperator, ) from monggregate.search.operators.operator import OperatorLiteral from monggregate.search.commons import FuzzyOptions # Aliases # ---------------------------------------------- -FacetType = Literal['string', 'number', 'date'] +FacetType = Literal["string", "number", "date"] + # Strings # ---------------------------------------------- class FacetName(FieldName): """ Subclass of FieldName to represent a facet name - + Facets should refer to collctions fields that are indexed as facet fields. """ + # Results # ---------------------------------------------- class FacetBucket(BaseModel): """ Represents a facet bucket. - + A facet bucket is an occurence (for categorical facets) or a range(for numeric facets) of a facet value in the search results. and the number of documents in the search results that have that facet value. """ - _id : str|int|float|datetime - count : int + _id: str | int | float | datetime + count: int + + @property + def expression(self) -> str: + """Return the expression for the facet bucket.""" + return self.model_dump(by_alias=True) class FacetBuckets(BaseModel): """ Represents the facet buckets for a facet. - - The facet result document contains the buckets option, + + The facet result document contains the buckets option, which is an array of resulting buckets for the facet. """ - buckets : list[FacetBucket] + buckets: list[FacetBucket] + + @property + def expression(self) -> str: + """Return the expression for the facet buckets.""" + return self.model_dump(by_alias=True) FacetResult = dict[FacetName, FacetBuckets] @@ -235,48 +247,51 @@ class FacetBuckets(BaseModel): class FacetDefinition(BaseModel): """ Abstract base class for facet definitions - + Used to define StringFacet, NumericFacet and DateFacet which are classes defining the parameters used to define the facets that need to be computed in a search query. """ - path : str - name : FacetName = "" - + path: str + name: FacetName = "" + + @property + def expression(self) -> str: + """Return the expression for the facet definition.""" + return self.model_dump(by_alias=True) - @pyd.validator('name', pre=True, always=True) - def set_name(cls, name: str, values:dict[str,str]) -> FacetName: + @pyd.validator("name", pre=True, always=True) + def set_name(cls, name: str, values: dict[str, str]) -> FacetName: """Sets the name from the field path""" path = values["path"] if not name: - name = path # TODO : Maybe the path might need to be cleaned ? (., $, etc) + name = path # TODO : Maybe the path might need to be cleaned ? (., $, etc) return name - + class StringFacet(FacetDefinition): """ String facet definition - + Attributes ------------------------- - type, Literal['string'] : facet type. Defaults to 'string' and has to be 'string' - path, str : path to the field to facet on - - num_buckets, int : Maximum number of facet categories to return in the results. - Value must be less than or equal to 1000. - If specified, Atlas Search may return fewer categories than requested if the data is grouped into fewer categories than your requested number. + - num_buckets, int : Maximum number of facet categories to return in the results. + Value must be less than or equal to 1000. + If specified, Atlas Search may return fewer categories than requested if the data is grouped into fewer categories than your requested number. If omitted, defaults to 10, which means that Atlas Search will return only the top 10 facet categories by count. - + """ - type : Literal['string'] = 'string' - num_buckets : int = 10 + type: Literal["string"] = "string" + num_buckets: int = 10 @property def expression(self) -> Expression: - - return self.express({self.name : self.dict(by_alias=True, exclude={"name"})}) + return self.express({self.name: self.dict(by_alias=True, exclude={"name"})}) class NumericFacet(FacetDefinition): @@ -291,17 +306,16 @@ class NumericFacet(FacetDefinition): the boundaries for each bucket. You must specify at least two boundaries. Each adjacent pair of values acts aas the inclusive lower boundary and exclusive upper boundary for a bucket. - default, str : default bucket name to use for values that do not fall in any bucket - + """ - type : Literal['number'] = 'number' - boundaries : list[int|float] - default : str|None + type: Literal["number"] = "number" + boundaries: list[int | float] + default: str | None @property def expression(self) -> Expression: - - return self.express({self.name : self.dict(by_alias=True, exclude={"name"})}) + return self.express({self.name: self.dict(by_alias=True, exclude={"name"})}) class DateFacet(FacetDefinition): @@ -316,18 +330,19 @@ class DateFacet(FacetDefinition): """ - type : Literal['date'] = 'date' - boundaries : list[datetime] - default : str|None + type: Literal["date"] = "date" + boundaries: list[datetime] + default: str | None @property def expression(self) -> Expression: - - return self.express({self.name : self.dict(by_alias=True, exclude={"name"})}) + return self.express({self.name: self.dict(by_alias=True, exclude={"name"})}) -AnyFacet = StringFacet|NumericFacet|DateFacet + +AnyFacet = StringFacet | NumericFacet | DateFacet Facets = list[AnyFacet] + # Collector # ---------------------------------------------- class Facet(SearchCollector): @@ -336,22 +351,21 @@ class Facet(SearchCollector): Description ------------------------- - The facet collector groups results by values or ranges in the specified faceted fields + The facet collector groups results by values or ranges in the specified faceted fields and returns the count for each of those groups. Attributes ------------------------- - operator, dict|None : operator to use to combine the facet results with the search results. - facets, dict[FacetName, FacetDefinition] : dictionary of facet definitions to use in the facet query. - - """ - operator : AnyOperator|None - facets : Facets = [] + """ + operator: AnyOperator | None + facets: Facets = [] @pyd.validator("facets") - def validate_facets(cls, facets:Facets)->Facets: + def validate_facets(cls, facets: Facets) -> Facets: """ Validates facets. Ensures the facets names are unique @@ -366,102 +380,85 @@ def validate_facets(cls, facets:Facets)->Facets: msg += "\n" msg += f"Facets names : {names}" raise ValueError(msg) - - return facets + return facets @property def expression(self) -> Expression: - if not self.facets: raise ValueError("No facets were defined") - - _statement = { - "facet":{ - "facets":{} - } - } + _statement = {"facet": {"facets": {}}} for facet in self.facets: _statement["facet"]["facets"].update(facet.expression) if self.operator: - _statement["facet"].update({"operator":self.operator.expression}) - + _statement["facet"].update({"operator": self.operator.expression}) + return self.express(_statement) - - #--------------------------------------------------------- + + # --------------------------------------------------------- # Constructors - #--------------------------------------------------------- + # --------------------------------------------------------- @classmethod def from_operator( - cls, - operator_name:OperatorLiteral, - path:str|list[str]|None=None, - query:str|list[str]|None=None, - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs:Any)->Self: + cls, + operator_name: OperatorLiteral, + path: str | list[str] | None = None, + query: str | list[str] | None = None, + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Instantiates a search stage from a search operator""" - kwargs.update( - { - "path":path, - "query":query, - "fuzzy":fuzzy, - "score":score - } - ) + kwargs.update({"path": path, "query": query, "fuzzy": fuzzy, "score": score}) return cls.__get_constructors_map__(operator_name)(**kwargs) - @classmethod def init_autocomplete( cls, - query:str|list[str], - path:str, - token_order:str="any", - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs:Any)->Self: + query: str | list[str], + path: str, + token_order: str = "any", + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """ Creates a search stage with an autocomplete operator - + Summary: ----------------------------- This stage searches for a word or phrase that contains a sequence of characters from an incomplete input string. """ - _autocomplete = Autocomplete( query=query, path=path, token_order=token_order, fuzzy=fuzzy, score=score, - **kwargs + **kwargs, ) return cls(operator=_autocomplete) - @classmethod def init_compound( cls, - minimum_should_clause:int=1, + minimum_should_clause: int = 1, *, - must : list[AnyOperator]=[], - must_not : list[AnyOperator]=[], - should : list[AnyOperator]=[], - filter : list[AnyOperator]=[], - **kwargs:Any - - )->Self: + must: list[AnyOperator] = [], + must_not: list[AnyOperator] = [], + should: list[AnyOperator] = [], + filter: list[AnyOperator] = [], + **kwargs: Any, + ) -> Self: """xxxx""" - _compound = Compound( must=must, @@ -469,20 +466,19 @@ def init_compound( should=should, filter=filter, minimum_should_clause=minimum_should_clause, - **kwargs + **kwargs, ) return cls(operator=_compound) - @classmethod def init_equals( cls, - path:str, - value:str|int|float|bool|datetime, - score:dict|None=None, - **kwargs:Any - )->Self: + path: str, + value: str | int | float | bool | datetime, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """ Creates a search stage with an equals operator @@ -491,21 +487,15 @@ def init_equals( This checks whether a field matches a value you specify. You may want to use this for filtering purposes post textual search. That is you may want to use it in a compound query or as, the second stage of your search. - + """ - - _equals = Equals( - path=path, - value=value, - score=score - ) + _equals = Equals(path=path, value=value, score=score) return cls(operator=_equals) - @classmethod - def init_exists(cls, path:str, **kwargs:Any)->Self: + def init_exists(cls, path: str, **kwargs: Any) -> Self: """ Creates a search stage with an exists operator @@ -514,45 +504,41 @@ def init_exists(cls, path:str, **kwargs:Any)->Self: This checks whether a field matches a value you specify. You may want to use this for filtering purposes post textual search. That is you may want to use it in a compound query or as, the second stage of your search. - - """ + """ _exists = Exists(path=path) return cls(operator=_exists) - - + @classmethod - def init_more_like_this(cls, like:dict|list[dict], **kwargs:Any)->Self: + def init_more_like_this(cls, like: dict | list[dict], **kwargs: Any) -> Self: """ Creates a search stage with a more_like_this operator Summary: -------------------------------- - The moreLikeThis operator returns documents similar to input documents. - The moreLikeThis operator allows you to build features for your applications + The moreLikeThis operator returns documents similar to input documents. + The moreLikeThis operator allows you to build features for your applications that display similar or alternative results based on one or more given documents. """ - - + _more_like_this = MoreLikeThis(like=like) return cls(operator=_more_like_this) - @classmethod def init_range( cls, - path:str|list[str], - gt:int|float|datetime|None=None, - lt:int|float|datetime|None=None, - gte:int|float|datetime|None=None, - lte:int|float|datetime|None=None, - score:dict|None=None, - **kwargs:Any - )->Self: + path: str | list[str], + gt: int | float | datetime | None = None, + lt: int | float | datetime | None = None, + gte: int | float | datetime | None = None, + lte: int | float | datetime | None = None, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """ Creates a search stage with a range operator @@ -561,133 +547,111 @@ def init_range( This checks whether a field value falls into a specific range You may want to use this for filtering purposes post textual search. That is you may want to use it in a compound query or as, the second stage of your search. - - + + """ - _range = Range( - path=path, - gt=gt, - gte=gte, - lt=lt, - lte=lte, - score=score - ) + _range = Range(path=path, gt=gt, gte=gte, lt=lt, lte=lte, score=score) return cls(operator=_range) - @classmethod def init_regex( cls, - query:str|list[str], - path:str|list[str], - allow_analyzed_field:bool=False, - score:dict|None=None, - **kwargs:Any - )->Self: + query: str | list[str], + path: str | list[str], + allow_analyzed_field: bool = False, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """ Creates a search stage with a regex operator. Summary: ---------------------------- regex interprets the query field as a regular expression. regex is a term-level operator, meaning that the query field isn't analyzed (read processed). - + """ - _regex = Regex( query=query, path=path, allow_analyzed_field=allow_analyzed_field, - score=score + score=score, ) return cls(operator=_regex) - @classmethod def init_text( cls, - query:str|list[str], - path:str|list[str], - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - synonyms:str|None=None, - **kwargs:Any - )->Self: + query: str | list[str], + path: str | list[str], + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + synonyms: str | None = None, + **kwargs: Any, + ) -> Self: """ Creates a search stage with a text opertor Summary: --------------------------------- - The text operator performs a full-text search using the analyzer that you specify in the index configuration. + The text operator performs a full-text search using the analyzer that you specify in the index configuration. If you omit an analyzer, the text operator uses the default standard analyzer. - - """ + """ _text = Text( - query=query, - path=path, - score=score, - fuzzy=fuzzy, - synonyms=synonyms + query=query, path=path, score=score, fuzzy=fuzzy, synonyms=synonyms ) return cls(operator=_text) - @classmethod def init_wildcard( cls, - query:str|list[str], - path:str|list[str], - allow_analyzed_field:bool=False, - score:dict|None=None, - **kwargs:Any - )->Self: + query: str | list[str], + path: str | list[str], + allow_analyzed_field: bool = False, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """ Creates a search stage with a wildcard opertor Summary: --------------------------------- The wildcard operator enables queries which use special characters in the search string that can match any character. - + """ - _wilcard = Wildcard( query=query, path=path, allow_analyzed_field=allow_analyzed_field, - score=score + score=score, ) return cls(operator=_wilcard) - # ---------------------------------------------- # Operators # ---------------------------------------------- def autocomplete( - self, - *, - query:str|list[str], - path:str, - token_order:str="any", - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + *, + query: str | list[str], + path: str, + token_order: str = "any", + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds an autocomplete clause to the current facet instance.""" - + _autocomplete = Autocomplete( - query=query, - path=path, - token_order=token_order, - fuzzy=fuzzy, - score=score + query=query, path=path, token_order=token_order, fuzzy=fuzzy, score=score ) clause_type = kwargs.get("type", "should") @@ -696,38 +660,37 @@ def autocomplete( else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _autocomplete elif isinstance(self.operator, Compound): self.operator.autocomplete( type=clause_type, - minimum_should_match=minimum_should_match, - **_autocomplete.dict()) + minimum_should_match=minimum_should_match, + **_autocomplete.dict(), + ) else: new_operator = Compound( should=[self.operator, _autocomplete], - minimum_should_match=minimum_should_match - ) + minimum_should_match=minimum_should_match, + ) self.operator = new_operator return self - + def equals( - self, - path:str, - value:str|int|float|bool|datetime, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + path: str, + value: str | int | float | bool | datetime, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds an equals clause to the current facet instance.""" - _equals = Equals( - path=path, - value=value, - score=score - ) + _equals = Equals(path=path, value=value, score=score) clause_type = kwargs.get("type", "should") if clause_type == "should": @@ -735,111 +698,105 @@ def equals( else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _equals elif isinstance(self.operator, Compound): self.operator.equals( type=clause_type, - minimum_should_match=minimum_should_match, - **_equals.dict()) + minimum_should_match=minimum_should_match, + **_equals.dict(), + ) else: new_operator = Compound( should=[self.operator, _equals], - minimum_should_match=minimum_should_match - ) + minimum_should_match=minimum_should_match, + ) self.operator = new_operator - - return self - def exists(self, path:str, **kwargs:Any)->Self: + def exists(self, path: str, **kwargs: Any) -> Self: """Adds an exists clause to the current facet instance.""" _exists = Exists(path=path) - + clause_type = kwargs.get("type", "should") if clause_type == "should": default_minimum_should_match = 1 else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _exists elif isinstance(self.operator, Compound): self.operator.exists( type=clause_type, - minimum_should_match=minimum_should_match, - **_exists.dict()) + minimum_should_match=minimum_should_match, + **_exists.dict(), + ) else: new_operator = Compound( should=[self.operator, _exists], - minimum_should_match=minimum_should_match - ) + minimum_should_match=minimum_should_match, + ) self.operator = new_operator - return self - - def more_like_this( - self, - like:dict|list[dict], - **kwargs:Any - )->Self: + + def more_like_this(self, like: dict | list[dict], **kwargs: Any) -> Self: """Adds a more_like_this clause to the current facet instance.""" _more_like_this = MoreLikeThis(like=like) - + clause_type = kwargs.get("type", "should") if clause_type == "should": default_minimum_should_match = 1 else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _more_like_this elif isinstance(self.operator, Compound): self.operator.more_like_this( type=clause_type, - minimum_should_match=minimum_should_match, - **_more_like_this.dict()) + minimum_should_match=minimum_should_match, + **_more_like_this.dict(), + ) else: new_operator = Compound( should=[self.operator, _more_like_this], - minimum_should_match=minimum_should_match - ) + minimum_should_match=minimum_should_match, + ) self.operator = new_operator - return self def range( - self, - *, - path:str|list[str], - gt:int|float|datetime|None=None, - lt:int|float|datetime|None=None, - gte:int|float|datetime|None=None, - lte:int|float|datetime|None=None, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + *, + path: str | list[str], + gt: int | float | datetime | None = None, + lt: int | float | datetime | None = None, + gte: int | float | datetime | None = None, + lte: int | float | datetime | None = None, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds a range clause to the current facet instance.""" - _range = Range( - path=path, - gt=gt, - gte=gte, - lt=lt, - lte=lte, - score=score - ) + _range = Range(path=path, gt=gt, gte=gte, lt=lt, lte=lte, score=score) clause_type = kwargs.get("type", "should") if clause_type == "should": @@ -847,42 +804,43 @@ def range( else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _range elif isinstance(self.operator, Compound): self.operator.range( type=clause_type, - minimum_should_match=minimum_should_match, - **_range.dict()) + minimum_should_match=minimum_should_match, + **_range.dict(), + ) else: new_operator = Compound( should=[self.operator, _range], - minimum_should_match=minimum_should_match - ) + minimum_should_match=minimum_should_match, + ) self.operator = new_operator - - return self def regex( - self, - *, - query:str|list[str], - path:str|list[str], - allow_analyzed_field:bool=False, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + *, + query: str | list[str], + path: str | list[str], + allow_analyzed_field: bool = False, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds a regex clause to the current facet instance.""" _regex = Regex( query=query, path=path, allow_analyzed_field=allow_analyzed_field, - score=score + score=score, ) clause_type = kwargs.get("type", "should") @@ -891,43 +849,41 @@ def regex( else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _regex elif isinstance(self.operator, Compound): self.operator.regex( type=clause_type, - minimum_should_match=minimum_should_match, - **_regex.dict()) + minimum_should_match=minimum_should_match, + **_regex.dict(), + ) else: new_operator = Compound( should=[self.operator, _regex], - minimum_should_match=minimum_should_match - ) + minimum_should_match=minimum_should_match, + ) self.operator = new_operator - return self def text( - self, - *, - query:str|list[str], - path:str|list[str], - fuzzy:FuzzyOptions|None=None, - score:dict|None=None, - synonyms:str|None=None, - **kwargs:Any - )->Self: + self, + *, + query: str | list[str], + path: str | list[str], + fuzzy: FuzzyOptions | None = None, + score: dict | None = None, + synonyms: str | None = None, + **kwargs: Any, + ) -> Self: """Adds a text clause to the current facet instance.""" _text = Text( - query=query, - path=path, - score=score, - fuzzy=fuzzy, - synonyms=synonyms + query=query, path=path, score=score, fuzzy=fuzzy, synonyms=synonyms ) clause_type = kwargs.get("type", "should") @@ -936,42 +892,42 @@ def text( else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _text elif isinstance(self.operator, Compound): self.operator.text( type=clause_type, - minimum_should_match=minimum_should_match, - **_text.dict()) + minimum_should_match=minimum_should_match, + **_text.dict(), + ) else: new_operator = Compound( - should=[self.operator, _text], - minimum_should_match=minimum_should_match - ) + should=[self.operator, _text], minimum_should_match=minimum_should_match + ) self.operator = new_operator - - return self def wildcard( - self, - *, - query:str|list[str], - path:str|list[str], - allow_analyzed_field:bool=False, - score:dict|None=None, - **kwargs:Any - )->Self: + self, + *, + query: str | list[str], + path: str | list[str], + allow_analyzed_field: bool = False, + score: dict | None = None, + **kwargs: Any, + ) -> Self: """Adds a wildcard clause to the current facet instance.""" _wildcard = Wildcard( query=query, path=path, allow_analyzed_field=allow_analyzed_field, - score=score + score=score, ) clause_type = kwargs.get("type", "should") @@ -980,207 +936,158 @@ def wildcard( else: default_minimum_should_match = 0 - minimum_should_match = kwargs.pop("minimum_should_match", default_minimum_should_match) + minimum_should_match = kwargs.pop( + "minimum_should_match", default_minimum_should_match + ) if not self.operator: self.operator = _wildcard elif isinstance(self.operator, Compound): self.operator.wildcard( type=clause_type, - minimum_should_match=minimum_should_match, - **_wildcard.dict()) + minimum_should_match=minimum_should_match, + **_wildcard.dict(), + ) else: new_operator = Compound( should=[self.operator, _wildcard], - minimum_should_match=minimum_should_match - ) + minimum_should_match=minimum_should_match, + ) self.operator = new_operator - return self - + # ---------------------------------------------- # Facets # ---------------------------------------------- def facet( - self, - path:str, - name:str|None=None, - type:FacetType='string', - num_buckets:int|None=None, - boundaries:list[int|float]|list[datetime]|None=None, - default:str|None=None, - **kwargs:Any # NOTE : To prevent errors from passing extra argumentscf #100 on GitHub - )->Self: - - if type=="string": + self, + path: str, + name: str | None = None, + type: FacetType = "string", + num_buckets: int | None = None, + boundaries: list[int | float] | list[datetime] | None = None, + default: str | None = None, + **kwargs: Any, # NOTE : To prevent errors from passing extra argumentscf #100 on GitHub + ) -> Self: + if type == "string": if num_buckets is None: num_buckets = 10 - facet = StringFacet( - name=name, - path=path, - num_buckets=num_buckets - ) - elif type=="number": + facet = StringFacet(name=name, path=path, num_buckets=num_buckets) + elif type == "number": facet = NumericFacet( - name=name, - path=path, - boundaries=boundaries, - default=default + name=name, path=path, boundaries=boundaries, default=default ) - elif type=="date": + elif type == "date": facet = DateFacet( - name=name, - path=path, - boundaries=boundaries, - default=default + name=name, path=path, boundaries=boundaries, default=default ) else: - raise ValueError(f"Invalid facet type. Valid facet types are 'string', 'number' and 'date'. Got {type} instead.") + raise ValueError( + f"Invalid facet type. Valid facet types are 'string', 'number' and 'date'. Got {type} instead." + ) self.facets.append(facet) return self def numeric( - self, - path:str, - *, - boundaries:list[int|float], - name:str|None=None, - default:str|None=None - )->Self: + self, + path: str, + *, + boundaries: list[int | float], + name: str | None = None, + default: str | None = None, + ) -> Self: """Adds a numeric facet to the current facet instance.""" self.facet( - type="number", - path=path, - name=name, - boundaries=boundaries, - default=default + type="number", path=path, name=name, boundaries=boundaries, default=default ) return self - + def date( - self, - path:str, - *, - boundaries:list[datetime], - name:str|None=None, - default:str|None=None - )->Self: + self, + path: str, + *, + boundaries: list[datetime], + name: str | None = None, + default: str | None = None, + ) -> Self: """Adds a date facet to the current facet instance.""" self.facet( - type="date", - path=path, - name=name, - boundaries=boundaries, - default=default + type="date", path=path, name=name, boundaries=boundaries, default=default ) return self - + def string( - self, - path:str, - *, - num_buckets:int=10, - name:str|None=None - )->Self: + self, path: str, *, num_buckets: int = 10, name: str | None = None + ) -> Self: """Adds a string facet to the current facet instance.""" - self.facet( - type="string", - path=path, - name=name, - num_buckets=num_buckets - ) + self.facet(type="string", path=path, name=name, num_buckets=num_buckets) return self - + # ---------------------------------------------- # Facet Interface # ---------------------------------------------- @staticmethod def NumericFacet( *, - path:str, - name:FacetName|None=None, - boundaries:list[int|float], - default:str|None=None - )->NumericFacet: + path: str, + name: FacetName | None = None, + boundaries: list[int | float], + default: str | None = None, + ) -> NumericFacet: """Returns a numeric facet instance.""" return NumericFacet( - name=name, - path=path, - boundaries=boundaries, - default=default + name=name, path=path, boundaries=boundaries, default=default ) - + @staticmethod def StringFacet( - *, - path:str, - name:FacetName|None=None, - num_buckets:int=10 - )->StringFacet: + *, path: str, name: FacetName | None = None, num_buckets: int = 10 + ) -> StringFacet: """Returns a string facet instance.""" - return StringFacet( - name=name, - path=path, - num_buckets=num_buckets - ) - + return StringFacet(name=name, path=path, num_buckets=num_buckets) + @staticmethod def DateFacet( *, - path:str, - name:FacetName|None=None, - boundaries:list[datetime], - default:str|None=None - )->DateFacet: + path: str, + name: FacetName | None = None, + boundaries: list[datetime], + default: str | None = None, + ) -> DateFacet: """Returns a date facet instance.""" - return DateFacet( - name=name, - path=path, - boundaries=boundaries, - default=default - ) + return DateFacet(name=name, path=path, boundaries=boundaries, default=default) # TODO : Overload this method to make return type more precise. @staticmethod def Facet( *, - type:Literal['string', 'number', 'date'], - path:str, - name:FacetName|None=None, - num_buckets:int=10, - boundaries:list[int|float]|list[datetime]|None=None, - default:str|None=None - )->AnyFacet: + type: Literal["string", "number", "date"], + path: str, + name: FacetName | None = None, + num_buckets: int = 10, + boundaries: list[int | float] | list[datetime] | None = None, + default: str | None = None, + ) -> AnyFacet: """Returns a facet instance.""" - if type=="string": - facet = Facet.StringFacet( - name=name, - path=path, - num_buckets=num_buckets - ) - elif type=="number": + if type == "string": + facet = Facet.StringFacet(name=name, path=path, num_buckets=num_buckets) + elif type == "number": facet = Facet.NumericFacet( - name=name, - path=path, - boundaries=boundaries, - default=default + name=name, path=path, boundaries=boundaries, default=default ) else: facet = Facet.DateFacet( - name=name, - path=path, - boundaries=boundaries, - default=default + name=name, path=path, boundaries=boundaries, default=default ) return facet @@ -1189,20 +1096,20 @@ def Facet( # Utilities # ---------------------------------------------- @classmethod - def __get_constructors_map__(cls, operator_name:str)->Callable[...,Self]: + def __get_constructors_map__(cls, operator_name: str) -> Callable[..., Self]: """Returns appropriate constructor from operator name""" _constructors_map = { - "autocomplete":cls.init_autocomplete, - "compound":cls.init_compound, - "equals":cls.init_equals, - "exists":cls.init_exists, - #"facet":cls.init_facet, - "more_like_this":cls.init_more_like_this, - "range":cls.init_range, - "regex":cls.init_regex, - "text":cls.init_text, - "wildcard":cls.init_wildcard + "autocomplete": cls.init_autocomplete, + "compound": cls.init_compound, + "equals": cls.init_equals, + "exists": cls.init_exists, + # "facet":cls.init_facet, + "more_like_this": cls.init_more_like_this, + "range": cls.init_range, + "regex": cls.init_regex, + "text": cls.init_text, + "wildcard": cls.init_wildcard, } - return _constructors_map[operator_name] \ No newline at end of file + return _constructors_map[operator_name] diff --git a/src/monggregate/search/commons/highlight.py b/src/monggregate/search/commons/highlight.py index dfb5ab87..57202ff1 100644 --- a/src/monggregate/search/commons/highlight.py +++ b/src/monggregate/search/commons/highlight.py @@ -8,23 +8,41 @@ from monggregate.base import BaseModel, pyd -# TODO : Check if those are missing an expression property + class HighlightOptions(BaseModel): """Class defining the highlighting parameters.""" - path : str - max_chars_to_examine : int = pyd.Field(500000, alias="maxCharsToExamine") - max_num_passages : int = pyd.Field(5, alias="maxNumPassages") + path: str + max_chars_to_examine: int = pyd.Field(500000, alias="maxCharsToExamine") + max_num_passages: int = pyd.Field(5, alias="maxNumPassages") + + @property + def expression(self) -> str: + """Return the expression for the highlighting.""" + + return self.model_dump(by_alias=True) + class HighlightText(BaseModel): """Highlighted text.""" - value : str - type : Literal["hit", "text"] + value: str + type: Literal["hit", "text"] + + @property + def expression(self) -> str: + """Return the expression for the highlighting.""" + return self.model_dump(by_alias=True) + class HightlightOutput(BaseModel): """Class defining the highlights appear in a search query results.""" - path : str - texts : list[HighlightText] - score : float + path: str + texts: list[HighlightText] + score: float + + @property + def expression(self) -> str: + """Return the expression for the highlighting.""" + return self.model_dump(by_alias=True) diff --git a/tests/tests_monggregate/tests_operators/tests_custom/__init__.py b/tests/tests_monggregate/tests_operators/tests_custom/__init__.py index 2c866a40..54ccf3d6 100644 --- a/tests/tests_monggregate/tests_operators/tests_custom/__init__.py +++ b/tests/tests_monggregate/tests_operators/tests_custom/__init__.py @@ -1,4 +1 @@ """Tests for `monggregate.operators.custom.custom`.""" - -from monggregate.operators.custom.custom import CustomOperator -# TODO diff --git a/tests/tests_monggregate/tests_search/tests_collectors/test_facet.py b/tests/tests_monggregate/tests_search/tests_collectors/test_facet.py index 46409041..758ef485 100644 --- a/tests/tests_monggregate/tests_search/tests_collectors/test_facet.py +++ b/tests/tests_monggregate/tests_search/tests_collectors/test_facet.py @@ -1 +1,74 @@ -# TODO +"""Tests for `monggregate.search.collectors.facet` module.""" + +import pytest + +from monggregate.search.collectors.facet import Facet, FacetName, FacetBucket + + +class TestFacetName: + """Tests for the `FacetName` class.""" + + def test_is_subclass_of_field_name(self) -> None: + """Test that `FacetName` is a subclass of `FieldName`.""" + from monggregate.fields import FieldName + + assert issubclass(FacetName, FieldName) + + +class TestFacetBucket: + """Tests for the `FacetBucket` class.""" + + @pytest.mark.xfail( + reason="Pydantic v1 ignore fields starting with _. Will be fixed in Pydantic v2 or by config." + ) + def test_has_required_fields(self) -> None: + """Test that `FacetBucket` has the required fields.""" + bucket = FacetBucket(_id="test", count=1) + assert bucket._id == "test" + assert bucket.count == 1 + + +class TestFacet: + """Tests for the `Facet` class.""" + + def test_is_subclass_of_search_collector(self) -> None: + """Test that `Facet` is a subclass of `SearchCollector`.""" + from monggregate.search.collectors.collector import SearchCollector + + assert issubclass(Facet, SearchCollector) + + def test_init_autocomplete(self) -> None: + """Test that `init_autocomplete` creates a facet with an autocomplete operator.""" + from monggregate.search.operators import Autocomplete + + facet = Facet.init_autocomplete(query="test", path="field") + assert isinstance(facet.operator, Autocomplete) + assert facet.operator.query == "test" + assert facet.operator.path == "field" + + def test_init_text(self) -> None: + """Test that `init_text` creates a facet with a text operator.""" + from monggregate.search.operators import Text + + facet = Facet.init_text(query="test", path="field") + assert isinstance(facet.operator, Text) + assert facet.operator.query == "test" + assert facet.operator.path == "field" + + def test_init_regex(self) -> None: + """Test that `init_regex` creates a facet with a regex operator.""" + from monggregate.search.operators import Regex + + facet = Facet.init_regex(query="test", path="field") + assert isinstance(facet.operator, Regex) + assert facet.operator.query == "test" + assert facet.operator.path == "field" + + def test_init_wildcard(self) -> None: + """Test that `init_wildcard` creates a facet with a wildcard operator.""" + from monggregate.search.operators import Wildcard + + facet = Facet.init_wildcard(query="test", path="field") + assert isinstance(facet.operator, Wildcard) + assert facet.operator.query == "test" + assert facet.operator.path == "field" diff --git a/tests/tests_monggregate/tests_search/tests_commons/test_count.py b/tests/tests_monggregate/tests_search/tests_commons/test_count.py index 46409041..1f0b90e9 100644 --- a/tests/tests_monggregate/tests_search/tests_commons/test_count.py +++ b/tests/tests_monggregate/tests_search/tests_commons/test_count.py @@ -1 +1,54 @@ -# TODO +"""Tests for `monggregate.search.commons.count` module.""" + +import pytest + +from monggregate.search.commons.count import CountOptions, CountResults + + +class TestCountOptions: + """Tests for the `CountOptions` class.""" + + def test_default_values(self) -> None: + """Test that `CountOptions` has the correct default values.""" + options = CountOptions() + assert options.type == "lowerBound" + assert options.threshold == 1000 + + def test_custom_values(self) -> None: + """Test that `CountOptions` accepts custom values.""" + options = CountOptions(type="total", threshold=500) + assert options.type == "total" + assert options.threshold == 500 + + def test_type_validation(self) -> None: + """Test that `CountOptions` validates the type field.""" + options = CountOptions(type="lower_bound") + assert options.type == "lowerBound" + + def test_expression_property(self) -> None: + """Test that `CountOptions` has an expression property.""" + options = CountOptions() + expression = options.expression + assert expression is not None + + +class TestCountResults: + """Tests for the `CountResults` class.""" + + def test_has_required_fields(self) -> None: + """Test that `CountResults` has the required fields.""" + results = CountResults(lower_bound=10, total=20) + assert results.lower_bound == 10 + assert results.total == 20 + + def test_optional_fields(self) -> None: + """Test that `CountResults` fields are optional.""" + results = CountResults() + assert results.lower_bound is None + assert results.total is None + + def test_expression_property(self) -> None: + """Test that `CountResults` has an expression property.""" + results = CountResults(lower_bound=10, total=20) + expression = results.expression + assert expression is not None diff --git a/tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py b/tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py index 46409041..30f29ab1 100644 --- a/tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py +++ b/tests/tests_monggregate/tests_search/tests_commons/test_fuzzy.py @@ -1 +1,37 @@ -# TODO +"""Tests for `monggregate.search.commons.fuzzy` module.""" + +import pytest + +from monggregate.search.commons.fuzzy import FuzzyOptions + + +class TestFuzzyOptions: + """Tests for the `FuzzyOptions` class.""" + + def test_default_values(self) -> None: + """Test that `FuzzyOptions` has the correct default values.""" + options = FuzzyOptions() + assert options.max_edits == 2 + assert options.max_expansions == 50 + assert options.prefix_length == 0 + + def test_custom_values(self) -> None: + """Test that `FuzzyOptions` accepts custom values.""" + options = FuzzyOptions(max_edits=1, max_expansions=100, prefix_length=2) + assert options.max_edits == 1 + assert options.max_expansions == 100 + assert options.prefix_length == 2 + + def test_expression_property(self) -> None: + """Test that `FuzzyOptions` has an expression property.""" + options = FuzzyOptions() + expression = options.expression + assert expression is not None + + def test_alias_mapping(self) -> None: + """Test that `FuzzyOptions` fields are properly aliased.""" + options = FuzzyOptions() + data = options.dict(by_alias=True) + assert "maxEdits" in data + assert "maxExpansions" in data + assert "prefixLength" in data diff --git a/tests/tests_monggregate/tests_search/tests_commons/test_highlight.py b/tests/tests_monggregate/tests_search/tests_commons/test_highlight.py index 46409041..0cf040ae 100644 --- a/tests/tests_monggregate/tests_search/tests_commons/test_highlight.py +++ b/tests/tests_monggregate/tests_search/tests_commons/test_highlight.py @@ -1 +1,68 @@ -# TODO +"""Tests for `monggregate.search.commons.highlight` module.""" + +import pytest + +from monggregate.search.commons.highlight import ( + HighlightOptions, + HighlightText, + HightlightOutput, +) + + +class TestHighlightOptions: + """Tests for the `HighlightOptions` class.""" + + def test_required_fields(self) -> None: + """Test that `HighlightOptions` has the required fields.""" + options = HighlightOptions(path="field") + assert options.path == "field" + + def test_default_values(self) -> None: + """Test that `HighlightOptions` has the correct default values.""" + options = HighlightOptions(path="field") + assert options.max_chars_to_examine == 500000 + assert options.max_num_passages == 5 + + def test_custom_values(self) -> None: + """Test that `HighlightOptions` accepts custom values.""" + options = HighlightOptions( + path="field", max_chars_to_examine=100000, max_num_passages=3 + ) + assert options.path == "field" + assert options.max_chars_to_examine == 100000 + assert options.max_num_passages == 3 + + def test_alias_mapping(self) -> None: + """Test that `HighlightOptions` fields are properly aliased.""" + options = HighlightOptions(path="field") + data = options.dict(by_alias=True) + assert "maxCharsToExamine" in data + assert "maxNumPassages" in data + + +class TestHighlightText: + """Tests for the `HighlightText` class.""" + + def test_required_fields(self) -> None: + """Test that `HighlightText` has the required fields.""" + text = HighlightText(value="test", type="hit") + assert text.value == "test" + assert text.type == "hit" + + def test_type_validation(self) -> None: + """Test that `HighlightText` validates the type field.""" + with pytest.raises(ValueError): + HighlightText(value="test", type="invalid") + + +class TestHighlightOutput: + """Tests for the `HighlightOutput` class.""" + + def test_required_fields(self) -> None: + """Test that `HighlightOutput` has the required fields.""" + output = HightlightOutput( + path="field", texts=[HighlightText(value="test", type="hit")], score=1.0 + ) + assert output.path == "field" + assert len(output.texts) == 1 + assert output.score == 1.0