Writing Parsers¶
This guide covers how to write a new parser for Muninn, following the project's conventions and design principles.
Parser Structure¶
Every parser consists of:
- A
TypedDictdefining the output schema - A parser class extending
BaseParser - A
@register()decorator binding it to an OS and command - A
tagsclass variable categorizing the parser - A
parse()classmethod that transforms raw CLI output into structured data
Minimal Example¶
"""Parser for 'show privilege' command on IOS-XE."""
import re
from typing import ClassVar, TypedDict
from muninn.os import OS
from muninn.parser import BaseParser
from muninn.registry import register
from muninn.tags import ParserTag
class ShowPrivilegeResult(TypedDict):
"""Schema for 'show privilege' parsed output."""
privilege_level: int
@register(OS.CISCO_IOSXE, "show privilege")
class ShowPrivilegeParser(BaseParser[ShowPrivilegeResult]):
"""Parser for 'show privilege' command."""
tags: ClassVar[frozenset[ParserTag]] = frozenset({ParserTag.SYSTEM})
_PRIVILEGE_PATTERN = re.compile(r"Current privilege level is (?P<level>\d+)")
@classmethod
def parse(cls, output: str) -> ShowPrivilegeResult:
for line in output.splitlines():
match = cls._PRIVILEGE_PATTERN.search(line.strip())
if match:
return ShowPrivilegeResult(
privilege_level=int(match.group("level")),
)
msg = "No privilege level found in output"
raise ValueError(msg)
Key Conventions¶
One Parser Per File¶
Each parser lives in its own file under src/muninn/parsers/<os>/. The filename mirrors the command with underscores replacing spaces:
show clock->show_clock.pyshow ip ospf neighbor->show_ip_ospf_neighbor.py
TypedDict Output Schemas¶
Parsers return plain dictionaries, but define a TypedDict for the return type. This provides IDE autocompletion without introducing runtime overhead.
class ShowClockResult(TypedDict):
time: str
timezone: str
day_of_week: str
month: str
day: str
year: str
Tags¶
Built-in parsers must define a non-empty tags set using one or more values from the ParserTag enum. Tags categorize parsers for browsing and filtering in the parser library.
The full set of available tags is defined in muninn.tags.ParserTag. If you feel a tag is missing for your parser's feature area, you're welcome to add a new ParserTag value as part of your contribution.
Dict-of-Dicts Output Pattern¶
When output contains multiple entries keyed by a natural identifier (neighbor ID, interface name, etc.), use nested dictionaries rather than lists:
# Preferred
{
"10.1.1.1": {"state": "FULL", "interface": "Ethernet1/1"},
"10.1.1.2": {"state": "FULL", "interface": "Ethernet1/2"},
}
# Avoid
[
{"neighbor_id": "10.1.1.1", "state": "FULL", "interface": "Ethernet1/1"},
{"neighbor_id": "10.1.1.2", "state": "FULL", "interface": "Ethernet1/2"},
]
Use nested dictionaries for hierarchical data rather than composite string keys:
# Preferred
{
"GigabitEthernet0/1": {
"groups": {
"5": {"state": "Active", "priority": 110}
}
}
}
# Avoid - composite key encodes multiple dimensions
{
"GigabitEthernet0/1|5": {"state": "Active", "priority": 110}
}
Multi-OS Parsers¶
A single parser class can be registered for multiple operating systems by stacking decorators:
@register(OS.CISCO_NXOS, "show clock")
@register(OS.CISCO_IOS, "show clock")
@register(OS.CISCO_IOSXE, "show clock")
class ShowClockParser(BaseParser[ShowClockResult]):
...
When a parser covers multiple operating systems, place the file under the OS directory you consider the primary target. There's no need to duplicate the file across OS directories. If a different OS later needs divergent parsing logic, that's when you'd create a separate parser file under the other OS directory.
File Placement¶
Each parser lives in its own file under src/muninn/parsers/<os>/. Parsers are auto-discovered at import time - adding a new file is all that's needed, no __init__.py registration required.
A typical single-OS parser:
For multi-OS parsers, place the file under the primary OS directory. Only create a separate file under another OS directory if it needs different parsing logic:
src/muninn/parsers/
├── iosxe/
│ ├── __init__.py
│ └── show_clock.py # registers for IOS-XE, IOS, and NX-OS
└── nxos/
├── __init__.py
└── show_clock.py # only needed if NX-OS requires different parsing logic
Next Steps¶
- Parser Registration - Literal vs regex command registration
- Testing Parsers - How to write test fixtures