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.'