From 44df022e323bf574ea9816abdb9693318179e148 Mon Sep 17 00:00:00 2001 From: PedroHMCosme Date: Tue, 14 Jul 2026 10:47:33 -0300 Subject: [PATCH] feat(gateway): slot v2 loader (contract C1) --- ingestor/io/__init__.py | 0 ingestor/io/slots.py | 25 +++++++++++++++++++++++++ tests/unit/io/__init__.py | 0 tests/unit/io/test_slots.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 ingestor/io/__init__.py create mode 100644 ingestor/io/slots.py create mode 100644 tests/unit/io/__init__.py create mode 100644 tests/unit/io/test_slots.py diff --git a/ingestor/io/__init__.py b/ingestor/io/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ingestor/io/slots.py b/ingestor/io/slots.py new file mode 100644 index 0000000..1e720b3 --- /dev/null +++ b/ingestor/io/slots.py @@ -0,0 +1,25 @@ +"""Parse the slot v2 structure written by the Orchestrator to Redis (contract C1).""" + +SERVER_FIELDS = ('server_id', 'url', 'server_uri', 'use_security', 'subscription_period_ms') + + +def _parse_tag(address: str, tag: dict) -> dict: + names = tag.get('names') + if not names: + raise ValueError(f'tag {address!r} has no names') + return { + 'frequency': tag['frequency'], + 'names': names, + 'active_read': tag.get('active_read', {'enabled': False}), + } + + +def _parse_server(server: dict) -> dict: + parsed = {field: server[field] for field in SERVER_FIELDS if field in server} + parsed['tags'] = {addr: _parse_tag(addr, tag) for addr, tag in server['tags'].items()} + return parsed + + +def parse_slot(raw: dict) -> dict: + """Normalize a raw slot v2 dict into `{server_name: {..., tags: {addr: tag}}}`.""" + return {name: _parse_server(server) for name, server in raw.items()} diff --git a/tests/unit/io/__init__.py b/tests/unit/io/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/io/test_slots.py b/tests/unit/io/test_slots.py new file mode 100644 index 0000000..ccbf17b --- /dev/null +++ b/tests/unit/io/test_slots.py @@ -0,0 +1,28 @@ +from ingestor.io.slots import parse_slot + +RAW = {'server1': {'server_id': '1', 'url': 'opc.tcp://h:4840', 'use_security': True, + 'subscription_period_ms': 1000, 'tags': {'ns=2;i=2': {'frequency': '15000', + 'active_read': {'enabled': True, 'cycle_multiple': 3}, + 'names': [{'collection': 'raw_x', 'tag_name': 'Counter', 'model_name': 'm1', 'model_id': '1'}, + {'collection': 'raw_y', 'tag_name': 'Contador', 'model_name': 'm2', 'model_id': '2'}]}}}} + + +def test_parse_slot_multi_name(): + slot = parse_slot(RAW) + tag = slot['server1']['tags']['ns=2;i=2'] + assert len(tag['names']) == 2 + assert tag['active_read']['cycle_multiple'] == 3 + + +def test_parse_slot_defaults_active_read_disabled(): + raw = {'s': {'server_id': '1', 'url': 'u', 'tags': {'ns=2;i=1': { + 'frequency': '1000', 'names': [{'collection': 'c', 'tag_name': 't', 'model_name': 'm', 'model_id': '1'}]}}}} + tag = parse_slot(raw)['s']['tags']['ns=2;i=1'] + assert tag['active_read'] == {'enabled': False} + + +def test_parse_slot_rejects_tag_without_names(): + raw = {'s': {'server_id': '1', 'url': 'u', 'tags': {'ns=2;i=1': {'frequency': '1000'}}}} + import pytest + with pytest.raises((KeyError, ValueError)): + parse_slot(raw)