Mechanical: ruff format, and ruff check --fix with the rule set every NAPALM driver uses (E, F, I, UP, B at line length 100, Python 3.10). Mapping and Iterable come from collections.abc, imports are sorted. What ruff cannot fix comes in the next commit.
138 lines
3.9 KiB
Python
138 lines
3.9 KiB
Python
"""Helpers shared by the parsers. All pure: dict in, plain values out."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
# DMI defaults that board vendors leave in place (Supermicro boards ship most of them).
|
|
PLACEHOLDERS = frozenset(
|
|
{
|
|
"to be filled by o.e.m.",
|
|
"default string",
|
|
"not specified",
|
|
"not applicable",
|
|
"n/a",
|
|
"none",
|
|
"0123456789",
|
|
"system serial number",
|
|
"system product name",
|
|
"system manufacturer",
|
|
}
|
|
)
|
|
|
|
|
|
def text(value: Any) -> str | None:
|
|
"""A stripped string, ``None`` for missing or blank values."""
|
|
if value is None:
|
|
return None
|
|
value = str(value).strip()
|
|
return value or None
|
|
|
|
|
|
def ident(value: Any) -> str | None:
|
|
"""Like ``text``, but DMI placeholder strings count as missing too."""
|
|
value = text(value)
|
|
if value is None or value.lower() in PLACEHOLDERS:
|
|
return None
|
|
return value
|
|
|
|
|
|
def number(value: Any) -> float | None:
|
|
"""A float, ``None`` if the value is missing or not a number."""
|
|
if isinstance(value, bool) or value is None:
|
|
return None
|
|
try:
|
|
return float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def integer(value: Any) -> int | None:
|
|
if isinstance(value, bool) or value is None:
|
|
return None
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def boolean(value: Any) -> bool | None:
|
|
return value if isinstance(value, bool) else None
|
|
|
|
|
|
def odata_id(ref: Any) -> str | None:
|
|
"""The ``@odata.id`` of a reference object, or ``None``."""
|
|
if isinstance(ref, Mapping):
|
|
return text(ref.get("@odata.id"))
|
|
return None
|
|
|
|
|
|
def link(resource: Mapping[str, Any], *keys: str) -> str | None:
|
|
"""Follow ``keys`` into nested objects and return the ``@odata.id`` found there."""
|
|
value: Any = resource
|
|
for key in keys:
|
|
if not isinstance(value, Mapping):
|
|
return None
|
|
value = value.get(key)
|
|
return odata_id(value)
|
|
|
|
|
|
def links(resource: Mapping[str, Any]) -> dict[str, str]:
|
|
"""All top-level properties of ``resource`` that are single references."""
|
|
found = {}
|
|
for key, value in resource.items():
|
|
if key.startswith("@") or key == "Links":
|
|
continue
|
|
path = odata_id(value)
|
|
if path is not None:
|
|
found[key] = path
|
|
return found
|
|
|
|
|
|
def link_list(resource: Mapping[str, Any], *keys: str) -> list[str]:
|
|
"""The ``@odata.id`` of each reference in the array at ``keys``."""
|
|
value: Any = resource
|
|
for key in keys:
|
|
if not isinstance(value, Mapping):
|
|
return []
|
|
value = value.get(key)
|
|
if not isinstance(value, list):
|
|
return []
|
|
return [path for path in (odata_id(ref) for ref in value) if path is not None]
|
|
|
|
|
|
def status(resource: Mapping[str, Any]) -> tuple[str | None, str | None]:
|
|
"""``(Status.State, Status.Health)``."""
|
|
st = resource.get("Status")
|
|
if not isinstance(st, Mapping):
|
|
return None, None
|
|
return text(st.get("State")), text(st.get("Health"))
|
|
|
|
|
|
def present(resource: Mapping[str, Any]) -> bool:
|
|
return status(resource)[0] != "Absent"
|
|
|
|
|
|
def location_label(resource: Mapping[str, Any]) -> str | None:
|
|
"""``Location.PartLocation.ServiceLabel``, the printed label of a slot or bay."""
|
|
loc = resource.get("Location")
|
|
if not isinstance(loc, Mapping):
|
|
return None
|
|
part = loc.get("PartLocation")
|
|
if not isinstance(part, Mapping):
|
|
return None
|
|
return text(part.get("ServiceLabel"))
|
|
|
|
|
|
def unique_name(names: Mapping[str, Any], name: str, suffix: Any) -> str:
|
|
"""``name``, or ``name (suffix)`` if it is already taken."""
|
|
if name not in names:
|
|
return name
|
|
candidate = f"{name} ({suffix})"
|
|
n = 2
|
|
while candidate in names:
|
|
candidate = f"{name} ({suffix}-{n})"
|
|
n += 1
|
|
return candidate
|