2020-12-09 16:18:57 +00:00
|
|
|
"""Helper for httpx."""
|
2024-03-08 15:36:11 +00:00
|
|
|
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-05-24 08:24:09 +00:00
|
|
|
from collections.abc import Callable, Coroutine
|
2020-12-09 16:18:57 +00:00
|
|
|
import sys
|
2023-07-22 22:03:44 +00:00
|
|
|
from typing import Any, Self
|
2020-12-09 16:18:57 +00:00
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
2022-09-29 01:24:04 +00:00
|
|
|
from homeassistant.const import APPLICATION_NAME, EVENT_HOMEASSISTANT_CLOSE, __version__
|
2021-03-27 11:55:24 +00:00
|
|
|
from homeassistant.core import Event, HomeAssistant, callback
|
2020-12-09 16:18:57 +00:00
|
|
|
from homeassistant.loader import bind_hass
|
2024-05-07 16:25:16 +00:00
|
|
|
from homeassistant.util.hass_dict import HassKey
|
2023-04-15 19:32:30 +00:00
|
|
|
from homeassistant.util.ssl import (
|
|
|
|
SSLCipherList,
|
|
|
|
client_context,
|
|
|
|
create_no_verify_ssl_context,
|
|
|
|
)
|
2020-12-09 16:18:57 +00:00
|
|
|
|
2021-12-23 19:14:47 +00:00
|
|
|
from .frame import warn_use
|
|
|
|
|
2023-07-03 18:21:59 +00:00
|
|
|
# We have a lot of integrations that poll every 10-30 seconds
|
|
|
|
# and we want to keep the connection open for a while so we
|
|
|
|
# don't have to reconnect every time so we use 15s to match aiohttp.
|
|
|
|
KEEP_ALIVE_TIMEOUT = 15
|
2024-05-07 16:25:16 +00:00
|
|
|
DATA_ASYNC_CLIENT: HassKey[httpx.AsyncClient] = HassKey("httpx_async_client")
|
|
|
|
DATA_ASYNC_CLIENT_NOVERIFY: HassKey[httpx.AsyncClient] = HassKey(
|
|
|
|
"httpx_async_client_noverify"
|
|
|
|
)
|
2023-07-03 18:21:59 +00:00
|
|
|
DEFAULT_LIMITS = limits = httpx.Limits(keepalive_expiry=KEEP_ALIVE_TIMEOUT)
|
2024-03-08 17:44:42 +00:00
|
|
|
SERVER_SOFTWARE = (
|
|
|
|
f"{APPLICATION_NAME}/{__version__} "
|
|
|
|
f"httpx/{httpx.__version__} Python/{sys.version_info[0]}.{sys.version_info[1]}"
|
2020-12-09 16:18:57 +00:00
|
|
|
)
|
|
|
|
USER_AGENT = "User-Agent"
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
@bind_hass
|
2021-03-27 11:55:24 +00:00
|
|
|
def get_async_client(hass: HomeAssistant, verify_ssl: bool = True) -> httpx.AsyncClient:
|
2020-12-09 16:18:57 +00:00
|
|
|
"""Return default httpx AsyncClient.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
key = DATA_ASYNC_CLIENT if verify_ssl else DATA_ASYNC_CLIENT_NOVERIFY
|
|
|
|
|
2024-05-07 16:25:16 +00:00
|
|
|
if (client := hass.data.get(key)) is None:
|
2020-12-09 16:18:57 +00:00
|
|
|
client = hass.data[key] = create_async_httpx_client(hass, verify_ssl)
|
|
|
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
2021-03-27 08:02:01 +00:00
|
|
|
class HassHttpXAsyncClient(httpx.AsyncClient):
|
|
|
|
"""httpx AsyncClient that suppresses context management."""
|
|
|
|
|
2023-02-07 04:30:22 +00:00
|
|
|
async def __aenter__(self) -> Self:
|
2021-03-27 08:02:01 +00:00
|
|
|
"""Prevent an integration from reopen of the client via context manager."""
|
|
|
|
return self
|
|
|
|
|
2024-04-07 21:30:50 +00:00
|
|
|
async def __aexit__(self, *args: object) -> None:
|
2021-03-27 08:02:01 +00:00
|
|
|
"""Prevent an integration from close of the client via context manager."""
|
|
|
|
|
|
|
|
|
2020-12-09 16:18:57 +00:00
|
|
|
@callback
|
|
|
|
def create_async_httpx_client(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant,
|
2020-12-09 16:18:57 +00:00
|
|
|
verify_ssl: bool = True,
|
|
|
|
auto_cleanup: bool = True,
|
2023-04-15 19:32:30 +00:00
|
|
|
ssl_cipher_list: SSLCipherList = SSLCipherList.PYTHON_DEFAULT,
|
2020-12-09 16:18:57 +00:00
|
|
|
**kwargs: Any,
|
|
|
|
) -> httpx.AsyncClient:
|
|
|
|
"""Create a new httpx.AsyncClient with kwargs, i.e. for cookies.
|
|
|
|
|
|
|
|
If auto_cleanup is False, the client will be
|
|
|
|
automatically closed on homeassistant_stop.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2023-03-24 07:40:47 +00:00
|
|
|
ssl_context = (
|
2023-04-15 19:32:30 +00:00
|
|
|
client_context(ssl_cipher_list)
|
|
|
|
if verify_ssl
|
|
|
|
else create_no_verify_ssl_context(ssl_cipher_list)
|
2023-03-24 07:40:47 +00:00
|
|
|
)
|
2021-03-27 08:02:01 +00:00
|
|
|
client = HassHttpXAsyncClient(
|
2023-03-24 07:40:47 +00:00
|
|
|
verify=ssl_context,
|
2020-12-09 16:18:57 +00:00
|
|
|
headers={USER_AGENT: SERVER_SOFTWARE},
|
2023-07-03 18:21:59 +00:00
|
|
|
limits=DEFAULT_LIMITS,
|
2020-12-09 16:18:57 +00:00
|
|
|
**kwargs,
|
|
|
|
)
|
|
|
|
|
|
|
|
original_aclose = client.aclose
|
|
|
|
|
2023-03-08 21:57:54 +00:00
|
|
|
client.aclose = warn_use( # type: ignore[method-assign]
|
2020-12-09 16:18:57 +00:00
|
|
|
client.aclose, "closes the Home Assistant httpx client"
|
|
|
|
)
|
|
|
|
|
|
|
|
if auto_cleanup:
|
|
|
|
_async_register_async_client_shutdown(hass, client, original_aclose)
|
|
|
|
|
|
|
|
return client
|
|
|
|
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def _async_register_async_client_shutdown(
|
2021-03-27 11:55:24 +00:00
|
|
|
hass: HomeAssistant,
|
2020-12-09 16:18:57 +00:00
|
|
|
client: httpx.AsyncClient,
|
2024-05-24 08:24:09 +00:00
|
|
|
original_aclose: Callable[[], Coroutine[Any, Any, None]],
|
2020-12-09 16:18:57 +00:00
|
|
|
) -> None:
|
|
|
|
"""Register httpx AsyncClient aclose on Home Assistant shutdown.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
|
|
|
|
async def _async_close_client(event: Event) -> None:
|
|
|
|
"""Close httpx client."""
|
|
|
|
await original_aclose()
|
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_client)
|