diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 472b1384..413dbfc5 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -610,3 +610,16 @@ update_embedders_1: |- }) reset_embedders_1: |- client.index('INDEX_NAME').reset_embedders() +post_render_template_1: |- + client.update_experimental_features({"renderRoute": True}) + + client.render_template( + template={ + 'kind': 'inlineDocumentTemplate', + 'inline': 'An inline document template rendered on {{doc.id}}' + }, + input={ + 'kind': 'inlineDocument', + 'inline': {'id': 'this document'} + } + ) diff --git a/meilisearch/client.py b/meilisearch/client.py index 5e1d890b..cb7f8319 100644 --- a/meilisearch/client.py +++ b/meilisearch/client.py @@ -280,6 +280,45 @@ def multi_search( body={"queries": queries, "federation": federation}, ) + def render_template( + self, + template: Mapping[str, Any], + input: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + """Render a template against an input document. + + This is an experimental route. Enable the ``renderRoute`` feature on your + Meilisearch instance before calling it, for example + ``client.update_experimental_features({"renderRoute": True})``. + + https://www.meilisearch.com/docs/reference/api/template + + Parameters + ---------- + template: + Dictionary describing the template or fragment to render, for example + {"kind": "inlineDocumentTemplate", "inline": "Rendered on {{doc.id}}"}. + input (optional): + Dictionary describing what to render the template with, for example + {"kind": "inlineDocument", "inline": {"id": "this document"}}. When omitted + the route returns the unrendered template and ``rendered`` is null. + + Returns + ------- + rendered: + Dictionary with the unrendered ``template`` and the ``rendered`` result. + + 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 + """ + body: dict[str, Any] = {"template": template} + if input is not None: + body["input"] = input + + return self.http.post(self.config.paths.render_template, body=body) + def update_documents_by_function( self, index_uid: str, queries: dict[str, list[dict[str, Any]]] ) -> dict[str, Any]: diff --git a/meilisearch/config.py b/meilisearch/config.py index 9c311a64..8cc59496 100644 --- a/meilisearch/config.py +++ b/meilisearch/config.py @@ -49,6 +49,7 @@ class Paths: experimental_features = "experimental-features" webhooks = "webhooks" export = "export" + render_template = "render-template" def __init__( self, diff --git a/tests/client/test_client_render_template.py b/tests/client/test_client_render_template.py new file mode 100644 index 00000000..ef2e9bbd --- /dev/null +++ b/tests/client/test_client_render_template.py @@ -0,0 +1,55 @@ +from unittest.mock import patch + +import pytest + + +def test_render_template_calls_route(client): + """The SDK posts template and input to the render-template route.""" + template = {"kind": "inlineDocumentTemplate", "inline": "Rendered on {{doc.id}}"} + document = {"kind": "inlineDocument", "inline": {"id": "42"}} + + with patch.object( + client.http, "post", return_value={"template": "x", "rendered": "y"} + ) as mock_post: + client.render_template(template=template, input=document) + + mock_post.assert_called_once_with( + "render-template", + body={"template": template, "input": document}, + ) + + +def test_render_template_omits_input_when_none(client): + """Without an input the body only carries the template.""" + template = {"kind": "inlineDocumentTemplate", "inline": "Rendered on {{doc.id}}"} + + with patch.object( + client.http, "post", return_value={"template": "x", "rendered": None} + ) as mock_post: + client.render_template(template=template) + + mock_post.assert_called_once_with("render-template", body={"template": template}) + + +@pytest.mark.usefixtures("enable_render_route") +def test_render_template_inline(client): + """Renders an inline template with an inline document.""" + response = client.render_template( + template={"kind": "inlineDocumentTemplate", "inline": "Rendered on {{doc.id}}"}, + input={"kind": "inlineDocument", "inline": {"id": "42"}}, + ) + + assert isinstance(response, dict) + assert {"template", "rendered"} <= set(response) + assert "42" in response["rendered"] + + +@pytest.mark.usefixtures("enable_render_route") +def test_render_template_without_input(client): + """Omitting the input returns the unrendered template and a null result.""" + response = client.render_template( + template={"kind": "inlineDocumentTemplate", "inline": "Rendered on {{doc.id}}"}, + ) + + assert isinstance(response, dict) + assert response["rendered"] is None diff --git a/tests/conftest.py b/tests/conftest.py index 5efde68d..9d630286 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_render_route(): + requests.patch( + f"{common.BASE_URL}/experimental-features", + headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, + json={"renderRoute": True}, + timeout=10, + ) + yield + requests.patch( + f"{common.BASE_URL}/experimental-features", + headers={"Authorization": f"Bearer {common.MASTER_KEY}"}, + json={"renderRoute": False}, + timeout=10, + )