tools/cloud-build/babysit/cli_ui.py (82 lines of code) (raw):

# Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import sys from typing import Sequence, Dict, Optional import time from enum import Enum from collections import defaultdict from .core import Status, Build, latest_by_trigger, trig_name class Color(Enum): GREEN = "\033[92m" YELLOW = "\033[93m" RED = "\033[91m" BLUE = "\033[94m" END = "\033[0m" class CliUI: # implements UIProto def __init__(self, no_color=False, short_url=False) -> None: self._status: Dict[str, Status] = {} self._change = False self._no_color = no_color self._short_url = short_url def on_init(self, builds: Sequence[Build]) -> None: for b in builds: self._status[b.id] = b.status if not builds: print("found no builds") else: self._render_summary(builds) def on_done(self, builds: Sequence[Build]) -> None: print("done") if self._change: self._render_summary(builds) def on_update(self, builds: Sequence[Build]) -> None: for b in builds: if b.status != self._status.get(b.id): print(self._render_build(b)) self._change = True self._status[b.id] = b.status def on_action(self, action: str, build: Build) -> None: pass def sleep(self, sec: int) -> None: time.sleep(sec) def _render_summary(self, builds: Sequence[Build]) -> None: status_order = { # show success and pending first (as less interesting) Status.SUCCESS: 0, Status.PENDING: 1} order_fn = lambda bc: (status_order.get(bc.build.status, 100), bc.build.status, trig_name(bc.build)) cnt = defaultdict(int) ordered = sorted(latest_by_trigger(builds).values(), key=order_fn) for bc in ordered: print(self._render_build(bc.build, bc.count)) cnt[bc.build.status] += 1 print(f"------- TOTAL:{sum(cnt.values())} ", end="") for s, c in cnt.items(): print(f"| {self._render_status(s)}: {c} ", end="") print("") def _color(self) -> bool: if self._no_color: return False return hasattr(sys.stdout, 'isatty') and sys.stdout.isatty() def _render_build(self, build: Build, count:int=1) -> str: status = self._render_status(build.status) cnt = f"[{count}]" if count > 1 else " " link = self._render_link(build) return f"{status}{cnt} {link}" def _render_status(self, status: Optional[Status]) -> str: sn = "NONE" if status is None else status.name if not self._color(): return sn CM = { Status.SUCCESS: Color.GREEN, Status.FAILURE: Color.RED, Status.TIMEOUT: Color.RED, Status.PENDING: Color.END, # default Status.QUEUED: Color.BLUE, Status.WORKING: Color.BLUE, } def_color = Color.YELLOW # render "unusual" states with something bright clr = CM.get(status, def_color).value return f"{clr}{sn}{Color.END.value}" def _url(self, build: Build) -> str: if not self._short_url: return build.log_url return f"go/ghpc-cb/{build.id}" def _render_link(self, build: Build) -> str: name, url = trig_name(build), self._url(build) return f"{name}\t{url}"