"""NATS input adapter for OPC write commands (contract C2). Thin adapter: decode the message JSON and hand the dict to WriteEngine, which is already raise-safe against malformed commands. No metrics of its own — the engine records write metrics; a malformed-JSON drop is logged here. No reconnect logic — nats-py handles reconnection internally. """ import json import nats from nats.aio.client import Client from nats.aio.subscription import Subscription from sientia_do.observability.logger import Logger from ingestor.engines.write import WriteEngine class NatsWriteConsumer: def __init__( self, nats_url: str, write_engine: WriteEngine, logger: Logger, subject: str = 'opc.write', queue: str = 'gateway-writers', ): self.nats_url = nats_url self.write_engine = write_engine self.logger = logger self.subject = subject self.queue = queue self.nc: Client | None = None self.sub: Subscription | None = None async def start(self) -> None: self.nc = await nats.connect(servers=self.nats_url) self.sub = await self.nc.subscribe(self.subject, queue=self.queue, cb=self._handle) self.logger.info(f'NatsWriteConsumer started: subject={self.subject} queue={self.queue}') async def _handle(self, msg) -> None: try: payload = json.loads(msg.data) except ValueError as e: # includes JSONDecodeError and UnicodeDecodeError preview = msg.data[:200] self.logger.error(f'NatsWriteConsumer dropped malformed JSON ({e}): {preview!r}') return await self.write_engine.write(payload) async def stop(self) -> None: if self.nc is None: return if self.sub is not None: await self.sub.unsubscribe() await self.nc.drain()