Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Changelog

### Next

* feat(ui): add modern Terminal User Interface (TUI) with `rich` tables for beautiful output formatting
* Add unified `kaggle search` command across competitions, datasets, notebooks, models, users, and discussions
* Add `--wait`/`--poll-interval` to `kaggle competitions submit` to wait for scoring, and add `kaggle competitions submission <ref>` to look up a single submission's status and score

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Some of the key features are:
* List, create, update, download or delete models & model variations.
* List, update & run, download code & output or delete kernels (notebooks).
* Browse and read discussion forums.
* **Modern Terminal User Interface (TUI)**: Beautiful and responsive output tables out of the box using `rich`.

## Installation

Expand Down
26 changes: 26 additions & 0 deletions documentation/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Kaggle CLI TUI (Terminal User Interface)

Kaggle CLI now ships with a built-in modern Terminal User Interface (TUI) powered by [`rich`](https://github.com/Textualize/rich).

## Visual Enhancements

When you run list commands (e.g., `kaggle datasets list`, `kaggle competitions list`, etc.) in an interactive terminal, the CLI will format the output as a gorgeous table with:
- **Rounded Borders:** Clean and modern look.
- **Color Coding:** Headers in bright cyan, borders in bright black.
- **Smart Alignment:** Numeric columns (like sizes, download counts, and vote counts) are automatically right-aligned for easier scanning.
- **Graceful Text Wrapping:** Content that overflows is neatly wrapped.

## Automatic Fallback (No-breaking changes)

The new UI is completely **safe for automation scripts**. The CLI automatically detects if it is being piped into another command (e.g., `kaggle datasets list > data.txt` or `kaggle datasets list | grep ...`).

If standard output is not a TTY terminal, or if the `rich` library is not available, the CLI will seamlessly and silently fall back to the raw, uncolored string formatting. This guarantees 100% backward compatibility for all your existing scripts.

## Supported Commands
The modern UI affects the output of all list-oriented commands, including:
- `kaggle competitions list`
- `kaggle datasets list`
- `kaggle kernels list`
- `kaggle models list`
- `kaggle models instances`
- `kaggle config view`
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies = [
"protobuf",
"jupytext",
"python-dotenv",
"rich >= 13.0.0",
]

[project.scripts]
Expand Down
11 changes: 11 additions & 0 deletions src/kaggle/api/kaggle_api_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -8705,6 +8705,17 @@ def print_table(self, items, fields, labels=None):
"""
if labels is None:
labels = fields

try:
from kaggle.ui import print_rich_table, RICH_AVAILABLE
if RICH_AVAILABLE:
def attr_getter(i, f):
return getattr(i, self.camel_to_snake(f))
if print_rich_table(items, fields, labels, string_formatter=self.string, attr_getter=attr_getter):
return
except ImportError:
pass

formats = []
borders = []
if len(items) == 0:
Expand Down
74 changes: 74 additions & 0 deletions src/kaggle/ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import sys
from typing import List, Any, Optional

try:
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.theme import Theme
from rich.text import Text

custom_theme = Theme({
"info": "cyan",
"warning": "yellow",
"error": "bold red",
"success": "bold green"
})
console = Console(theme=custom_theme)
RICH_AVAILABLE = True
except ImportError:
RICH_AVAILABLE = False
console = None

def print_rich_table(items: List[Any], fields: List[str], labels: Optional[List[str]] = None, string_formatter=None, attr_getter=None):
if not RICH_AVAILABLE:
return False

if labels is None:
labels = fields

if not items:
console.print(Panel("No data found.", style="warning", expand=False))
return True

from rich import box
table = Table(show_header=True, header_style="bold bright_cyan", border_style="bright_black", box=box.ROUNDED)

for i, label in enumerate(labels):
field = fields[i].lower()
justify = "right" if field in ["size", "reward", "id", "downloadcount", "votecount"] else "left"
table.add_column(label, justify=justify)

for item in items:
row = []
for field in fields:
val = attr_getter(item, field) if attr_getter else getattr(item, field)
val_str = string_formatter(val) if string_formatter else str(val)
row.append(val_str)
table.add_row(*row)

console.print(table)
return True

def print_info(message: str):
if RICH_AVAILABLE:
console.print(message, style="info")
else:
print(message)

def print_error(message: str, exc: Optional[Exception] = None):
if RICH_AVAILABLE:
if exc:
console.print(f"[error]Error:[/error] {message}\n[dim]{str(exc)}[/dim]")
else:
console.print(f"[error]Error:[/error] {message}")
else:
print(f"Error: {message}", file=sys.stderr)
if exc:
print(str(exc), file=sys.stderr)

def print_success(message: str):
if RICH_AVAILABLE:
console.print(f"✅ [success]{message}[/success]")
else:
print(message)
41 changes: 41 additions & 0 deletions tests/unit/test_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pytest
from unittest.mock import patch, MagicMock
from kaggle.ui import print_rich_table, print_info, print_error, print_success, RICH_AVAILABLE

class DummyItem:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)

def test_print_rich_table_no_data():
with patch("kaggle.ui.console") as mock_console:
assert print_rich_table([], ["id", "name"]) is True
mock_console.print.assert_called_once()
assert "No data found" in str(mock_console.print.call_args)

def test_print_rich_table_with_data():
items = [DummyItem(id=1, name="Test")]
with patch("kaggle.ui.console") as mock_console:
assert print_rich_table(items, ["id", "name"], ["ID", "Name"]) is True
mock_console.print.assert_called_once()
table_arg = mock_console.print.call_args[0][0]
# It should be a Table object
from rich.table import Table
assert isinstance(table_arg, Table)
assert len(table_arg.columns) == 2
assert len(table_arg.rows) == 1

def test_print_info():
with patch("kaggle.ui.console") as mock_console:
print_info("Hello")
mock_console.print.assert_called_once_with("Hello", style="info")

def test_print_error():
with patch("kaggle.ui.console") as mock_console:
print_error("Error msg")
mock_console.print.assert_called_once_with("[error]Error:[/error] Error msg")

def test_print_success():
with patch("kaggle.ui.console") as mock_console:
print_success("Done")
mock_console.print.assert_called_once_with("✅ [success]Done[/success]")
Loading