Skalabilitas utilitas baris perintah internal (internal tooling) sering kali terbentur oleh dua masalah utama: import bloat yang memperlambat waktu startup aplikasi dan antarmuka konsol yang kaku. Menggabungkan library Rich untuk formatting teks/tabel dan Textual untuk Terminal User Interface (TUI) asinkron memberikan solusi modern tanpa mengorbankan performa sistem.
Arsitektur Modular Berbasis Command Pattern
Pola desain Command Pattern memisahkan antarmuka deklaratif modul dari logika eksekusi intinya. Setiap tool didaftarkan sebagai objek mandiri yang mengimplementasikan protocol seragam. Ini mencegah ketergantungan erat (tight coupling) antar utilitas.
from typing import Protocol, runtime_checkable
from rich.console import Console
@runtime_checkable
class ToolCommand(Protocol):
name: str
description: str
def execute(self, console: Console, *args: str) -> int:
...
Dynamic Module Loading Tanpa Import Bloat
Memuat semua library berat (seperti SDK cloud, konektor database, atau parser log) secara langsung di file konfigurasi utama membuat CLI lambat dibuka. Solusinya adalah memuat metadata modul terlebih dahulu, kemudian melakukan lazy importing menggunakan pustaka bawaan importlib saat perintah dipilih.
import importlib
from typing import Dict, Type
from app.core.protocol import ToolCommand
REGISTRY: Dict[str, str] = {
"db-ping": "app.tools.db:DatabasePingTool",
"k8s-logs": "app.tools.k8s:KubernetesLogTool",
}
def load_tool(command_name: str) -> ToolCommand:
if command_name not in REGISTRY:
raise ValueError(f"Perintah '{command_name}' tidak terdaftar.")
module_path, class_name = REGISTRY[command_name].split(":")
module = importlib.import_module(module_path)
command_class: Type[ToolCommand] = getattr(module, class_name)
return command_class()
Pendekatan ini membatasi pemrosesan I/O dan eksekusi bytecode hanya pada pustaka yang benar-benar dipanggil pengguna pada sesi aktif.
Antarmuka Konsol Interaktif Menggunakan Textual dan Rich
Textual menangani perulangan event (event loop), layout terminal reaktif, serta binding keyboard, sementara Rich menangani perenderan teks berformat, status spinner, dan tabel output di dalam widget Textual.
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, ListView, ListItem, Label, Log
from rich.table import Table
class MultitoolTUI(App):
BINDINGS = [("q", "quit", "Keluar"), ("r", "run_selected", "Eksekusi")]
def compose(self) -> ComposeResult:
yield Header()
yield ListView(
ListItem(Label("db-ping"), id="db-ping"),
ListItem(Label("k8s-logs"), id="k8s-logs"),
id="tool-list"
)
yield Log(id="console-log")
yield Footer()
def on_list_view_selected(self, event: ListView.Selected) -> None:
log_widget = self.query_one("#console-log", Log)
selected_id = event.item.id
log_widget.write_line(f"Menyiapkan eksekusi untuk modul: {selected_id}")
Validasi dan Update Metadata Internal Tool Secara Aman
Jika multitool mengambil daftar modul atau metadata konfigurasi dari endpoint remote (misalnya internal artifact repository), integritas skema wajib divalidasi guna mencegah eksekusi kode berbahaya atau konfigurasi korup. Validasi dapat diimplementasikan menggunakan schema guard ketat dengan tipe data Python baku.
import json
import urllib.request
from dataclasses import dataclass
@dataclass(frozen=True)
class ToolMetadata:
version: str
checksum: str
enabled: bool
def fetch_validated_metadata(endpoint_url: str) -> ToolMetadata:
req = urllib.request.Request(endpoint_url, headers={"User-Agent": "InternalCLI/1.0"})
with urllib.request.urlopen(req, timeout=5) as response:
if response.status != 200:
raise RuntimeError(f"Gagal mengambil data, status HTTP: {response.status}")
raw_data = json.loads(response.read().decode("utf-8"))
# Validasi skema eksplisit
if not isinstance(raw_data.get("version"), str) or not isinstance(raw_data.get("checksum"), str):
raise ValueError("Payload metadata tidak memenuhi skema yang valid.")
return ToolMetadata(
version=raw_data["version"],
checksum=raw_data["checksum"],
enabled=bool(raw_data.get("enabled", False))
)
Unit Testing Runner CLI
Runner perintah harus diuji secara terisolasi tanpa memerlukan rendering terminal penuh. Gunakan output stream in-memory Rich untuk memverifikasi routing command dan penanganan exception.
import io
from rich.console import Console
def test_tool_runner_execution():
test_buffer = io.StringIO()
mock_console = Console(file=test_buffer, force_terminal=False)
class DummyTool:
name = "test-cmd"
description = "Test command runner"
def execute(self, console: Console, *args: str) -> int:
console.print("Runner executed successfully")
return 0
tool = DummyTool()
exit_code = tool.execute(mock_console)
assert exit_code == 0
assert "Runner executed successfully" in test_buffer.getvalue()
if __name__ == "__main__":
test_tool_runner_execution()
print("Semua assert test berhasil lolos.")
Mitigasi Risiko dan Best Practice
- Fail-Fast Error Boundaries: Tangani kegagalan impor modul pada level registry dengan fallback visual yang jelas agar kerusakan satu modul tidak membuat keseluruhan CLI crash.
- Subprocess Safety: Jika multitool menjalankan binary lokal eksternal, gunakan
subprocess.rundenganshell=Falsedan teruskan argumen via array untuk mitigasi command injection. - Terminal Reset: Pastikan exception yang tidak tertangani mengeksekusi teardown terminal dengan benar agar state terminal pengguna tidak rusak setelah CLI ditutup mendadak.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!