|
| 1 | +"""Shared factory for direct-Supervisor httpx clients. |
| 2 | +
|
| 3 | +Three call sites in the codebase talk directly to the Home Assistant |
| 4 | +Supervisor REST API at ``http://supervisor`` rather than through |
| 5 | +``HomeAssistantClient.httpx_client`` (which is bound to HA Core, not the |
| 6 | +Supervisor — different base URL, different token, different role gate): |
| 7 | +
|
| 8 | +- :meth:`ha_mcp.client.rest_client.HomeAssistantClient._supervisor_logs_get` |
| 9 | + — fetches addon and system-service logs |
| 10 | +- :func:`ha_mcp.tools.tools_bug_report._fetch_addon_logs` — bundles ha-mcp's |
| 11 | + own addon logs into a bug-report payload |
| 12 | +- :func:`ha_mcp.settings_ui._restart_addon` — POSTs ``/addons/self/restart`` |
| 13 | + from the settings UI |
| 14 | +
|
| 15 | +All three share the same boilerplate (base URL, ``Authorization: Bearer |
| 16 | +${SUPERVISOR_TOKEN}`` header), so this module supplies a single factory and |
| 17 | +keeps the three sites consistent. |
| 18 | +""" |
| 19 | + |
| 20 | +from __future__ import annotations |
| 21 | + |
| 22 | +import os |
| 23 | +import ssl |
| 24 | + |
| 25 | +import httpx |
| 26 | + |
| 27 | +from .._version import get_supervisor_base_url |
| 28 | + |
| 29 | +__all__ = ["make_supervisor_httpx_client"] |
| 30 | + |
| 31 | + |
| 32 | +def make_supervisor_httpx_client( |
| 33 | + *, |
| 34 | + timeout: float | httpx.Timeout, |
| 35 | + verify: bool | str | ssl.SSLContext, |
| 36 | +) -> httpx.AsyncClient: |
| 37 | + """Construct an ``httpx.AsyncClient`` pre-configured for the Supervisor REST API. |
| 38 | +
|
| 39 | + Args: |
| 40 | + timeout: Per-request timeout. Accepts either a plain ``float`` |
| 41 | + (seconds, applied to all phases) or a full :class:`httpx.Timeout` |
| 42 | + for finer-grained control. |
| 43 | + verify: TLS verify policy. A no-op for the default |
| 44 | + ``http://supervisor`` base URL (plain HTTP — no TLS to verify), |
| 45 | + but kept as a parameter because :func:`get_supervisor_base_url` |
| 46 | + honours ``SUPERVISOR_BASE_URL`` env-var overrides that may be |
| 47 | + HTTPS in non-add-on test rigs. The full httpx ``verify`` surface |
| 48 | + (``bool``, CA-bundle path, or :class:`ssl.SSLContext`) is |
| 49 | + accepted and forwarded verbatim. |
| 50 | +
|
| 51 | + Returns: |
| 52 | + A new :class:`httpx.AsyncClient` bound to the Supervisor base URL |
| 53 | + with ``Authorization: Bearer ${SUPERVISOR_TOKEN}`` preset. Callers |
| 54 | + pass relative paths (``/addons/self/logs``) to ``client.get/post``; |
| 55 | + ``base_url`` joins them onto the Supervisor host. |
| 56 | +
|
| 57 | + Raises: |
| 58 | + RuntimeError: ``SUPERVISOR_TOKEN`` is unset or empty in the |
| 59 | + environment. Each call site has its own absent-token policy |
| 60 | + (a rich :class:`HomeAssistantAuthError`, a silent ``""`` |
| 61 | + return, or a 400 ``JSONResponse``) that does not share a |
| 62 | + common shape, so the factory cannot translate. Detecting the |
| 63 | + absence at construction time prevents a malformed |
| 64 | + ``Authorization: Bearer `` header from being read as a token |
| 65 | + rejection by Supervisor, which would mask the missing-env-var |
| 66 | + root cause. |
| 67 | +
|
| 68 | + Note: |
| 69 | + ``SUPERVISOR_TOKEN`` is read from env at construction time and |
| 70 | + baked into the constructed client's ``Authorization`` header. |
| 71 | + Reusing a single client across token rotations would not pick up |
| 72 | + the new value — short-lived ``async with`` callers are unaffected, |
| 73 | + but a future long-lived caller would need to discard and re-create. |
| 74 | + """ |
| 75 | + token = os.environ.get("SUPERVISOR_TOKEN", "") |
| 76 | + if not token: |
| 77 | + raise RuntimeError( |
| 78 | + "SUPERVISOR_TOKEN is not set; " |
| 79 | + "make_supervisor_httpx_client cannot construct an " |
| 80 | + "authenticated client. Callers must verify the token is " |
| 81 | + "present before invoking the factory." |
| 82 | + ) |
| 83 | + return httpx.AsyncClient( |
| 84 | + base_url=get_supervisor_base_url(), |
| 85 | + timeout=timeout, |
| 86 | + verify=verify, |
| 87 | + headers={"Authorization": f"Bearer {token}"}, |
| 88 | + ) |
0 commit comments