import sys import threading import time from dataclasses import dataclass, field from datetime import datetime from typing import Optional, TypedDict, Callable, Dict, Literal from rich.console import Console from rich.style import Style from rich.text import Text from e2b.template.utils import strip_ansi_escape_codes """Log entry severity levels.""" LogEntryLevel = Literal["debug", "info", "warn", "error"] @dataclass class LogEntry: """ Represents a single log entry from the template build process. """ timestamp: datetime level: LogEntryLevel message: str def __post_init__(self): self.message = strip_ansi_escape_codes(self.message) def __str__(self) -> str: return f"[{self.timestamp.isoformat()}] [{self.level}] {self.message}" @dataclass class LogEntryStart(LogEntry): """ Special log entry indicating the start of a build process. """ level: LogEntryLevel = field(default="debug", init=False) @dataclass class LogEntryEnd(LogEntry): """ Special log entry indicating the end of a build process. """ level: LogEntryLevel = field(default="debug", init=False) """ Interval in milliseconds for updating the build timer display. """ TIMER_UPDATE_INTERVAL_MS = 150 """ Default minimum log level to display. """ DEFAULT_LEVEL: LogEntryLevel = "info" """ Colored labels for each log level. """ levels: Dict[LogEntryLevel, tuple[str, Style]] = { "error": ("ERROR", Style(color="red")), "warn": ("WARN ", Style(color="#FF4400")), "info": ("INFO ", Style(color="#FF8800")), "debug": ("DEBUG", Style(color="bright_black")), } """ Numeric ordering of log levels for comparison (lower = less severe). """ level_order = { "debug": 0, "info": 1, "warn": 2, "error": 3, } def set_interval(func, interval): """ Returns a stop function that can be called to cancel the interval. Similar to JavaScript's setInterval. :param func: Function to execute at each interval :param interval: Interval duration in **seconds** :return: Stop function that can be called to cancel the interval """ stopped = threading.Event() def loop(): while not stopped.is_set(): if stopped.wait(interval): # wait returns True if stopped break if not stopped.is_set(): # Double-check before executing func() threading.Thread(target=loop, daemon=True).start() return stopped.set # Return the stop function class DefaultBuildLoggerInitialState(TypedDict): start_time: float animation_frame: int timer: Optional[Callable[[], None]] class DefaultBuildLogger: __console = Console() __min_level: LogEntryLevel __state: DefaultBuildLoggerInitialState def __init__(self, min_level: Optional[LogEntryLevel] = None): self.__min_level = min_level if min_level is not None else DEFAULT_LEVEL self.__reset_initial_state() def logger(self, log): if isinstance(log, LogEntryStart): self.__start_timer() return if isinstance(log, LogEntryEnd): if self.__state["timer"] is not None: self.__state["timer"]() return # Filter by minimum level if level_order[log.level] < level_order[self.__min_level]: return formatted_line = self.__format_log_line(log) self.__console.print(formatted_line) # Redraw the timer line self.__update_timer() def __reset_initial_state(self, timer: Optional[Callable[[], None]] = None): self.__state = { "start_time": time.time(), "animation_frame": 0, "timer": timer, } def __format_timer_line(self) -> str: elapsed_seconds = time.time() - self.__state["start_time"] return f"{elapsed_seconds:.1f}s" def __animate_status(self) -> str: frames = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"] idx = self.__state["animation_frame"] % len(frames) return frames[idx] def __format_log_line(self, line: LogEntry) -> Text: timer = self.__format_timer_line().ljust(5) timestamp = line.timestamp.strftime("%H:%M:%S") level_text, level_style = levels.get(line.level, levels[DEFAULT_LEVEL]) # Build a rich Text object text = Text.assemble( timer, " | ", (timestamp, "dim"), " ", (level_text, level_style), " ", line.message, ) return text def __start_timer(self): if not sys.stdout.isatty(): return # Start the timer interval stop_timer = set_interval( self.__update_timer, TIMER_UPDATE_INTERVAL_MS / 1000.0 ) self.__reset_initial_state(stop_timer) # Initial timer display self.__update_timer() def __update_timer(self): if not sys.stdout.isatty(): return self.__state["animation_frame"] += 1 jumping_squares = self.__animate_status() timer_text = Text.assemble( jumping_squares, " Building ", self.__format_timer_line() ) # Print with carriage return self.__console.print(timer_text, end="\r") def default_build_logger( min_level: Optional[LogEntryLevel] = None, ) -> Callable[[LogEntry], None]: """ Create a default build logger with animated timer display. :param min_level: Minimum log level to display (default: 'info') :return: Logger function that accepts LogEntry instances Example ```python from e2b import Template, default_build_logger template = Template().from_python_image() # Use with build - implementation would be in build_async module # await Template.build(template, # alias='my-template', # on_build_logs=default_build_logger(min_level='debug') # ) ``` """ build_logger = DefaultBuildLogger(min_level) return build_logger.logger