feat(python): add galaxy-browse CLI subcommand (§4.6)
This commit is contained in:
@@ -24,7 +24,7 @@ from zb_mom_ww_mxgateway.errors import MxGatewayError
|
||||
from zb_mom_ww_mxgateway.galaxy import GalaxyRepositoryClient
|
||||
from zb_mom_ww_mxgateway.generated import galaxy_repository_pb2 as galaxy_pb
|
||||
from zb_mom_ww_mxgateway.generated import mxaccess_gateway_pb2 as pb
|
||||
from zb_mom_ww_mxgateway.options import ClientOptions
|
||||
from zb_mom_ww_mxgateway.options import BrowseChildrenOptions, ClientOptions
|
||||
from zb_mom_ww_mxgateway.values import MxValueInput, to_mx_value
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -553,6 +553,63 @@ def galaxy_discover(**kwargs: Any) -> None:
|
||||
)
|
||||
|
||||
|
||||
@main.command("galaxy-browse")
|
||||
@gateway_options
|
||||
@click.option(
|
||||
"--depth",
|
||||
default=0,
|
||||
type=int,
|
||||
show_default=True,
|
||||
help="Eagerly expand the root nodes this many further levels before printing.",
|
||||
)
|
||||
@click.option(
|
||||
"--category-id",
|
||||
"category_ids",
|
||||
multiple=True,
|
||||
type=int,
|
||||
help="Restrict to objects whose category_id matches one of these ids (repeatable).",
|
||||
)
|
||||
@click.option(
|
||||
"--template-chain-contains",
|
||||
"template_chain_contains",
|
||||
multiple=True,
|
||||
help="Restrict to objects whose template chain contains this entry (repeatable).",
|
||||
)
|
||||
@click.option(
|
||||
"--tag-name-glob",
|
||||
"tag_name_glob",
|
||||
default=None,
|
||||
help="Restrict to objects whose tag name matches this glob.",
|
||||
)
|
||||
@click.option(
|
||||
"--include-attributes",
|
||||
"include_attributes",
|
||||
is_flag=True,
|
||||
help="Include each object's attribute metadata in the browse results.",
|
||||
)
|
||||
@click.option(
|
||||
"--alarm-bearing-only",
|
||||
"alarm_bearing_only",
|
||||
is_flag=True,
|
||||
help="Only return objects that own at least one alarm-bearing attribute.",
|
||||
)
|
||||
@click.option(
|
||||
"--historized-only",
|
||||
"historized_only",
|
||||
is_flag=True,
|
||||
help="Only return objects that own at least one historized attribute.",
|
||||
)
|
||||
@click.option("--json", "output_json", is_flag=True, help="Emit JSON output.")
|
||||
def galaxy_browse(**kwargs: Any) -> None:
|
||||
"""Browse the deployed Galaxy object hierarchy as a lazy-expanded tree."""
|
||||
|
||||
_run(
|
||||
_galaxy_browse(**kwargs),
|
||||
output_json=kwargs["output_json"],
|
||||
secrets=_secrets(kwargs),
|
||||
)
|
||||
|
||||
|
||||
@main.command("galaxy-watch")
|
||||
@gateway_options
|
||||
@click.option(
|
||||
@@ -1027,6 +1084,79 @@ async def _galaxy_discover(**kwargs: Any) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
async def _galaxy_browse(**kwargs: Any) -> dict[str, Any]:
|
||||
depth = int(kwargs["depth"])
|
||||
if depth < 0:
|
||||
raise click.BadParameter("must be non-negative", param_hint="--depth")
|
||||
options = BrowseChildrenOptions(
|
||||
category_ids=tuple(kwargs.get("category_ids") or ()),
|
||||
template_chain_contains=tuple(kwargs.get("template_chain_contains") or ()),
|
||||
tag_name_glob=kwargs.get("tag_name_glob"),
|
||||
include_attributes=True if kwargs.get("include_attributes") else None,
|
||||
alarm_bearing_only=bool(kwargs.get("alarm_bearing_only")),
|
||||
historized_only=bool(kwargs.get("historized_only")),
|
||||
)
|
||||
async with await _connect_galaxy(kwargs) as galaxy:
|
||||
roots = await galaxy.browse(options)
|
||||
for root in roots:
|
||||
await _expand_to_depth(root, depth)
|
||||
return {
|
||||
"command": "galaxy-browse",
|
||||
"nodes": [_browse_node_dict(node) for node in roots],
|
||||
"_text": _render_browse_tree(roots),
|
||||
}
|
||||
|
||||
|
||||
async def _expand_to_depth(node: Any, depth: int) -> None:
|
||||
"""Recursively expand a LazyBrowseNode up to ``depth`` further levels.
|
||||
|
||||
``depth == 0`` leaves the node unexpanded so only the requested level is
|
||||
printed; each level beyond fetches and recurses into the loaded children.
|
||||
"""
|
||||
|
||||
if depth <= 0:
|
||||
return
|
||||
if node.has_children_hint:
|
||||
await node.expand()
|
||||
for child in node.children:
|
||||
await _expand_to_depth(child, depth - 1)
|
||||
|
||||
|
||||
def _browse_node_dict(node: Any) -> dict[str, Any]:
|
||||
"""Render one LazyBrowseNode (and any already-expanded descendants).
|
||||
|
||||
Mirrors the ``galaxy-discover`` object shape with an added
|
||||
``hasChildrenHint`` flag and a nested ``children`` array, matching the
|
||||
cross-client browse JSON surface.
|
||||
"""
|
||||
|
||||
payload = _message_dict(node.object)
|
||||
payload["hasChildrenHint"] = bool(node.has_children_hint)
|
||||
payload["children"] = (
|
||||
[_browse_node_dict(child) for child in node.children] if node.is_expanded else []
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _render_browse_tree(roots: list[Any]) -> str:
|
||||
"""Render the lazy-browse roots as a node count plus an indented tree."""
|
||||
|
||||
lines: list[str] = [str(len(roots))]
|
||||
for root in roots:
|
||||
_append_browse_node_lines(root, 0, lines)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _append_browse_node_lines(node: Any, indent: int, lines: list[str]) -> None:
|
||||
obj = node.object
|
||||
marker = "+" if node.has_children_hint else "-"
|
||||
pad = " " * indent
|
||||
lines.append(f"{pad}{marker} {obj.tag_name} {obj.browse_name} (gobject {obj.gobject_id})")
|
||||
if node.is_expanded:
|
||||
for child in node.children:
|
||||
_append_browse_node_lines(child, indent + 2, lines)
|
||||
|
||||
|
||||
async def _galaxy_watch(**kwargs: Any) -> dict[str, Any]:
|
||||
last_seen = kwargs.get("last_seen_deploy_time")
|
||||
last_seen_dt = _parse_datetime(last_seen) if last_seen else None
|
||||
@@ -1131,11 +1261,17 @@ def _emit(
|
||||
output_json: bool,
|
||||
text: str | None = None,
|
||||
) -> None:
|
||||
# A payload may carry a pre-rendered text representation under the private
|
||||
# "_text" key (used by commands like galaxy-browse whose text output is a
|
||||
# custom indented tree rather than the default JSON dump). Strip it so it
|
||||
# never leaks into the JSON branch.
|
||||
rendered_text = payload.pop("_text", None) if isinstance(payload, dict) else None
|
||||
|
||||
if output_json:
|
||||
click.echo(json.dumps(payload, sort_keys=True))
|
||||
return
|
||||
|
||||
click.echo(text or json.dumps(payload, sort_keys=True))
|
||||
click.echo(text or rendered_text or json.dumps(payload, sort_keys=True))
|
||||
|
||||
|
||||
async def _collect_events(
|
||||
|
||||
Reference in New Issue
Block a user