← Home

Ch 12 — Building the App

Ch 12 — Building the App

This is where everything clicks together. Aryan has variables, types, operators, strings, control flow, data structures, functions, classes, builtins, the standard library, psutil, and async HTTP. Chapter 12 assembles all of it into the final RAM Manager application — a full CLI tool with rich output, live updates, config file support, subprocess integration, and a versioned release history from v0.0 to v1.2.


Version History

VersionWhat was added
v0.0Bare script — print(psutil.virtual_memory())
v0.1Polling loop with threshold alert
v0.2Per-process table with formatted output
v0.3Config file (config.json)
v0.4Argparse CLI flags (--threshold, --interval)
v0.5click CLI with subcommands
v0.6rich styled tables
v0.7rich.live dashboard (live updating)
v0.8subprocess integration for ps / top
v0.9requests webhook alert
v1.0textual TUI app with panels
v1.1dataclasses + typed models throughout
v1.2Full capstone: async polling, history, reports

click — CLI Framework

click turns functions into subcommands with @click.command() and typed options.

import click

@click.group()
def cli():
    """RAM Manager — monitor your system memory."""
    pass

@cli.command()
@click.option("--threshold", default=80, help="Alert threshold (%).")
@click.option("--interval",  default=1,  help="Poll interval (seconds).")
@click.option("--count",     default=10, help="Number of polls to run.")
def watch(threshold, interval, count):
    """Live polling mode."""
    for tick in range(count):
        pct = psutil.virtual_memory().percent
        status = "WARN" if pct > threshold else "OK"
        click.echo(f"[{status}] RAM: {pct:.1f}%")
        time.sleep(interval)

@cli.command()
def report():
    """Generate a one-time snapshot report."""
    # ... write CSV report
    click.echo("Report saved.")

if __name__ == "__main__":
    cli()

rich — Beautiful Terminal Output

rich provides styled tables, panels, progress bars, and markup.

from rich.console import Console
from rich.table import Table

console = Console()

def print_process_table(snapshots):
    table = Table(title="Top Processes by RAM", show_lines=True)
    table.add_column("PID",     style="dim",    justify="right")
    table.add_column("Name",    style="bold")
    table.add_column("RSS MB",  style="cyan",   justify="right")
    table.add_column("Usage %", style="yellow", justify="right")

    for snap in sorted(snapshots, key=lambda s: s.rss_mb, reverse=True)[:10]:
        color = "red" if snap.pct > 80 else "green"
        table.add_row(
            str(snap.pid),
            snap.name,
            f"{snap.rss_mb:.1f}",
            f"[{color}]{snap.pct:.1f}%[/{color}]",
        )

    console.print(table)

rich.live — Live Updating Dashboard

Update a table in-place without scrolling the terminal.

from rich.live import Live
from rich.table import Table
import time

def make_table(snapshots) -> Table:
    table = Table(title=f"RAM Live — {datetime.now():%H:%M:%S}")
    # ... add columns and rows
    return table

with Live(make_table([]), refresh_per_second=1) as live:
    for _ in range(60):
        snaps = get_current_snapshots()
        live.update(make_table(snaps))
        time.sleep(1)

Config File

Load settings from JSON at startup, fall back to defaults.

from pathlib import Path
import json

DEFAULT_CONFIG = {
    "threshold_pct": 80,
    "poll_interval_s": 1,
    "top_n": 10,
    "log_path": str(Path.home() / ".ram_manager" / "ram.log"),
}

def load_config(path="config.json") -> dict:
    cfg_path = Path(path)
    if cfg_path.exists():
        with cfg_path.open() as f:
            user_cfg = json.load(f)
        return {**DEFAULT_CONFIG, **user_cfg}   # user overrides defaults
    return DEFAULT_CONFIG

subprocess — Shell Integration

Run ps, lsof, or top and parse their output.

import subprocess

def get_open_file_count(pid: int) -> int:
    result = subprocess.run(
        ["lsof", "-p", str(pid)],
        capture_output=True,
        text=True,
    )
    # Each line after the header is one open file
    lines = result.stdout.strip().splitlines()
    return max(0, len(lines) - 1)

Always use a list of arguments (not a shell string) to avoid injection vulnerabilities.


argparse — Standard Library CLI

A lighter alternative to click for simple scripts.

import argparse

parser = argparse.ArgumentParser(description="RAM Manager")
parser.add_argument("--threshold", type=float, default=80.0)
parser.add_argument("--interval",  type=int,   default=1)
parser.add_argument("--output",    type=str,   default="report.csv")

args = parser.parse_args()
# args.threshold  →  80.0
# args.interval   →  1

textual — Full TUI Application

textual builds full terminal UI apps with panels, widgets, and event loops.

from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, DataTable
from textual.reactive import reactive

class RamManagerApp(App):
    CSS_PATH = "ram_manager.tcss"

    BINDINGS = [
        ("q", "quit",    "Quit"),
        ("r", "refresh", "Refresh"),
    ]

    def compose(self) -> ComposeResult:
        yield Header()
        yield DataTable()
        yield Footer()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_columns("PID", "Name", "RSS MB", "%")
        self.set_interval(1, self.refresh_data)

    def refresh_data(self) -> None:
        table = self.query_one(DataTable)
        table.clear()
        for proc in psutil.process_iter(["pid", "name", "memory_percent"]):
            try:
                table.add_row(
                    str(proc.pid),
                    proc.name()[:25],
                    f"{proc.memory_info().rss / 1024**2:.1f}",
                    f"{proc.memory_percent():.1f}",
                )
            except (psutil.NoSuchProcess, psutil.AccessDenied):
                pass

App Architecture — v1.2

flowchart TD
    A[Entry Point: cli.py] --> B[click CLI group]
    B --> C[watch subcommand]
    B --> D[report subcommand]
    B --> E[tui subcommand]
    C --> F[load_config]
    F --> G[psutil polling loop]
    G --> H[ProcessSnapshot dataclasses]
    H --> I{Threshold crossed?}
    I -->|Yes| J[post_with_retry → Slack]
    I -->|No| K[Append to history deque]
    K --> L[rich.live update table]
    L --> G
    D --> M[write_report → CSV]
    E --> N[textual RamManagerApp]

Module Dependency Map

mindmap
  root((RAM Manager v1.2))
    CLI Layer
      click subcommands
      argparse fallback
    UI Layer
      rich tables
      rich.live dashboard
      textual TUI
    Data Layer
      psutil system reads
      dataclasses models
      collections.deque history
    Config Layer
      json config file
      os.environ secrets
      pathlib paths
    Network Layer
      requests webhook alert
      httpx async polling
      retry + backoff
    Persistence Layer
      csv reports
      sqlite3 history db
      logging structured logs

Key Takeaways

  • click provides composable CLI subcommands with type-checked options — the go-to for Python CLIs.
  • rich transforms plain terminal output into styled tables, panels, and markup with minimal code.
  • rich.live enables in-place updating dashboards — essential for a live RAM monitor.
  • Config files + environment variables + CLI flags form a three-tier config system: defaults → file → CLI wins.
  • subprocess.run() with a list argument safely integrates external system tools without shell injection risk.
  • argparse is the standard library’s CLI parser — use it for lightweight scripts, click for complex apps.
  • textual builds full TUI applications with widgets, event loops, and CSS-like styling.
  • The final RAM Manager is the sum of every concept in the course: data structures hold the snapshots, classes model them, functions process them, async fetches external data, and the UI layers display it all live.