Source code for pycsamt.iot.protocols.http
"""HTTP(S) telemetry transport using the standard library.
:class:`HTTPTelemetryClient` POSTs each packet as a JSON body to
``endpoint``. It relies only on :mod:`urllib`, so no third-party HTTP
dependency is required. Bearer tokens and extra headers may be supplied
through options or a :class:`~pycsamt.iot.security.SecurityConfig`.
"""
from __future__ import annotations
from typing import Any
from ..core import TelemetryPacket
from .base import (
BaseTelemetryClient,
IoTProtocol,
TelemetryError,
)
__all__ = ["HTTPTelemetryClient"]
[docs]
class HTTPTelemetryClient(BaseTelemetryClient):
"""POST telemetry packets to an HTTP(S) endpoint."""
protocol = IoTProtocol.HTTP
def __init__(
self,
endpoint: str | None = None,
*,
dry_run: bool = False,
timeout: float = 10.0,
token: str | None = None,
headers: dict[str, str] | None = None,
method: str = "POST",
**options: Any,
) -> None:
super().__init__(
endpoint,
dry_run=dry_run,
timeout=timeout,
token=token,
headers=headers,
method=method,
**options,
)
def _connect(self) -> None:
# HTTP is connectionless; validate configuration eagerly instead.
url = self._require_endpoint()
if not str(url).lower().startswith(("http://", "https://")):
raise TelemetryError(
f"HTTP endpoint must start with http:// or https:// (got {url!r})."
)
def _headers(self) -> dict[str, str]:
headers = {"Content-Type": "application/json"}
extra = self.options.get("headers") or {}
headers.update({str(k): str(v) for k, v in extra.items()})
token = self.options.get("token")
if token:
headers.setdefault("Authorization", f"Bearer {token}")
return headers
def _transport_send(self, packet: TelemetryPacket) -> str:
import urllib.error
import urllib.request
url = self._require_endpoint()
request = urllib.request.Request(
url,
data=self._payload_bytes(packet),
headers=self._headers(),
method=str(self.options.get("method", "POST")),
)
timeout = float(self.options.get("timeout", 10.0))
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
status = getattr(response, "status", response.getcode())
except urllib.error.HTTPError as exc:
raise TelemetryError(
f"HTTP {exc.code} from {url}: {exc}"
) from exc
except urllib.error.URLError as exc:
raise TelemetryError(
f"HTTP request to {url} failed: {exc}"
) from exc
if not 200 <= int(status) < 300:
raise TelemetryError(f"HTTP endpoint returned status {status}.")
return f"http {status}"
def _transport_healthcheck(self) -> bool:
# Reachability is only known on a real request; assume config-valid.
return bool(self.endpoint)