Cache transient templates compiles provided via api (#89065)

* Cache transient templates compiles provided via api

partially fixes #89047 (there is more going on here)

* add a bit more coverage just to be sure

* switch method

* Revert "switch method"

This reverts commit 0e9e1c8cbe8753159f4fd6775cdc9cf217d66f0e.

* tweak

* hold hass

* empty for github flakey
This commit is contained in:
J. Nick Koston 2023-03-02 16:31:12 -10:00 committed by Franck Nijhof
parent 322eb4bd83
commit d4c28a1f4a
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
4 changed files with 70 additions and 4 deletions

View File

@ -1,5 +1,6 @@
"""Rest API for Home Assistant.""" """Rest API for Home Assistant."""
import asyncio import asyncio
from functools import lru_cache
from http import HTTPStatus from http import HTTPStatus
import logging import logging
@ -350,6 +351,12 @@ class APIComponentsView(HomeAssistantView):
return self.json(request.app["hass"].config.components) return self.json(request.app["hass"].config.components)
@lru_cache
def _cached_template(template_str: str, hass: ha.HomeAssistant) -> template.Template:
"""Return a cached template."""
return template.Template(template_str, hass)
class APITemplateView(HomeAssistantView): class APITemplateView(HomeAssistantView):
"""View to handle Template requests.""" """View to handle Template requests."""
@ -362,7 +369,7 @@ class APITemplateView(HomeAssistantView):
raise Unauthorized() raise Unauthorized()
try: try:
data = await request.json() data = await request.json()
tpl = template.Template(data["template"], request.app["hass"]) tpl = _cached_template(data["template"], request.app["hass"])
return tpl.async_render(variables=data.get("variables"), parse_result=False) return tpl.async_render(variables=data.get("variables"), parse_result=False)
except (ValueError, TemplateError) as ex: except (ValueError, TemplateError) as ex:
return self.json_message( return self.json_message(

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from contextlib import suppress from contextlib import suppress
from functools import wraps from functools import lru_cache, wraps
from http import HTTPStatus from http import HTTPStatus
import logging import logging
import secrets import secrets
@ -365,6 +365,12 @@ async def webhook_stream_camera(
return webhook_response(resp, registration=config_entry.data) return webhook_response(resp, registration=config_entry.data)
@lru_cache
def _cached_template(template_str: str, hass: HomeAssistant) -> template.Template:
"""Return a cached template."""
return template.Template(template_str, hass)
@WEBHOOK_COMMANDS.register("render_template") @WEBHOOK_COMMANDS.register("render_template")
@validate_schema( @validate_schema(
{ {
@ -381,7 +387,7 @@ async def webhook_render_template(
resp = {} resp = {}
for key, item in data.items(): for key, item in data.items():
try: try:
tpl = template.Template(item[ATTR_TEMPLATE], hass) tpl = _cached_template(item[ATTR_TEMPLATE], hass)
resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES))
except TemplateError as ex: except TemplateError as ex:
resp[key] = {"error": str(ex)} resp[key] = {"error": str(ex)}

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
import datetime as dt import datetime as dt
from functools import lru_cache
import json import json
from typing import Any, cast from typing import Any, cast
@ -424,6 +425,12 @@ def handle_ping(
connection.send_message(pong_message(msg["id"])) connection.send_message(pong_message(msg["id"]))
@lru_cache
def _cached_template(template_str: str, hass: HomeAssistant) -> template.Template:
"""Return a cached template."""
return template.Template(template_str, hass)
@decorators.websocket_command( @decorators.websocket_command(
{ {
vol.Required("type"): "render_template", vol.Required("type"): "render_template",
@ -440,7 +447,7 @@ async def handle_render_template(
) -> None: ) -> None:
"""Handle render_template command.""" """Handle render_template command."""
template_str = msg["template"] template_str = msg["template"]
template_obj = template.Template(template_str, hass) template_obj = _cached_template(template_str, hass)
variables = msg.get("variables") variables = msg.get("variables")
timeout = msg.get("timeout") timeout = msg.get("timeout")
info = None info = None

View File

@ -349,6 +349,52 @@ async def test_api_template(hass: HomeAssistant, mock_api_client: TestClient) ->
assert body == "10" assert body == "10"
hass.states.async_set("sensor.temperature", 20)
resp = await mock_api_client.post(
const.URL_API_TEMPLATE,
json={"template": "{{ states.sensor.temperature.state }}"},
)
body = await resp.text()
assert body == "20"
hass.states.async_remove("sensor.temperature")
resp = await mock_api_client.post(
const.URL_API_TEMPLATE,
json={"template": "{{ states.sensor.temperature.state }}"},
)
body = await resp.text()
assert body == ""
async def test_api_template_cached(
hass: HomeAssistant, mock_api_client: TestClient
) -> None:
"""Test the template API uses the cache."""
hass.states.async_set("sensor.temperature", 30)
resp = await mock_api_client.post(
const.URL_API_TEMPLATE,
json={"template": "{{ states.sensor.temperature.state }}"},
)
body = await resp.text()
assert body == "30"
hass.states.async_set("sensor.temperature", 40)
resp = await mock_api_client.post(
const.URL_API_TEMPLATE,
json={"template": "{{ states.sensor.temperature.state }}"},
)
body = await resp.text()
assert body == "40"
async def test_api_template_error( async def test_api_template_error(
hass: HomeAssistant, mock_api_client: TestClient hass: HomeAssistant, mock_api_client: TestClient