Resolve Client.Python-003, -005, -009 code-review findings

Client.Python-003: stream_events_raw and query_active_alarms passed `timeout`
to the stub with no TypeError fallback, unlike _unary. Both now route through
a shared _open_stream helper that strips `timeout` on TypeError.

Client.Python-005: discover_hierarchy buffered the entire Galaxy hierarchy in
memory. Added GalaxyRepositoryClient.iter_hierarchy, a lazy async generator
yielding objects page-by-page; discover_hierarchy is now a thin wrapper that
preserves its list contract. README documents iter_hierarchy.

Client.Python-009: added regression coverage for previously untested paths —
write2/add_item2 request shape, the MAX_BULK_ITEMS boundary, the None-argument
TypeError guards, TLS ca_file reading, and the non-auth map_rpc_error fallthrough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-18 21:45:16 -04:00
parent f13f35bc79
commit e4fbbb541a
7 changed files with 611 additions and 14 deletions
+19 -2
View File
@@ -133,7 +133,7 @@ class GatewayClient:
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
if self.options.stream_timeout is not None:
kwargs["timeout"] = self.options.stream_timeout
call = self.raw_stub.StreamEvents(request, **kwargs)
call = _open_stream(self.raw_stub.StreamEvents, request, kwargs)
return _canceling_iterator(call)
async def acknowledge_alarm(
@@ -169,7 +169,7 @@ class GatewayClient:
kwargs: dict[str, Any] = {"metadata": merge_metadata(self.options.api_key, metadata)}
if self.options.stream_timeout is not None:
kwargs["timeout"] = self.options.stream_timeout
call = self.raw_stub.QueryActiveAlarms(request, **kwargs)
call = _open_stream(self.raw_stub.QueryActiveAlarms, request, kwargs)
return _canceling_active_alarms_iterator(call)
async def _unary(
@@ -201,6 +201,23 @@ class GatewayClient:
raise map_rpc_error(operation, error) from error
def _open_stream(method: Any, request: Any, kwargs: dict[str, Any]) -> Any:
"""Open a server-streaming call, dropping ``timeout`` if the stub rejects it.
Mirrors the fallback in ``_unary`` so an older or fake stub that does not
accept a ``timeout`` keyword argument does not crash when ``stream_timeout``
is configured.
"""
try:
return method(request, **kwargs)
except TypeError as error:
if "timeout" not in kwargs or "unexpected keyword argument 'timeout'" not in str(error):
raise
kwargs.pop("timeout")
return method(request, **kwargs)
async def _canceling_iterator(call: Any) -> AsyncIterator[pb.MxEvent]:
try:
async for event in call:
+23 -5
View File
@@ -114,10 +114,17 @@ class GalaxyRepositoryClient:
return None
return reply.time_of_last_deploy.ToDatetime()
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
"""Return the deployed Galaxy object hierarchy as raw proto messages."""
async def iter_hierarchy(self) -> AsyncIterator[galaxy_pb.GalaxyObject]:
"""Yield the deployed Galaxy object hierarchy one object at a time.
Pages are fetched lazily: a page is only requested once the caller has
consumed every object from the previous page. This keeps peak memory
bounded by a single page (``_DISCOVER_HIERARCHY_PAGE_SIZE`` objects)
rather than the whole Galaxy. Use this for large Galaxies; use
:meth:`discover_hierarchy` when a fully buffered ``list`` is convenient
and the Galaxy is known to be small.
"""
objects: list[galaxy_pb.GalaxyObject] = []
seen_page_tokens: set[str] = set()
page_token = ""
while True:
@@ -129,16 +136,27 @@ class GalaxyRepositoryClient:
page_token=page_token,
),
)
objects.extend(reply.objects)
for obj in reply.objects:
yield obj
page_token = reply.next_page_token
if not page_token:
return objects
return
if page_token in seen_page_tokens:
raise MxGatewayError(
f"galaxy discover hierarchy returned repeated page token {page_token!r}"
)
seen_page_tokens.add(page_token)
async def discover_hierarchy(self) -> list[galaxy_pb.GalaxyObject]:
"""Return the deployed Galaxy object hierarchy as raw proto messages.
This buffers every object (and its full attribute list) into a single
in-memory ``list``. For a large Galaxy prefer :meth:`iter_hierarchy`,
which streams objects page by page without holding the whole hierarchy.
"""
return [obj async for obj in self.iter_hierarchy()]
def watch_deploy_events(
self,
last_seen_deploy_time: datetime | None = None,