Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions meilisearch/models/search_rule.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand All @@ -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):
Expand Down
20 changes: 20 additions & 0 deletions tests/client/test_client_dynamic_search_rules_meilisearch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for dynamic search rule endpoints."""

from copy import deepcopy
from datetime import datetime

import pytest

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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)

Expand Down
25 changes: 25 additions & 0 deletions tests/models/test_search_rule.py
Original file line number Diff line number Diff line change
@@ -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
Loading