Refactor BaseDevice and Interfaces models for improved validation and structure

- Introduced `_declared_sections` in `BaseDevice` to track declared template groups.
- Enhanced `_validate_contract` method to conditionally validate VLANs based on declared sections.
- Updated docstrings in `BaseDevice` and `Interfaces` models for clarity on expected structures.
- Refactored `Interfaces` and `Vlans` models to improve field definitions and aliases.
- Commented out unused `vlans` method in `Keenetic` model for future reference.
This commit is contained in:
IluaAir
2026-02-22 15:52:45 +03:00
parent 3635a07b27
commit 753268a381
3 changed files with 55 additions and 34 deletions

View File

@@ -12,7 +12,9 @@ class BaseDevice(ABC):
def __init__(self, config: str): def __init__(self, config: str):
self.config: str = config self.config: str = config
self._loaded_template = self._load_template() self._loaded_template = self._load_template()
self._declared_sections = None
self._validate_template_groups() self._validate_template_groups()
self.raw: dict = self._run_ttp() self.raw: dict = self._run_ttp()
@@ -20,15 +22,17 @@ class BaseDevice(ABC):
@abstractmethod @abstractmethod
def template(self) -> str: def template(self) -> str:
""" """
Returns: Expected structure:
Название файла с парсером ttp Название файла с парсером ttp
Returns:
None
""" """
def vlans(self) -> list[dict]: def vlans(self) -> list[dict]:
""" """
Parse VLAN configuration from self.raw['vlans']. Parse VLAN configuration from self.raw['vlans'].
Expected raw structure: Expected structure:
[{"id": 10, "description": "MGMT"}, {"id": 15, "name": "SSH"}, ...] [{"id": 10, "description": "MGMT"}, {"id": 15, "name": "SSH"}, ...]
Returns: Returns:
@@ -64,16 +68,20 @@ class BaseDevice(ABC):
""" """
return self.raw.get("system", None) return self.raw.get("system", None)
def _validate_contract(self): def _validate_contract(self) -> dict:
optional_vlans = self.vlans() system_data = self.system()
if optional_vlans: interfaces_data = self.interfaces() or []
optional_vlans = [Vlans(**item) for item in optional_vlans] result = {
return { "system": System(**system_data),
"system": System(**self.system()), "interfaces": [Interfaces(**item) for item in interfaces_data],
"interfaces": [Interfaces(**items) for items in self.interfaces()], "vlans": [],
"vlans": optional_vlans,
} }
if "vlans" in self._declared_sections:
vlans_data = self.vlans() or []
result["vlans"] = [Vlans(**item) for item in vlans_data]
return result
def _load_template(self): def _load_template(self):
"""Подгрузка темплейтов из папки models/templates""" """Подгрузка темплейтов из папки models/templates"""
path = Path(__file__).parent / "models" / "templates" / self.template path = Path(__file__).parent / "models" / "templates" / self.template
@@ -82,20 +90,20 @@ class BaseDevice(ABC):
return path.read_text(encoding="utf-8") return path.read_text(encoding="utf-8")
def _validate_template_groups(self) -> None: def _validate_template_groups(self) -> None:
"""Проверка что TTP темлпейт имеет декларированные группы для всех требуемых и опциональных секций""" """Проверяем только обязательные группы в template."""
try: try:
root = ET.fromstring(self._loaded_template) root = ET.fromstring(self._loaded_template)
except ET.ParseError: except ET.ParseError:
root = ET.fromstring(f"<template>{self._loaded_template}</template>") root = ET.fromstring(f"<template>{self._loaded_template}</template>")
declared = {g.get("name") for g in root.iter("group") if g.get("name")} declared = {g.get("name") for g in root.iter("group") if g.get("name")}
expected = self._REQUIRED_SECTIONS | self._OPTIONAL_SECTIONS self._declared_sections = declared
missing = expected - declared
if missing: missing_required = self._REQUIRED_SECTIONS - declared
if missing_required:
raise ValueError( raise ValueError(
f"{self.__class__.__name__}: template '{self.template}' " f"{self.__class__.__name__}: template '{self.template}' "
f"missing group declarations: {sorted(missing)}. " f"missing required groups: {sorted(missing_required)}. "
f"Declared groups: {sorted(declared)}" f"Declared groups: {sorted(declared)}"
) )
@@ -108,8 +116,8 @@ class BaseDevice(ABC):
if missing: if missing:
raise ValueError( raise ValueError(
f"{self.__class__.__name__}: TTP template '{self.template}' " f"{self.__class__.__name__}: TTP template '{self.template}' "
f"did not produce required sections: {sorted(missing)}. " f"did not produce required groups: {sorted(missing)}. "
f"Got: {(raw.keys())}" f"Return only: {(raw.keys())}"
) )
return raw return raw

View File

@@ -2,27 +2,41 @@ from ipaddress import IPv4Address
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
class Interfaces(BaseModel): class Base(BaseModel):
name: str model_config = ConfigDict(populate_by_name=True)
ip_address: IPv4Address | None = None
mask: int | None = None
description: str | None = None
class System(BaseModel): class System(BaseModel):
"""
Requred
"""
model: str model: str
serial_number: str serial_number: str
version: str version: str
class Vlans(BaseModel): class Interfaces(Base):
model_config = ConfigDict(populate_by_name=True) """
Requred
"""
vlan_id: int name: str = Field(alias="interface")
ip_address: IPv4Address | None = None
mask: int | None = None
description: str | None = None
class Vlans(Base):
"""
Optional
"""
vlan_id: int = Field(alias="id")
name: str | None = Field(default=None, alias="description") name: str | None = Field(default=None, alias="description")
class Device(BaseModel): class Device(BaseModel):
system: System system: System
interfaces: list[Interfaces] = [] interfaces: list[Interfaces]
vlans: list[Vlans] = [] vlans: list[Vlans] = []

View File

@@ -1,7 +1,6 @@
from ipaddress import ip_interface from ipaddress import ip_interface
from oxi.interfaces import register_parser from oxi.interfaces import register_parser
from oxi.interfaces.base import BaseDevice from oxi.interfaces.base import BaseDevice
from oxi.interfaces.contract import Interfaces, Vlans
@register_parser(["NDMS", "keenetic", "KeeneticOS"]) @register_parser(["NDMS", "keenetic", "KeeneticOS"])
@@ -34,13 +33,13 @@ class Keenetic(BaseDevice):
item["description"] = decoded item["description"] = decoded
return interfaces return interfaces
def vlans(self): # def vlans(self):
vlans = self.raw["vlans"] # vlans = self.raw["vlans"]
for item in vlans: # for item in vlans:
if item.get("description"): # if item.get("description"):
decoded = self._decode_utf(item.get("description", "")) # decoded = self._decode_utf(item.get("description", ""))
item["description"] = decoded # item["description"] = decoded
return vlans # return vlans
if __name__ == "__main__": if __name__ == "__main__":