jmri_core.persistence.roster_index_xml

roster.xml — the index file listing every roster entry.

Per contracts/xml/schema/roster.xsd, the top-level element is <roster-config> containing one <roster> (with <locomotive> elements referencing the per-loco files by id) and optional <rosterGroup> definitions.

A locomotive entry in the index repeats the same attributes as the per-loco file's root, plus a groupName attribute when assigned to a group. This module models just the index — Stage 1's roster_xml handles the per-loco files.

  1"""``roster.xml`` — the index file listing every roster entry.
  2
  3Per ``contracts/xml/schema/roster.xsd``, the top-level element is
  4``<roster-config>`` containing one ``<roster>`` (with ``<locomotive>``
  5elements referencing the per-loco files by ``id``) and optional
  6``<rosterGroup>`` definitions.
  7
  8A locomotive entry in the index repeats the same attributes as the per-loco
  9file's root, *plus* a ``groupName`` attribute when assigned to a group.
 10This module models just the index — Stage 1's ``roster_xml`` handles the
 11per-loco files.
 12"""
 13from __future__ import annotations
 14
 15import pathlib
 16
 17from lxml import etree
 18from pydantic import BaseModel, ConfigDict, Field
 19
 20from jmri_core.beans.roster_entry import RosterEntry
 21from jmri_core.persistence.roster_xml import _element_to_entry, _entry_to_element
 22
 23
 24class RosterGroup(BaseModel):
 25    """A named group of roster entries (e.g. "freight power")."""
 26    model_config = ConfigDict(populate_by_name=True)
 27    name: str
 28    members: list[str] = Field(default_factory=list)
 29
 30
 31class RosterIndex(BaseModel):
 32    """Top-level ``<roster-config>`` content."""
 33    model_config = ConfigDict(arbitrary_types_allowed=True)
 34    entries: list[RosterEntry] = Field(default_factory=list)
 35    groups: list[RosterGroup] = Field(default_factory=list)
 36    filter: str = ""
 37
 38
 39def load_roster_index_bytes(data: bytes) -> RosterIndex:
 40    parser = etree.XMLParser(remove_blank_text=False)
 41    root = etree.fromstring(data, parser=parser)
 42    if root.tag != "roster-config":
 43        raise ValueError(f"expected <roster-config>, got <{root.tag}>")
 44
 45    roster_elem = root.find("roster")
 46    entries = (
 47        [_element_to_entry(loco) for loco in roster_elem.findall("locomotive")]
 48        if roster_elem is not None
 49        else []
 50    )
 51    filter_attr = roster_elem.get("filter", "") if roster_elem is not None else ""
 52
 53    groups: list[RosterGroup] = [
 54        RosterGroup(name=(g.text or "").strip())
 55        for rg in root.findall("rosterGroup")
 56        for g in rg.findall("group")
 57    ]
 58
 59    return RosterIndex(entries=entries, groups=groups, filter=filter_attr)
 60
 61
 62def load_roster_index(path: str | pathlib.Path) -> RosterIndex:
 63    return load_roster_index_bytes(pathlib.Path(path).read_bytes())
 64
 65
 66def save_roster_index_bytes(index: RosterIndex) -> bytes:
 67    root = etree.Element("roster-config")
 68    roster = etree.SubElement(root, "roster")
 69    if index.filter:
 70        roster.set("filter", index.filter)
 71    for entry in index.entries:
 72        roster.append(_entry_to_element(entry))
 73
 74    if index.groups:
 75        rg = etree.SubElement(root, "rosterGroup")
 76        for group in index.groups:
 77            g = etree.SubElement(rg, "group")
 78            g.text = group.name
 79
 80    return etree.tostring(
 81        root,
 82        xml_declaration=True,
 83        encoding="utf-8",
 84        pretty_print=True,
 85        standalone=False,
 86    )
 87
 88
 89def save_roster_index(path: str | pathlib.Path, index: RosterIndex) -> None:
 90    pathlib.Path(path).write_bytes(save_roster_index_bytes(index))
 91
 92
 93__all__ = [
 94    "RosterGroup",
 95    "RosterIndex",
 96    "load_roster_index",
 97    "load_roster_index_bytes",
 98    "save_roster_index",
 99    "save_roster_index_bytes",
100]
class RosterGroup(pydantic.main.BaseModel):
25class RosterGroup(BaseModel):
26    """A named group of roster entries (e.g. "freight power")."""
27    model_config = ConfigDict(populate_by_name=True)
28    name: str
29    members: list[str] = Field(default_factory=list)

A named group of roster entries (e.g. "freight power").

name: str = PydanticUndefined
members: list[str] = PydanticUndefined
class RosterIndex(pydantic.main.BaseModel):
32class RosterIndex(BaseModel):
33    """Top-level ``<roster-config>`` content."""
34    model_config = ConfigDict(arbitrary_types_allowed=True)
35    entries: list[RosterEntry] = Field(default_factory=list)
36    groups: list[RosterGroup] = Field(default_factory=list)
37    filter: str = ""

Top-level <roster-config> content.

entries: list[jmri_core.beans.RosterEntry] = PydanticUndefined
groups: list[RosterGroup] = PydanticUndefined
filter: str = ''
def load_roster_index( path: str | pathlib._local.Path) -> RosterIndex:
63def load_roster_index(path: str | pathlib.Path) -> RosterIndex:
64    return load_roster_index_bytes(pathlib.Path(path).read_bytes())
def load_roster_index_bytes(data: bytes) -> RosterIndex:
40def load_roster_index_bytes(data: bytes) -> RosterIndex:
41    parser = etree.XMLParser(remove_blank_text=False)
42    root = etree.fromstring(data, parser=parser)
43    if root.tag != "roster-config":
44        raise ValueError(f"expected <roster-config>, got <{root.tag}>")
45
46    roster_elem = root.find("roster")
47    entries = (
48        [_element_to_entry(loco) for loco in roster_elem.findall("locomotive")]
49        if roster_elem is not None
50        else []
51    )
52    filter_attr = roster_elem.get("filter", "") if roster_elem is not None else ""
53
54    groups: list[RosterGroup] = [
55        RosterGroup(name=(g.text or "").strip())
56        for rg in root.findall("rosterGroup")
57        for g in rg.findall("group")
58    ]
59
60    return RosterIndex(entries=entries, groups=groups, filter=filter_attr)
def save_roster_index( path: str | pathlib._local.Path, index: RosterIndex) -> None:
90def save_roster_index(path: str | pathlib.Path, index: RosterIndex) -> None:
91    pathlib.Path(path).write_bytes(save_roster_index_bytes(index))
def save_roster_index_bytes(index: RosterIndex) -> bytes:
67def save_roster_index_bytes(index: RosterIndex) -> bytes:
68    root = etree.Element("roster-config")
69    roster = etree.SubElement(root, "roster")
70    if index.filter:
71        roster.set("filter", index.filter)
72    for entry in index.entries:
73        roster.append(_entry_to_element(entry))
74
75    if index.groups:
76        rg = etree.SubElement(root, "rosterGroup")
77        for group in index.groups:
78            g = etree.SubElement(rg, "group")
79            g.text = group.name
80
81    return etree.tostring(
82        root,
83        xml_declaration=True,
84        encoding="utf-8",
85        pretty_print=True,
86        standalone=False,
87    )