Ch 11 — Real-World Tooling
Ch 11 — Real-World Tooling
The RAM manager is almost feature-complete. Now Aryan needs to connect it to the real world: actually reading system memory with psutil, fetching data over HTTP, handling network failures with retries, streaming large responses, persisting conversation history for an AI assistant integration, going async for non-blocking I/O, and generating file reports. This chapter bridges the gap between Python exercises and production software.
psutil — System Resource Access
psutil is the backbone of any system monitor written in Python.
import psutil
# Overall memory
mem = psutil.virtual_memory()
print(f"Total: {mem.total / (1024**3):.1f} GB")
print(f"Used: {mem.used / (1024**3):.1f} GB ({mem.percent:.1f}%)")
print(f"Free: {mem.available / (1024**3):.1f} GB")
# Per-process
for proc in psutil.process_iter(["pid", "name", "memory_info"]):
try:
rss_mb = proc.info["memory_info"].rss / (1024 ** 2)
print(f"{proc.info['pid']:>6} {proc.info['name']:<25} {rss_mb:>8.1f} MB")
except (psutil.NoSuchProcess, psutil.AccessDenied):
continueHTTP Client — requests
Fetch external data (e.g., send alerts via webhook, pull version info).
import requests
def send_slack_alert(webhook_url: str, message: str) -> bool:
payload = {"text": message}
resp = requests.post(webhook_url, json=payload, timeout=5)
resp.raise_for_status() # raises HTTPError for 4xx/5xx
return TrueEnvironment Variables
Keep secrets and deployment-specific config out of source code.
import os
SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL", "")
POLL_INTERVAL = int(os.environ.get("POLL_INTERVAL_S", "1"))
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
if not SLACK_WEBHOOK:
raise RuntimeError("SLACK_WEBHOOK_URL environment variable is not set")Use python-dotenv in development to load a .env file automatically.
Retry / Backoff
Transient network failures are normal. Retry with exponential backoff.
import time, requests
from requests.exceptions import RequestException
def post_with_retry(url: str, payload: dict, max_retries: int = 3) -> dict:
wait = 1.0
for attempt in range(1, max_retries + 1):
try:
resp = requests.post(url, json=payload, timeout=5)
resp.raise_for_status()
return resp.json()
except RequestException as exc:
if attempt == max_retries:
raise
print(f"Attempt {attempt} failed: {exc}. Retrying in {wait}s…")
time.sleep(wait)
wait *= 2 # exponential backoffThe tenacity library provides a decorator-based version of this pattern for production use.
Streaming Responses
When the API response is large (e.g., an LLM generating a long report), stream it instead of waiting.
import requests
def stream_report(url: str) -> str:
lines = []
with requests.get(url, stream=True, timeout=30) as resp:
resp.raise_for_status()
for chunk in resp.iter_lines():
if chunk:
line = chunk.decode()
lines.append(line)
print(line, flush=True) # live output
return "\n".join(lines)Conversation History
Maintain a rolling history buffer for LLM-powered chat assistants embedded in the manager.
from collections import deque
from dataclasses import dataclass, field
from typing import List
@dataclass
class Message:
role: str # "user" | "assistant" | "system"
content: str
class ConversationHistory:
def __init__(self, max_turns: int = 20):
self._history: deque[Message] = deque(maxlen=max_turns * 2)
def add(self, role: str, content: str) -> None:
self._history.append(Message(role, content))
def as_list(self) -> List[dict]:
return [{"role": m.role, "content": m.content} for m in self._history]asyncio + httpx — Non-Blocking I/O
Poll multiple endpoints concurrently without spinning up threads.
import asyncio
import httpx
async def fetch_ram_api(client: httpx.AsyncClient, host: str) -> dict:
resp = await client.get(f"http://{host}/api/ram", timeout=5)
return resp.json()
async def poll_all_hosts(hosts: list[str]) -> list[dict]:
async with httpx.AsyncClient() as client:
tasks = [fetch_ram_api(client, h) for h in hosts]
return await asyncio.gather(*tasks)
# Entry point
results = asyncio.run(poll_all_hosts(["host1", "host2", "host3"]))Typed API Models
Use dataclasses (or pydantic for validation) to define the shape of API responses.
from dataclasses import dataclass
@dataclass
class RamApiResponse:
host: str
total_mb: float
used_mb: float
percent: float
timestamp: str
def parse_response(raw: dict) -> RamApiResponse:
return RamApiResponse(**raw)File I/O Reports
Write a formatted text report to disk.
from pathlib import Path
from datetime import datetime
def write_report(snapshots: list, output_dir: Path) -> Path:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
report_path = output_dir / f"ram_report_{ts}.txt"
lines = [f"RAM Report — {ts}", "=" * 50]
for snap in sorted(snapshots, key=lambda s: s.rss_mb, reverse=True):
lines.append(f"{snap.name:<25} {snap.rss_mb:>8.1f} MB {snap.pct:>6.1f}%")
report_path.write_text("\n".join(lines))
return report_pathReal-World Component Flow
flowchart TD
A[psutil reads system RAM] --> B[ProcessSnapshot objects]
B --> C{Alert threshold crossed?}
C -->|Yes| D[post_with_retry to Slack webhook]
C -->|No| E[Append to history buffer]
D --> F{Retry needed?}
F -->|Yes| G[Exponential backoff]
G --> D
F -->|No| H[Log success]
E --> I[write_report to disk]
I --> J[CSV / text file saved]Async vs Sync Decision
flowchart LR
A[Task] --> B{I/O bound?}
B -->|Yes — network / disk| C{Many concurrent tasks?}
B -->|No — CPU bound| D[Multiprocessing or sync]
C -->|Yes| E[asyncio + httpx]
C -->|No| F[requests — sync is fine]
E --> G[asyncio.gather for parallel fetches]
F --> H[Simple sequential calls]Key Takeaways
psutilprovides cross-platform access to system memory data with minimal setup.requestshandles HTTP; always set atimeoutand callraise_for_status().- Environment variables keep secrets out of code;
os.environ.get(key, default)is safe for optional values. - Retry with exponential backoff makes the manager resilient to transient network failures.
- Streaming (
iter_lines()) handles large or incremental responses without buffering the full body. - A
deque(maxlen=N)is the cleanest rolling conversation history buffer. asyncio+httpxenable concurrent polling of multiple hosts without threading complexity.- Typed API models (
@dataclass) enforce response shape and make downstream code self-documenting.