diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 472b1384..b36aad27 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -610,3 +610,24 @@ update_embedders_1: |- }) reset_embedders_1: |- client.index('INDEX_NAME').reset_embedders() +list_dynamic_search_rules_1: |- + client.get_dynamic_search_rules() +get_dynamic_search_rule_1: |- + client.get_dynamic_search_rule('RULE_UID') +patch_dynamic_search_rule_1: |- + client.create_or_update_dynamic_search_rule('RULE_UID', { + 'description': 'Rule description', + 'priority': 10, + 'active': True, + 'conditions': [ + {'scope': 'query', 'isEmpty': True} + ], + 'actions': [ + { + 'selector': {'indexUid': 'movies', 'id': '123'}, + 'action': {'type': 'pin', 'position': 1} + } + ], + }) +delete_dynamic_search_rule_1: |- + client.delete_dynamic_search_rule('RULE_UID') diff --git a/meilisearch/client.py b/meilisearch/client.py index bd0ec8fa..13e92fab 100644 --- a/meilisearch/client.py +++ b/meilisearch/client.py @@ -30,6 +30,7 @@ MeilisearchError, ) from meilisearch.index import Index +from meilisearch.models.dynamic_search_rule import DynamicSearchRule, DynamicSearchRuleResults from meilisearch.models.key import Key, KeysResults from meilisearch.models.task import Batch, BatchResults, Task, TaskInfo, TaskResults from meilisearch.models.webhook import Webhook, WebhooksResults @@ -601,6 +602,103 @@ def delete_webhook(self, webhook_uuid: str) -> int: response = self.http.delete(f"{self.config.paths.webhooks}/{webhook_uuid}") return response.status_code + # DYNAMIC SEARCH RULES ROUTES + + def get_dynamic_search_rules(self) -> DynamicSearchRuleResults: + """Get all dynamic search rules. + + Returns + ------- + rules: + DynamicSearchRuleResults instance containing list of rules and pagination info. + https://www.meilisearch.com/docs/reference/api/dynamic-search-rules/list-dynamic-search-rules + + Raises + ------ + MeilisearchApiError + An error containing details about why Meilisearch can't process your request. + Meilisearch error codes are described here: + https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors + """ + rules = self.http.post(self.config.paths.dynamic_search_rules, {}) + return DynamicSearchRuleResults(**rules) + + def get_dynamic_search_rule(self, uid: str) -> DynamicSearchRule: + """Get information about a specific dynamic search rule. + + Parameters + ---------- + uid: + The uid of the dynamic search rule to retrieve. + + Returns + ------- + rule: + The dynamic search rule information. + https://www.meilisearch.com/docs/reference/api/dynamic-search-rules/get-a-dynamic-search-rule + + Raises + ------ + MeilisearchApiError + An error containing details about why Meilisearch can't process your request. + Meilisearch error codes are described here: + https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors + """ + rule = self.http.get(f"{self.config.paths.dynamic_search_rules}/{uid}") + return DynamicSearchRule(**rule) + + def create_or_update_dynamic_search_rule( + self, uid: str, options: Mapping[str, Any] + ) -> DynamicSearchRule: + """Create or update a dynamic search rule. + + Parameters + ---------- + uid: + The uid of the dynamic search rule to create or update. + options: + The dynamic search rule fields to create or update. + + Returns + ------- + rule: + The created or updated dynamic search rule. + https://www.meilisearch.com/docs/reference/api/dynamic-search-rules/create-or-update-a-dynamic-search-rule + + Raises + ------ + MeilisearchApiError + An error containing details about why Meilisearch can't process your request. + Meilisearch error codes are described here: + https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors + """ + rule = self.http.patch(f"{self.config.paths.dynamic_search_rules}/{uid}", options) + return DynamicSearchRule(**rule) + + def delete_dynamic_search_rule(self, uid: str) -> int: + """Delete a dynamic search rule. + + Parameters + ---------- + uid: + The uid of the dynamic search rule to delete. + + Returns + ------- + status_code: + The Response status code. 204 signifies a successful delete. + https://www.meilisearch.com/docs/reference/api/dynamic-search-rules/delete-a-dynamic-search-rule + + Raises + ------ + MeilisearchApiError + An error containing details about why Meilisearch can't process your request. + Meilisearch error codes are described here: + https://www.meilisearch.com/docs/reference/errors/error_codes#meilisearch-errors + """ + response = self.http.delete(f"{self.config.paths.dynamic_search_rules}/{uid}") + return response.status_code + def get_version(self) -> Dict[str, str]: """Get version Meilisearch diff --git a/meilisearch/config.py b/meilisearch/config.py index 3b9fa457..dfee3dda 100644 --- a/meilisearch/config.py +++ b/meilisearch/config.py @@ -50,6 +50,7 @@ class Paths: network = "network" experimental_features = "experimental-features" webhooks = "webhooks" + dynamic_search_rules = "dynamic-search-rules" export = "export" def __init__( diff --git a/meilisearch/models/dynamic_search_rule.py b/meilisearch/models/dynamic_search_rule.py new file mode 100644 index 00000000..1b190f4d --- /dev/null +++ b/meilisearch/models/dynamic_search_rule.py @@ -0,0 +1,27 @@ +from typing import Any, Dict, List, Optional + +from camel_converter.pydantic_base import CamelBase +from pydantic import ConfigDict + + +class DynamicSearchRule(CamelBase): + """Model for a Meilisearch dynamic search rule.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + + uid: str + description: Optional[str] = None + priority: Optional[int] = None + active: Optional[bool] = None + conditions: Optional[List[Dict[str, Any]]] = None + actions: Optional[List[Dict[str, Any]]] = None + + +class DynamicSearchRuleResults(CamelBase): + """Model for dynamic search rules list results.""" + + model_config = ConfigDict(arbitrary_types_allowed=True) + results: List[DynamicSearchRule] + offset: int + limit: int + total: int diff --git a/tests/client/test_client_dynamic_search_rules_meilisearch.py b/tests/client/test_client_dynamic_search_rules_meilisearch.py new file mode 100644 index 00000000..f1ce22d8 --- /dev/null +++ b/tests/client/test_client_dynamic_search_rules_meilisearch.py @@ -0,0 +1,118 @@ +"""Tests for dynamic search rule management endpoints.""" + +import pytest + +from meilisearch.errors import MeilisearchApiError + +pytestmark = pytest.mark.usefixtures("enable_dynamic_search_rules") + + +def test_get_dynamic_search_rules_empty(client): + """Test getting dynamic search rules when none exist.""" + rules = client.get_dynamic_search_rules() + assert rules.results is not None + assert isinstance(rules.results, list) + assert len(rules.results) == 0 + + +def test_create_or_update_dynamic_search_rule(client): + """Test creating a dynamic search rule.""" + rule_data = { + "description": "Test rule for promotion", + "priority": 10, + "active": True, + "conditions": [{"scope": "query", "isEmpty": True}], + "actions": [ + { + "selector": {"indexUid": "movies", "id": "123"}, + "action": {"type": "pin", "position": 1}, + } + ], + } + + rule = client.create_or_update_dynamic_search_rule("test-rule", rule_data) + + assert rule.uid == "test-rule" + assert rule.description == rule_data["description"] + assert rule.priority == 10 + assert rule.active is True + + +def test_get_dynamic_search_rule(client): + """Test getting a single dynamic search rule.""" + # Create a rule first + rule_data = { + "description": "Test rule", + "active": True, + } + + created_rule = client.create_or_update_dynamic_search_rule("test-rule", rule_data) + + # Get the rule + rule = client.get_dynamic_search_rule(created_rule.uid) + + assert rule.uid == created_rule.uid + assert rule.description == rule_data["description"] + + +def test_get_dynamic_search_rule_not_found(client): + """Test getting a dynamic search rule that doesn't exist.""" + with pytest.raises(MeilisearchApiError): + client.get_dynamic_search_rule("non-existent-uid") + + +def test_update_dynamic_search_rule(client): + """Test updating a dynamic search rule.""" + # Create a rule first + rule_data = { + "description": "Original description", + "priority": 10, + "active": True, + "conditions": [{"scope": "query", "isEmpty": True}], + "actions": [ + { + "selector": {"indexUid": "movies", "id": "123"}, + "action": {"type": "pin", "position": 1}, + } + ], + } + + created_rule = client.create_or_update_dynamic_search_rule("test-rule", rule_data) + + # Update the rule + update_data = { + "description": "Updated description", + "priority": 5, + } + + updated_rule = client.create_or_update_dynamic_search_rule(created_rule.uid, update_data) + + assert updated_rule.uid == created_rule.uid + assert updated_rule.description == update_data["description"] + assert updated_rule.priority == 5 + + +def test_delete_dynamic_search_rule(client): + """Test deleting a dynamic search rule.""" + # Create a rule first + rule_data = { + "description": "Rule to delete", + "active": True, + } + + created_rule = client.create_or_update_dynamic_search_rule("test-rule", rule_data) + + # Delete the rule + status_code = client.delete_dynamic_search_rule(created_rule.uid) + + assert status_code == 204 + + # Verify it's deleted + with pytest.raises(MeilisearchApiError): + client.get_dynamic_search_rule(created_rule.uid) + + +def test_delete_dynamic_search_rule_not_found(client): + """Test deleting a dynamic search rule that doesn't exist.""" + with pytest.raises(MeilisearchApiError): + client.delete_dynamic_search_rule("non-existent-uid") diff --git a/tests/conftest.py b/tests/conftest.py index cc61935a..36b8be45 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -352,3 +352,20 @@ def enable_network_options(): json={"network": False}, timeout=10, ) + + +@fixture +def enable_dynamic_search_rules(): + requests.patch( + f"{common.BASE_URL}/experimental-features", + headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, + json={"dynamicSearchRules": True}, + timeout=10, + ) + yield + requests.patch( + f"{common.BASE_URL}/experimental-features", + headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, + json={"dynamicSearchRules": False}, + timeout=10, + )