← Home

145. Requests + Os.Environ (App V0.6)

RAM Manager v0.6 — AI advice via LLM API

145. Requests + Os.Environ (App V0.6)

The requests Pattern

import os, requests
from dotenv import load_dotenv

load_dotenv()

def call_api(prompt: str) -> str:
    api_key = os.environ.get('ANTHROPIC_API_KEY')
    if not api_key:
        raise EnvironmentError('ANTHROPIC_API_KEY not set')

    response = requests.post(
        'https://api.anthropic.com/v1/messages',
        json={
            'model': 'claude-haiku-4-5-20251001',
            'max_tokens': 200,
            'messages': [{'role': 'user', 'content': prompt}],
        },
        headers={
            'x-api-key': api_key,
            'anthropic-version': '2023-06-01',
        },
        timeout=30,       # don't hang forever
    )
    response.raise_for_status()
    return response.json()['content'][0]['text']

With that background, Aryan wires up the LLM call:

def ask_llm(snapshot: dict, model: str) -> str:
    api_key = os.environ.get('ANTHROPIC_API_KEY')
    if not api_key:
        raise EnvironmentError('ANTHROPIC_API_KEY not set')
    pct = snapshot['percent']
    top = snapshot['processes'][0]['name'] if snapshot['processes'] else 'none'
    prompt = f"RAM at {pct:.1f}%. Top process: {top}. Give one brief suggestion."
    response = requests.post(
        'https://api.anthropic.com/v1/messages',
        json={'model': model, 'max_tokens': 200,
              'messages': [{'role': 'user', 'content': prompt}]},
        headers={'x-api-key': api_key, 'anthropic-version': '2023-06-01'},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()['content'][0]['text']

He runs it. The LLM says: “Chrome is using 2.1 GB. Consider closing unused tabs or restarting the browser to reclaim memory.”

In forty lines of Python. Direct HTTP. No SDK. Just requests.

📺 YouTube search: “building with Claude API python tutorial” — you’ll be able to follow along start to finish after these exercises.

💡 Fun fact: The requests library was created by Kenneth Reitz in 2011 because Python’s built-in urllib required 7+ lines of boilerplate just to make a GET request. requests reduced that to one line. It became one of the most downloaded Python packages ever — over 300 million downloads per month. It’s so beloved that Python’s own documentation says “Requests is ready for the demands of building robust and reliable HTTP–speaking applications.”

⚠️ Watch out: If you forget response.raise_for_status(), a 401 Unauthorized or 400 Bad Request silently returns. Your code tries response.json()['content'][0]['text'] and crashes with KeyError — but the real cause is an API auth failure, not a code bug. Always call raise_for_status() immediately after requests.post().

🤔 Think about it: The solution uses os.environ.get('ANTHROPIC_API_KEY') and raises EnvironmentError if missing. Why is a custom error message like 'ANTHROPIC_API_KEY not set. Add to .env file.' more helpful than just letting KeyError propagate? How would a teammate diagnose a KeyError: 'ANTHROPIC_API_KEY' versus your custom message?


🤖 Aryan wires up the Anthropic API. The RAM manager now sends a snapshot to Claude and prints the advice. He hardcodes the API key, forgets raise_for_status(), and builds the prompt by concatenating strings instead of using an f-string.

Learning objectives

  • Store API keys in .env, load with load_dotenv()
  • Build prompts with f-strings and join()
  • Always call raise_for_status() and set timeout=
  • Handle EnvironmentError and HTTPError gracefully

Key concepts

  • os.environ.get(‘KEY’) — safe key lookup
  • load_dotenv() — loads .env into environment
  • requests.post(json=, headers=, timeout=) — POST request
  • raise_for_status() — raise on 4xx/5xx
  • response.json()[‘content’][0][‘text’] — extract LLM response

Try it

Concept detail

App v0.6 — LLM API call

The RAM manager can now ask Claude for advice:

$ python ram_manager.py
RAM: 87.3%  (13.97/16.0 GB)

AI advice:
Your RAM is critically high at 87%. I recommend:
1. Quit Chrome — it's using 1.8 GB. Close unused tabs.
2. Python script (421) using 670 MB — check if it's still needed.
3. Consider adding more RAM or enabling swap if this is frequent.

The API call

response = requests.post(
    'https://api.anthropic.com/v1/messages',
    json={
        'model': 'claude-haiku-4-5-20251001',
        'max_tokens': 300,
        'messages': [{'role': 'user', 'content': prompt}],
    },
    headers={
        'x-api-key': api_key,
        'anthropic-version': '2023-06-01',
        'content-type': 'application/json',
    },
    timeout=30,
)
response.raise_for_status()
text = response.json()['content'][0]['text']

Solution

import os
import psutil
import requests
from pathlib import Path
from datetime import datetime
from dotenv import load_dotenv

load_dotenv()

API_URL = 'https://api.anthropic.com/v1/messages'

def take_snapshot(n: int = 10) -> dict:
    mem = psutil.virtual_memory()
    processes = []
    for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
        try:
            rss_mb = proc.info['memory_info'].rss / 1e6
            processes.append({'name': proc.info['name'], 'pid': proc.info['pid'], 'rss_mb': rss_mb})
        except (psutil.NoSuchProcess, psutil.AccessDenied):
            pass
    return {
        'percent': mem.percent,
        'used_gb': mem.used / 1e9,
        'total_gb': mem.total / 1e9,
        'processes': sorted(processes, key=lambda p: p['rss_mb'], reverse=True)[:n],
        'timestamp': datetime.now().isoformat(),
    }

def build_prompt(snapshot: dict) -> str:
    lines = [
        f"RAM usage: {snapshot['percent']:.1f}% "
        f"({snapshot['used_gb']:.1f}/{snapshot['total_gb']:.1f} GB)",
        '',
        'Top processes by RAM:',
    ]
    for p in snapshot['processes'][:5]:
        lines.append(f"  {p['name']:<20} PID {p['pid']:<8} {p['rss_mb']:.0f} MB")
    lines.append('\nWhat should I do to reduce RAM usage? Be concise.')
    return '\n'.join(lines)

def ask_llm(snapshot: dict) -> str:
    api_key = os.environ.get('ANTHROPIC_API_KEY')
    if not api_key:
        raise EnvironmentError('ANTHROPIC_API_KEY not set. Add to .env file.')

    prompt = build_prompt(snapshot)
    payload = {
        'model': 'claude-haiku-4-5-20251001',
        'max_tokens': 300,
        'messages': [{'role': 'user', 'content': prompt}],
    }
    headers = {
        'x-api-key': api_key,
        'anthropic-version': '2023-06-01',
        'content-type': 'application/json',
    }
    response = requests.post(API_URL, json=payload, headers=headers, timeout=30)
    response.raise_for_status()
    return response.json()['content'][0]['text']

def main():
    snapshot = take_snapshot()
    print(f"RAM: {snapshot['percent']:.1f}%  "
          f"({snapshot['used_gb']:.1f}/{snapshot['total_gb']:.1f} GB)\n")
    try:
        advice = ask_llm(snapshot)
        print('AI advice:')
        print(advice)
    except EnvironmentError as e:
        print(f'Skipping AI advice: {e}')
    except requests.HTTPError as e:
        print(f'API error: {e.response.status_code}')

if __name__ == '__main__':
    main()

Tests

import os
import pytest
from unittest.mock import patch, MagicMock

def test_api_key_not_hardcoded():
    import inspect
    src = inspect.getsource(ask_llm)
    assert 'sk-ant' not in src, 'Never hardcode API keys in source code'

def test_ask_llm_raises_without_key():
    with patch.dict(os.environ, {}, clear=True):
        os.environ.pop('ANTHROPIC_API_KEY', None)
        snap = {'percent': 72.0, 'used_gb': 11.6, 'total_gb': 16.0,
                'processes': [], 'timestamp': ''}
        with pytest.raises(EnvironmentError):
            ask_llm(snap)

def test_ask_llm_calls_raise_for_status():
    import inspect
    src = inspect.getsource(ask_llm)
    assert 'raise_for_status' in src

def test_ask_llm_has_timeout():
    import inspect
    src = inspect.getsource(ask_llm)
    assert 'timeout' in src

def test_build_prompt_contains_percent():
    snap = {'percent': 72.5, 'used_gb': 11.6, 'total_gb': 16.0,
            'processes': [{'name': 'chrome', 'pid': 812, 'rss_mb': 1800.0}]}
    prompt = build_prompt(snap)
    assert '72.5' in prompt
    assert 'chrome' in prompt

def test_build_prompt_uses_fstring_not_concatenation():
    import inspect
    src = inspect.getsource(build_prompt)
    # Rough check: should use f-strings not + string joins for main data
    assert "f'" in src or 'f"' in src, 'Use f-strings for prompt building'

def test_ask_llm_mock_success():
    mock_resp = MagicMock()
    mock_resp.json.return_value = {'content': [{'text': 'Kill chrome.'}]}
    mock_resp.raise_for_status = MagicMock()
    snap = {'percent': 85.0, 'used_gb': 13.6, 'total_gb': 16.0,
            'processes': [{'name': 'chrome', 'pid': 812, 'rss_mb': 1800.0}],
            'timestamp': ''}
    with patch('requests.post', return_value=mock_resp):
        with patch.dict(os.environ, {'ANTHROPIC_API_KEY': 'test-key'}):
            result = ask_llm(snap)
    assert result == 'Kill chrome.'

Resources