hoodini.utils.logging_utils

Centralized logging utilities with Rich styling and log levels.

  1"""Centralized logging utilities with Rich styling and log levels."""
  2
  3from __future__ import annotations
  4
  5import logging
  6from collections.abc import Callable
  7from datetime import datetime
  8from typing import Any
  9
 10from rich.console import Console
 11from rich.panel import Panel
 12
 13_QUIET = False
 14_DEBUG = False
 15
 16
 17def _make_console(quiet: bool, debug: bool) -> Console:
 18    # We add timestamps manually to messages to keep them visible during progress rendering.
 19    c = Console(
 20        quiet=quiet,
 21        log_time=False,
 22        log_path=False,
 23    )
 24    return c
 25
 26
 27console = _make_console(False, False)
 28logger = logging.getLogger("hoodini")
 29
 30
 31def configure_logging(*, quiet: bool = False, debug: bool = False) -> None:
 32    """Configure console/logging flags."""
 33    global _QUIET, _DEBUG, console
 34    _QUIET = quiet
 35    _DEBUG = debug
 36    console = _make_console(quiet, debug)
 37    level = logging.DEBUG if debug else logging.INFO
 38    logger.setLevel(level)
 39
 40
 41def info(message: str) -> None:
 42    if _QUIET:
 43        return
 44    console.print(f"{_ts()} [light_slate_grey]{message}[/light_slate_grey]")
 45
 46
 47def success(message: str) -> None:
 48    if _QUIET:
 49        return
 50    console.print(f"{_ts()} [green]✔ {message}[/green]")
 51
 52
 53def warn(message: str) -> None:
 54    if _QUIET:
 55        return
 56    console.print(f"{_ts()} [orange3]Warning:[/orange3] {message}")
 57
 58
 59def error(message: str) -> None:
 60    if _QUIET:
 61        return
 62    console.print(f"{_ts()} [bright_red]Error:[/bright_red] {message}")
 63
 64
 65def debug(message: str) -> None:
 66    if _QUIET or not _DEBUG:
 67        return
 68    console.print(f"{_ts()} [dim]{message}[/dim]")
 69
 70
 71def is_debug_enabled() -> bool:
 72    return _DEBUG and not _QUIET
 73
 74
 75def header(title: str, subtitle: str | None = None, border_style: str = "light_slate_grey") -> None:
 76    """Render a boxed header with optional subtitle."""
 77    if _QUIET:
 78        return
 79    text = f"[bold light_slate_grey]{title}[/bold light_slate_grey]"
 80    if subtitle:
 81        text += f"\n[dim]{subtitle}[/dim]"
 82    console.print(Panel.fit(text, border_style=border_style))
 83
 84
 85def stage_header(title: str, emoji: str = "") -> None:
 86    """Log a stage banner with timestamp."""
 87    header(f"{emoji} {title}" if emoji else title)
 88
 89
 90def stage_done(message: str) -> None:
 91    """Log a completion message with timestamp."""
 92    if _QUIET:
 93        return
 94    console.print(Panel.fit(f"[bold green]✔️  {message}[/bold green]", border_style="green"))
 95    console.print("[light_slate_grey]" + "─" * 80 + "[/light_slate_grey]")
 96
 97
 98def prompt(message: str, default: str | None = None) -> str:
 99    """Prompt user input aligned with log indentation."""
100    if _QUIET:
101        return default or ""
102    suffix = f" ({default})" if default else ""
103    prompt_text = f"{_ts()} {message}{suffix}: "
104    return console.input(prompt_text).strip()
105
106
107def run_with_spinner(
108    title: str, func: Callable[..., Any], *args, spinner_name: str = "dots", **kwargs
109):
110    """
111    Display a Rich spinner with `title` while running `func(*args, **kwargs)`.
112    Returns whatever `func` returns.
113    """
114    if _QUIET:
115        return func(*args, **kwargs)
116    with console.status(f"[bold cyan]{title}[/bold cyan]", spinner=spinner_name):
117        return func(*args, **kwargs)
118
119
120def _ts() -> str:
121    t = datetime.now().strftime("%H:%M:%S")
122    return f"[grey53][[/grey53][light_slate_grey]{t}[/light_slate_grey][grey53]][/grey53]"
console = <console width=534 None>
logger = <Logger hoodini (WARNING)>
def configure_logging(*, quiet: bool = False, debug: bool = False) -> None:
32def configure_logging(*, quiet: bool = False, debug: bool = False) -> None:
33    """Configure console/logging flags."""
34    global _QUIET, _DEBUG, console
35    _QUIET = quiet
36    _DEBUG = debug
37    console = _make_console(quiet, debug)
38    level = logging.DEBUG if debug else logging.INFO
39    logger.setLevel(level)

Configure console/logging flags.

def info(message: str) -> None:
42def info(message: str) -> None:
43    if _QUIET:
44        return
45    console.print(f"{_ts()} [light_slate_grey]{message}[/light_slate_grey]")
def success(message: str) -> None:
48def success(message: str) -> None:
49    if _QUIET:
50        return
51    console.print(f"{_ts()} [green]✔ {message}[/green]")
def warn(message: str) -> None:
54def warn(message: str) -> None:
55    if _QUIET:
56        return
57    console.print(f"{_ts()} [orange3]Warning:[/orange3] {message}")
def error(message: str) -> None:
60def error(message: str) -> None:
61    if _QUIET:
62        return
63    console.print(f"{_ts()} [bright_red]Error:[/bright_red] {message}")
def debug(message: str) -> None:
66def debug(message: str) -> None:
67    if _QUIET or not _DEBUG:
68        return
69    console.print(f"{_ts()} [dim]{message}[/dim]")
def is_debug_enabled() -> bool:
72def is_debug_enabled() -> bool:
73    return _DEBUG and not _QUIET
def stage_header(title: str, emoji: str = '') -> None:
86def stage_header(title: str, emoji: str = "") -> None:
87    """Log a stage banner with timestamp."""
88    header(f"{emoji} {title}" if emoji else title)

Log a stage banner with timestamp.

def stage_done(message: str) -> None:
91def stage_done(message: str) -> None:
92    """Log a completion message with timestamp."""
93    if _QUIET:
94        return
95    console.print(Panel.fit(f"[bold green]✔️  {message}[/bold green]", border_style="green"))
96    console.print("[light_slate_grey]" + "─" * 80 + "[/light_slate_grey]")

Log a completion message with timestamp.

def prompt(message: str, default: str | None = None) -> str:
 99def prompt(message: str, default: str | None = None) -> str:
100    """Prompt user input aligned with log indentation."""
101    if _QUIET:
102        return default or ""
103    suffix = f" ({default})" if default else ""
104    prompt_text = f"{_ts()} {message}{suffix}: "
105    return console.input(prompt_text).strip()

Prompt user input aligned with log indentation.

def run_with_spinner( title: str, func: Callable[..., typing.Any], *args, spinner_name: str = 'dots', **kwargs):
108def run_with_spinner(
109    title: str, func: Callable[..., Any], *args, spinner_name: str = "dots", **kwargs
110):
111    """
112    Display a Rich spinner with `title` while running `func(*args, **kwargs)`.
113    Returns whatever `func` returns.
114    """
115    if _QUIET:
116        return func(*args, **kwargs)
117    with console.status(f"[bold cyan]{title}[/bold cyan]", spinner=spinner_name):
118        return func(*args, **kwargs)

Display a Rich spinner with title while running func(*args, **kwargs). Returns whatever func returns.