fix(python): UTC-normalize galaxy-last-deploy output, add deploy-event collector, help text, test
This commit is contained in:
@@ -22,6 +22,7 @@ from zb_mom_ww_mxgateway.auth import redact_secret
|
|||||||
from zb_mom_ww_mxgateway.client import GatewayClient
|
from zb_mom_ww_mxgateway.client import GatewayClient
|
||||||
from zb_mom_ww_mxgateway.errors import MxGatewayError
|
from zb_mom_ww_mxgateway.errors import MxGatewayError
|
||||||
from zb_mom_ww_mxgateway.galaxy import GalaxyRepositoryClient
|
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.generated import mxaccess_gateway_pb2 as pb
|
||||||
from zb_mom_ww_mxgateway.options import ClientOptions
|
from zb_mom_ww_mxgateway.options import ClientOptions
|
||||||
from zb_mom_ww_mxgateway.values import MxValueInput, to_mx_value
|
from zb_mom_ww_mxgateway.values import MxValueInput, to_mx_value
|
||||||
@@ -561,8 +562,20 @@ def galaxy_discover(**kwargs: Any) -> None:
|
|||||||
help="ISO-8601 timestamp; when it matches the current cached deploy time the "
|
help="ISO-8601 timestamp; when it matches the current cached deploy time the "
|
||||||
"bootstrap event is suppressed.",
|
"bootstrap event is suppressed.",
|
||||||
)
|
)
|
||||||
@click.option("--max-events", default=1, type=int, show_default=True)
|
@click.option(
|
||||||
@click.option("--timeout", default=5.0, type=float, show_default=True)
|
"--max-events",
|
||||||
|
default=1,
|
||||||
|
type=int,
|
||||||
|
show_default=True,
|
||||||
|
help="Stop after collecting this many deploy events.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--timeout",
|
||||||
|
default=5.0,
|
||||||
|
type=float,
|
||||||
|
show_default=True,
|
||||||
|
help="Seconds to wait for each event before stopping.",
|
||||||
|
)
|
||||||
@click.option("--json", "output_json", is_flag=True, help="Emit JSON output.")
|
@click.option("--json", "output_json", is_flag=True, help="Emit JSON output.")
|
||||||
def galaxy_watch(**kwargs: Any) -> None:
|
def galaxy_watch(**kwargs: Any) -> None:
|
||||||
"""Stream a bounded number of Galaxy deploy events."""
|
"""Stream a bounded number of Galaxy deploy events."""
|
||||||
@@ -998,7 +1011,10 @@ async def _galaxy_last_deploy(**kwargs: Any) -> dict[str, Any]:
|
|||||||
"present": last_deploy is not None,
|
"present": last_deploy is not None,
|
||||||
}
|
}
|
||||||
if last_deploy is not None:
|
if last_deploy is not None:
|
||||||
payload["timeOfLastDeploy"] = last_deploy.isoformat()
|
# galaxy.py returns a timezone-NAIVE UTC datetime (protobuf ToDatetime()).
|
||||||
|
# Stamp it as UTC so the emitted ISO-8601 carries an unambiguous offset,
|
||||||
|
# matching the Go client's "...Z" output.
|
||||||
|
payload["timeOfLastDeploy"] = last_deploy.replace(tzinfo=timezone.utc).isoformat()
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
@@ -1015,7 +1031,7 @@ async def _galaxy_watch(**kwargs: Any) -> dict[str, Any]:
|
|||||||
last_seen = kwargs.get("last_seen_deploy_time")
|
last_seen = kwargs.get("last_seen_deploy_time")
|
||||||
last_seen_dt = _parse_datetime(last_seen) if last_seen else None
|
last_seen_dt = _parse_datetime(last_seen) if last_seen else None
|
||||||
async with await _connect_galaxy(kwargs) as galaxy:
|
async with await _connect_galaxy(kwargs) as galaxy:
|
||||||
events = await _collect_events(
|
events = await _collect_deploy_events(
|
||||||
galaxy.watch_deploy_events(last_seen_dt),
|
galaxy.watch_deploy_events(last_seen_dt),
|
||||||
max_events=kwargs["max_events"],
|
max_events=kwargs["max_events"],
|
||||||
timeout=kwargs["timeout"],
|
timeout=kwargs["timeout"],
|
||||||
@@ -1178,6 +1194,34 @@ async def _collect_alarm_messages(
|
|||||||
return collected
|
return collected
|
||||||
|
|
||||||
|
|
||||||
|
async def _collect_deploy_events(
|
||||||
|
events: Any,
|
||||||
|
*,
|
||||||
|
max_events: int,
|
||||||
|
timeout: float,
|
||||||
|
) -> list[galaxy_pb.DeployEvent]:
|
||||||
|
if max_events > MAX_AGGREGATE_EVENTS:
|
||||||
|
raise click.BadParameter(
|
||||||
|
f"must be less than or equal to {MAX_AGGREGATE_EVENTS}",
|
||||||
|
param_hint="--max-events",
|
||||||
|
)
|
||||||
|
|
||||||
|
collected: list[galaxy_pb.DeployEvent] = []
|
||||||
|
iterator = events.__aiter__()
|
||||||
|
try:
|
||||||
|
while len(collected) < max_events:
|
||||||
|
collected.append(await asyncio.wait_for(iterator.__anext__(), timeout=timeout))
|
||||||
|
except StopAsyncIteration:
|
||||||
|
pass
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
close = getattr(iterator, "aclose", None)
|
||||||
|
if close is not None:
|
||||||
|
await close()
|
||||||
|
return collected
|
||||||
|
|
||||||
|
|
||||||
def _parse_value(raw_value: str, value_type: str) -> MxValueInput:
|
def _parse_value(raw_value: str, value_type: str) -> MxValueInput:
|
||||||
normalized = value_type.lower()
|
normalized = value_type.lower()
|
||||||
if normalized == "bool":
|
if normalized == "bool":
|
||||||
|
|||||||
@@ -216,9 +216,11 @@ def test_batch_continues_after_error_line() -> None:
|
|||||||
class _FakeGalaxyClient:
|
class _FakeGalaxyClient:
|
||||||
"""Minimal async-context-manager fake satisfying the galaxy command bodies."""
|
"""Minimal async-context-manager fake satisfying the galaxy command bodies."""
|
||||||
|
|
||||||
def __init__(self, *, ok: bool = True, objects=None) -> None:
|
def __init__(self, *, ok: bool = True, objects=None, last_deploy=None, events=None) -> None:
|
||||||
self._ok = ok
|
self._ok = ok
|
||||||
self._objects = objects or []
|
self._objects = objects or []
|
||||||
|
self._last_deploy = last_deploy
|
||||||
|
self._events = events or []
|
||||||
|
|
||||||
async def __aenter__(self) -> "_FakeGalaxyClient":
|
async def __aenter__(self) -> "_FakeGalaxyClient":
|
||||||
return self
|
return self
|
||||||
@@ -232,6 +234,19 @@ class _FakeGalaxyClient:
|
|||||||
async def discover_hierarchy(self):
|
async def discover_hierarchy(self):
|
||||||
return self._objects
|
return self._objects
|
||||||
|
|
||||||
|
async def get_last_deploy_time(self):
|
||||||
|
# Mirrors galaxy.py: protobuf ToDatetime() yields a timezone-NAIVE UTC datetime.
|
||||||
|
return self._last_deploy
|
||||||
|
|
||||||
|
def watch_deploy_events(self, _last_seen_deploy_time=None):
|
||||||
|
events = self._events
|
||||||
|
|
||||||
|
async def _iter():
|
||||||
|
for event in events:
|
||||||
|
yield event
|
||||||
|
|
||||||
|
return _iter()
|
||||||
|
|
||||||
|
|
||||||
def _patch_galaxy_connect(monkeypatch: pytest.MonkeyPatch, fake: _FakeGalaxyClient) -> None:
|
def _patch_galaxy_connect(monkeypatch: pytest.MonkeyPatch, fake: _FakeGalaxyClient) -> None:
|
||||||
async def fake_connect(options, **_kwargs):
|
async def fake_connect(options, **_kwargs):
|
||||||
@@ -275,6 +290,42 @@ def test_galaxy_discover_serializes_objects(monkeypatch: pytest.MonkeyPatch) ->
|
|||||||
assert payload["objects"][1]["gobjectId"] == 8
|
assert payload["objects"][1]["gobjectId"] == 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_galaxy_last_deploy_emits_utc_iso(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""The naive-UTC deploy time from the library must be emitted as unambiguous UTC ISO-8601."""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
naive_utc = datetime(2025, 6, 15, 12, 0, 0) # noqa: DTZ001 -- mirrors protobuf ToDatetime()
|
||||||
|
_patch_galaxy_connect(monkeypatch, _FakeGalaxyClient(last_deploy=naive_utc))
|
||||||
|
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
main,
|
||||||
|
["galaxy-last-deploy", "--plaintext", "--json"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
payload = json.loads(result.output)
|
||||||
|
assert payload["command"] == "galaxy-last-deploy"
|
||||||
|
assert payload["present"] is True
|
||||||
|
assert payload["timeOfLastDeploy"].endswith(("+00:00", "Z"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_galaxy_watch_serializes_deploy_events(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
from zb_mom_ww_mxgateway.generated import galaxy_repository_pb2 as galaxy_pb
|
||||||
|
|
||||||
|
events = [galaxy_pb.DeployEvent(sequence=1)]
|
||||||
|
_patch_galaxy_connect(monkeypatch, _FakeGalaxyClient(events=events))
|
||||||
|
|
||||||
|
result = CliRunner().invoke(
|
||||||
|
main,
|
||||||
|
["galaxy-watch", "--plaintext", "--max-events", "1", "--json"],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
payload = json.loads(result.output)
|
||||||
|
assert payload["command"] == "galaxy-watch"
|
||||||
|
assert len(payload["events"]) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_galaxy_commands_are_registered() -> None:
|
def test_galaxy_commands_are_registered() -> None:
|
||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
for command in (
|
for command in (
|
||||||
|
|||||||
Reference in New Issue
Block a user