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").
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.
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)
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 )