From 354668a2bcd5a82a812660030128711a31d31fd9 Mon Sep 17 00:00:00 2001 From: kangazhan <309998798+kangazhan@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:37:59 +0800 Subject: [PATCH] Add dynamic search rule filter support --- meilisearch/models/search_rule.py | 19 ++++++++++++++ ...client_dynamic_search_rules_meilisearch.py | 20 +++++++++++++++ tests/models/test_search_rule.py | 25 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 tests/models/test_search_rule.py diff --git a/meilisearch/models/search_rule.py b/meilisearch/models/search_rule.py index 8d446490..7da93c53 100644 --- a/meilisearch/models/search_rule.py +++ b/meilisearch/models/search_rule.py @@ -1,7 +1,11 @@ +from datetime import datetime from typing import Any +import pydantic from camel_converter.pydantic_base import CamelBase +from meilisearch._utils import is_pydantic_2, iso_to_date_time + class SearchRule(CamelBase): """A dynamic search rule configured on a Meilisearch instance.""" @@ -12,6 +16,21 @@ class SearchRule(CamelBase): active: bool conditions: dict[str, Any] actions: list[dict[str, Any]] + last_updated_at: datetime | None = None + + if is_pydantic_2(): + + @pydantic.field_validator("last_updated_at", mode="before") # type: ignore[attr-defined] + @classmethod + def validate_last_updated_at(cls, v: str) -> datetime | None: + return iso_to_date_time(v) + + else: # pragma: no cover + + @pydantic.validator("last_updated_at", pre=True) + @classmethod + def validate_last_updated_at(cls, v: str) -> datetime | None: + return iso_to_date_time(v) class SearchRulesResults(CamelBase): diff --git a/tests/client/test_client_dynamic_search_rules_meilisearch.py b/tests/client/test_client_dynamic_search_rules_meilisearch.py index bc537ba1..5a50cf4f 100644 --- a/tests/client/test_client_dynamic_search_rules_meilisearch.py +++ b/tests/client/test_client_dynamic_search_rules_meilisearch.py @@ -1,6 +1,7 @@ """Tests for dynamic search rule endpoints.""" from copy import deepcopy +from datetime import datetime import pytest @@ -38,6 +39,7 @@ def test_get_dynamic_search_rules_with_pagination_and_filters(client): all_rules = client.get_dynamic_search_rules() assert all_rules.total == 2 + assert all(isinstance(rule.last_updated_at, datetime) for rule in all_rules.results) first_page = client.get_dynamic_search_rules({"offset": 0, "limit": 1}) assert first_page.offset == 0 @@ -61,6 +63,7 @@ def test_get_dynamic_search_rule(client): assert rule.description == RULE_OPTIONS["description"] assert rule.precedence == RULE_OPTIONS["precedence"] assert rule.actions == RULE_OPTIONS["actions"] + assert isinstance(rule.last_updated_at, datetime) def test_update_dynamic_search_rule_creates_rule_and_returns_task(client): @@ -95,6 +98,23 @@ def test_update_dynamic_search_rule_updates_rule_and_returns_task(client): assert updated_rule.actions == RULE_OPTIONS["actions"] +def test_update_dynamic_search_rule_with_filter_condition(client): + options = deepcopy(RULE_OPTIONS) + options["conditions"] = { + "filter": { + "values": { + "color": "red", + "category": "shirt", + } + } + } + + _upsert_and_wait(client, "filtered-products", options) + + rule = client.get_dynamic_search_rule("filtered-products") + assert rule.conditions == options["conditions"] + + def test_delete_dynamic_search_rule_returns_task(client): _upsert_and_wait(client, "black-friday", RULE_OPTIONS) diff --git a/tests/models/test_search_rule.py b/tests/models/test_search_rule.py new file mode 100644 index 00000000..fc968a47 --- /dev/null +++ b/tests/models/test_search_rule.py @@ -0,0 +1,25 @@ +from datetime import datetime + +from meilisearch.models.search_rule import SearchRule + +RULE_RESPONSE = { + "uid": "black-friday", + "active": True, + "conditions": {"query": {"words": "black friday"}}, + "actions": [], +} + + +def test_search_rule_parses_last_updated_at(): + rule = SearchRule( + **RULE_RESPONSE, + lastUpdatedAt="2026-07-27T06:47:12.123456789Z", + ) + + assert rule.last_updated_at == datetime(2026, 7, 27, 6, 47, 12, 123456) + + +def test_search_rule_accepts_response_without_last_updated_at(): + rule = SearchRule(**RULE_RESPONSE) + + assert rule.last_updated_at is None