I am writing parsers for GNU/Linux commands. For example:
from pydantic import BaseModel
from mmiac.core.system import run_cmd
from mmiac.core.parsing import create_key
class HostnamectlResponse(BaseModel):
static_hostname: str
transient_hostname: str | None
pretty_hostname: str | None
icon_name: str | None
chassis: str | None
deployment: str | None
location: str | None
virtualization: str | None
cpe_os_name: str | None
operating_system: str | None
os_support_end: str | None
os_support_expired: str | None
os_support_remaining: str | None
kernel: str | None
machine_id: str | None
boot_id: str | None
kernel: str | None
hardware_vendor: str | None
hardware_model: str | None
firmware_version: str | None
firmware_date: str | None
def parse() -> dict:
"""Parses output from hostnamectl command.
The only required field is static hostname. Output fields from hostnamectl can be found below:
https://github.com/systemd/systemd/blob/main/src/hostname/hostnamectl.c
"""
content = run_cmd("hostnamectl").split("\n")
data = {}
for line in content:
try:
k, v = line.strip().split(":", 1)
except ValueError:
continue
k = create_key(k)
data[k] = v.strip()
return HostnamectlResponse(**data)
I was thinking about making each command parser a class, but at the moment I feel like a function suffices. I imagine I'll create some sort of dispatch table that dynamically loads modules in that have a parse()
method defined then call the functions using that.
But anyway, my question is related to the response. I'd love to make it such that the response is generic, but unfortunately, ps
output is much different than lspci
(and so on). I'm curious if there is a design pattern that I can reference, or anything really, when the functions you create have a common interface (parse()) but lack a common response.
Beyond the question itself, I appreciate any input here with regards to the design of this. My goal here is to make using the library as intuitive as possible without having to constantly reference docs.
This seems to be a decent place for the Proxy pattern:
CommandResponseProxy
(or whatever you want to call it) from the parser passing it the dictionaryCommandResponseProxy
Proxy is a structural design pattern that provides a surrogate that controls access to another object. The sections below provide more details if needed.
CommandResponseProxy
Definition:
class CommandResponseProxy:
def __init__(self, command_response_dict):
self._response = command_response_dict
def __getitem__(self, key):
# Additinal functionality or access control logic
# For example, you can perform validation, transformations, or anything else
value = self._response[key]
return value
Return from the parser:
# return HostnamectlResponse(**data)
return CommandResponseProxy(data)
Notice the dictionary is passed directly to the proxy instead of unpacking it with **
.
for command_to_parse in available_commands_to_parse:
# where command_to_parse = 'hostnamectl'
response = parse(command_to_parse)
print(response['chassis']) # desktop
The CommandResponseProxy
is a bit useless as written since its essentially just another dictionary but it provides room for other things such as validation, formatting, or anything else you wish to do with the output from within the proxy.
from typing import Type, TypeVar
T = TypeVar("T")
class CommandResponseProxy:
#...
def to(self, cls: Type[T]) -> T:
return cls(**self._response)
The proxy can provide a concrete class representing the command output using the typing
library.
It could be used like:
# where command_to_parse = 'hostnamectl'
# command response proxy
response = parse(command_to_parse)
# use a concrete class
hostnamectlResponse = response.to(HostnamectlResponse)
print(hostnamectlResponse.chassis) # desktop
You may be able to simplify this depending on your use case. Combine these things into the parse function:
def parse(command_to_parse, cls: Type[T]) -> T:
# perform validation on the arguments
# get command output
# generate command output dictionary
# where 'data' is the dictionary of command output:
return cls(**data)
Which could be used like:
hostnamectlResponse = parse('hostnamectl', HostnamectlResponse)
print(hostnamectlResponse.chassis) # desktop