Compare commits

...

2 Commits

Author SHA1 Message Date
Joseph Doherty 4a19854eb9 docs: per-client High-level walker example using LazyBrowseNode
Add a "High-level walker" subsection under each client's "Browsing
lazily" section showing idiomatic use of LazyBrowseNode (browse +
expand, idempotency note, redeploy refresh pattern).
2026-05-28 14:34:19 -04:00
Joseph Doherty a4467e23ef client/python: make LazyBrowseNode.expand concurrency-safe 2026-05-28 14:32:35 -04:00
7 changed files with 181 additions and 9 deletions
+26
View File
@@ -218,6 +218,32 @@ for (int i = 0; i < roots.Children.Count; i++)
}
```
#### High-level walker
For UI trees, the client provides a `LazyBrowseNode` walker that handles
sibling pagination and the `child_has_children` hint for you:
```csharp
await using GalaxyRepositoryClient repository = GalaxyRepositoryClient.Create(
new MxGatewayClientOptions { Endpoint = new Uri("http://localhost:5000"), ApiKey = apiKey });
IReadOnlyList<LazyBrowseNode> roots = await repository.BrowseAsync();
foreach (LazyBrowseNode root in roots)
{
if (root.HasChildrenHint)
{
await root.ExpandAsync();
}
foreach (LazyBrowseNode child in root.Children)
{
Console.WriteLine($"{child.Object.TagName} ({(child.HasChildrenHint ? "has children" : "leaf")})");
}
}
```
`ExpandAsync` is idempotent — calling it twice fires only one RPC,
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
`BrowseAsync` again from the root.
### Watching deploy events
`WatchDeployEventsAsync` opens the `WatchDeployEvents` server-streaming RPC. The
+40
View File
@@ -143,6 +143,46 @@ for i, child := range reply.GetChildren() {
}
```
#### High-level walker
For UI trees, the client provides a `LazyBrowseNode` walker that handles
sibling pagination and the `child_has_children` hint for you:
```go
galaxy, err := mxgateway.DialGalaxy(ctx, mxgateway.Options{
Endpoint: "localhost:5000",
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
Plaintext: true,
})
if err != nil {
log.Fatal(err)
}
defer galaxy.Close()
roots, err := galaxy.Browse(ctx, nil)
if err != nil {
log.Fatal(err)
}
for _, root := range roots {
if root.HasChildrenHint() {
if err := root.Expand(ctx); err != nil {
log.Fatal(err)
}
}
for _, child := range root.Children() {
kind := "leaf"
if child.HasChildrenHint() {
kind = "has children"
}
fmt.Printf("%s (%s)\n", child.Object().GetTagName(), kind)
}
}
```
`Expand` is idempotent — calling it twice fires only one RPC,
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
`Browse` again from the root.
### Watching deploy events
`WatchDeployEvents` opens a server-streaming subscription. The server emits a
+30
View File
@@ -139,6 +139,36 @@ for (int i = 0; i < children.size(); i++) {
}
```
#### High-level walker
For UI trees, the client provides a `LazyBrowseNode` walker that handles
sibling pagination and the `child_has_children` hint for you:
```java
MxGatewayClientOptions options = MxGatewayClientOptions.builder()
.endpoint("localhost:5000")
.apiKey(System.getenv("MXGATEWAY_API_KEY"))
.plaintext(true)
.build();
try (GalaxyRepositoryClient galaxy = GalaxyRepositoryClient.connect(options)) {
List<LazyBrowseNode> roots = galaxy.browse();
for (LazyBrowseNode root : roots) {
if (root.hasChildrenHint()) {
root.expand();
}
for (LazyBrowseNode child : root.getChildren()) {
String kind = child.hasChildrenHint() ? "has children" : "leaf";
System.out.println(child.getObject().getTagName() + " (" + kind + ")");
}
}
}
```
`expand` is idempotent — calling it twice fires only one RPC,
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
`browse` again from the root.
### Watching deploy events
`GalaxyRepository.WatchDeployEvents` is a server-streaming RPC: the gateway
+24
View File
@@ -157,6 +157,30 @@ for child, has_children in zip(reply.children, reply.child_has_children):
print(child.tag_name, "expand=" + str(has_children))
```
#### High-level walker
For UI trees, the client provides a `LazyBrowseNode` walker that handles
sibling pagination and the `child_has_children` hint for you:
```python
async with await GalaxyRepositoryClient.connect(
endpoint="localhost:5000",
api_key="<gateway-api-key>",
plaintext=True,
) as galaxy:
roots = await galaxy.browse()
for root in roots:
if root.has_children_hint:
await root.expand()
for child in root.children:
kind = "has children" if child.has_children_hint else "leaf"
print(f"{child.object.tag_name} ({kind})")
```
`expand` is idempotent — calling it twice fires only one RPC,
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
`browse` again from the root.
### Watching deploy events
`GalaxyRepositoryClient.watch_deploy_events` opens a server-streaming
@@ -292,6 +292,7 @@ class LazyBrowseNode:
self._options = options
self._children: list[LazyBrowseNode] = []
self._is_expanded = False
self._expand_lock = asyncio.Lock()
@property
def object(self) -> galaxy_pb.GalaxyObject:
@@ -317,14 +318,17 @@ class LazyBrowseNode:
"""Fetch direct children of this node; no-op on subsequent calls."""
if self._is_expanded:
return
new_children: list[LazyBrowseNode] = []
async for child in self._client._iter_browse_children(
parent_gobject_id=self._object.gobject_id,
options=self._options,
):
new_children.append(child)
self._children.extend(new_children)
self._is_expanded = True
async with self._expand_lock:
if self._is_expanded:
return
new_children: list[LazyBrowseNode] = []
async for child in self._client._iter_browse_children(
parent_gobject_id=self._object.gobject_id,
options=self._options,
):
new_children.append(child)
self._children.extend(new_children)
self._is_expanded = True
async def _canceling_iterator(call: Any) -> AsyncIterator[galaxy_pb.DeployEvent]:
+24 -1
View File
@@ -391,6 +391,28 @@ async def test_browse_expand_idempotent_no_second_rpc() -> None:
assert len(roots[0].children) == 1
@pytest.mark.asyncio
async def test_browse_expand_concurrent_callers_only_fire_one_rpc() -> None:
stub = FakeGalaxyStub()
stub.browse_children.replies = [
_build_browse_reply([_obj(1, "Plant", is_area=True)], [True], 7),
_build_browse_reply([_obj(2, "Mixer_001")], [False], 7),
]
client = await GalaxyRepositoryClient.connect(
ClientOptions(endpoint="fake", plaintext=True),
stub=stub,
)
roots = await client.browse()
# Ten concurrent expand calls on the same node should issue exactly one RPC.
await asyncio.gather(*(roots[0].expand() for _ in range(10)))
assert roots[0].is_expanded
assert len(roots[0].children) == 1
# 1 roots fetch + exactly 1 expand fetch = 2 total
assert len(stub.browse_children.requests) == 2
@pytest.mark.asyncio
async def test_browse_expand_unknown_parent_raises_mxgateway_error() -> None:
stub = FakeGalaxyStub()
@@ -506,7 +528,8 @@ class FakeUnary:
def __init__(self, replies: list[Any]) -> None:
self.replies = replies
self.requests: list[Any] = []
self.exceptions: list[BaseException] = []
# None entries mean "no exception on this call"; aligns with the replies queue index-by-index.
self.exceptions: list[BaseException | None] = []
self.metadata: tuple[tuple[str, str], ...] | None = None
async def __call__(
+25
View File
@@ -157,6 +157,31 @@ for (child, has_children) in reply.children.iter().zip(reply.child_has_children.
}
```
#### High-level walker
For UI trees, the client provides a `LazyBrowseNode` walker that handles
sibling pagination and the `child_has_children` hint for you:
```rust
let mut client = GalaxyClient::connect(
ClientOptions::new("http://localhost:5000").with_api_key(ApiKey::new(api_key)),
).await?;
let roots = client.browse(None).await?;
for root in &roots {
if root.has_children_hint() {
root.expand().await?;
}
for child in root.children().await {
let kind = if child.has_children_hint() { "has children" } else { "leaf" };
println!("{} ({kind})", child.object().tag_name);
}
}
```
`expand` is idempotent — calling it twice fires only one RPC,
and is safe under concurrent callers. To refresh after a Galaxy redeploy, call
`browse` again from the root.
### Watching deploy events
`watch_deploy_events` opens the `WatchDeployEvents` server stream. The