Source code for pycsamt.iot.protocols.file

"""File-backed telemetry transport (newline-delimited JSON).

A :class:`FileTelemetryClient` appends each packet as one JSON object per
line (JSONL) to ``endpoint``. It doubles as a source: :meth:`receive`
walks previously written lines. This transport is dependency-free and is
the natural default for offline logging, replay, and testing.
"""

from __future__ import annotations

import json
import os
from typing import Any

from ..core import TelemetryPacket
from .base import (
    BaseTelemetryClient,
    IoTProtocol,
    TelemetryError,
)

__all__ = ["FileTelemetryClient"]


[docs] class FileTelemetryClient(BaseTelemetryClient): """Append/replay telemetry packets to a JSON-lines file.""" protocol = IoTProtocol.FILE def __init__( self, endpoint: str | None = None, *, dry_run: bool = False, append: bool = True, **options: Any, ) -> None: super().__init__(endpoint, dry_run=dry_run, append=append, **options) self._read_cursor = 0 self._read_buffer: list[dict[str, Any]] = [] def _connect(self) -> None: path = self._require_endpoint() parent = os.path.dirname(os.path.abspath(path)) if parent: os.makedirs(parent, exist_ok=True) mode = "a" if self.options.get("append", True) else "w" if mode == "w" and os.path.exists(path): open(path, "w", encoding="utf-8").close() def _disconnect(self) -> None: self._read_buffer = [] self._read_cursor = 0 def _transport_send(self, packet: TelemetryPacket) -> str: path = self._require_endpoint() line = json.dumps(packet.as_dict(), default=str) with open(path, "a", encoding="utf-8") as handle: handle.write(line + "\n") return f"appended to {path}" def _load_buffer(self) -> None: path = self._require_endpoint() if not os.path.exists(path): self._read_buffer = [] return with open(path, encoding="utf-8") as handle: rows: list[dict[str, Any]] = [] for raw in handle: raw = raw.strip() if not raw: continue try: rows.append(json.loads(raw)) except json.JSONDecodeError as exc: raise TelemetryError( f"malformed telemetry line in {path}: {exc}" ) from exc self._read_buffer = rows def _transport_receive( self, *, timeout: float | None = None ) -> dict[str, Any] | None: if self._read_cursor == 0 or not self._read_buffer: self._load_buffer() if self._read_cursor >= len(self._read_buffer): return None row = self._read_buffer[self._read_cursor] self._read_cursor += 1 return row
[docs] def read_all(self) -> list[dict[str, Any]]: """Return every packet dictionary written to the file.""" self._load_buffer() return list(self._read_buffer)
def _transport_healthcheck(self) -> bool: path = self._require_endpoint() parent = os.path.dirname(os.path.abspath(path)) or "." return os.access(parent, os.W_OK)