HTTP & APIs
Infrastructure automation means talking to APIs. Master HTTP clients and REST patterns.
httpx Basics
httpx is the modern choice - supports sync and async, HTTP/2, and is well-designed.
Installation
uv add httpx
Simple Requests
import httpx
# GET
response = httpx.get("https://api.example.com/endpoints")
print(response.status_code) # 200
print(response.json()) # Parsed JSON
# POST with JSON body
response = httpx.post(
"https://api.example.com/endpoints",
json={"mac": "00:11:22:33:44:55", "group": "Employees"}
)
# PUT
response = httpx.put(
"https://api.example.com/endpoints/123",
json={"group": "Guests"}
)
# DELETE
response = httpx.delete("https://api.example.com/endpoints/123")
Response Handling
import httpx
response = httpx.get("https://api.example.com/data")
# Status
response.status_code # 200
response.is_success # True (2xx)
response.is_error # False
response.is_redirect # False
# Headers
response.headers # Headers dict
response.headers["content-type"]
# Content
response.text # String content
response.content # Bytes
response.json() # Parsed JSON
# Raise on error
response.raise_for_status() # Raises HTTPStatusError if 4xx/5xx
Client Sessions
Use a client for multiple requests - it reuses connections.
import httpx
# Context manager (recommended)
with httpx.Client() as client:
r1 = client.get("https://api.example.com/users")
r2 = client.get("https://api.example.com/endpoints")
# Connection closed automatically
# Reusable client
client = httpx.Client(
base_url="https://ise-01:9060",
timeout=30.0,
verify=False # Disable SSL verification (dev only)
)
try:
response = client.get("/ers/config/endpoint")
finally:
client.close()
Configuration
import httpx
client = httpx.Client(
base_url="https://ise-01:9060/ers/config",
timeout=httpx.Timeout(
connect=5.0,
read=30.0,
write=10.0,
pool=5.0
),
headers={
"Accept": "application/json",
"Content-Type": "application/json"
},
verify=False, # SSL verification
follow_redirects=True,
http2=True # Enable HTTP/2
)
# Request with client
response = client.get("/endpoint") # Full URL: https://ise-01:9060/ers/config/endpoint
Authentication
Basic Auth
import httpx
# Simple
response = httpx.get(
"https://api.example.com/data",
auth=("username", "password")
)
# With client
client = httpx.Client(
base_url="https://ise-01:9060/ers/config",
auth=("admin", "password")
)
Bearer Token
import httpx
# Manual header
response = httpx.get(
"https://api.example.com/data",
headers={"Authorization": "Bearer eyJhbGc..."}
)
# Custom auth class
class BearerAuth(httpx.Auth):
def __init__(self, token: str):
self.token = token
def auth_flow(self, request):
request.headers["Authorization"] = f"Bearer {self.token}"
yield request
client = httpx.Client(auth=BearerAuth("eyJhbGc..."))
API Key
import httpx
# Header-based
client = httpx.Client(
headers={"X-API-Key": "your-api-key"}
)
# Query parameter
response = httpx.get(
"https://api.example.com/data",
params={"api_key": "your-api-key"}
)
OAuth2 / Token Refresh
import httpx
from datetime import datetime, timedelta
class OAuth2Client:
def __init__(self, token_url: str, client_id: str, client_secret: str):
self.token_url = token_url
self.client_id = client_id
self.client_secret = client_secret
self.access_token: str | None = None
self.token_expiry: datetime | None = None
def get_token(self) -> str:
"""Get valid access token, refreshing if needed."""
if self._token_valid():
return self.access_token
response = httpx.post(
self.token_url,
data={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
expires_in = data.get("expires_in", 3600)
self.token_expiry = datetime.now() + timedelta(seconds=expires_in - 60)
return self.access_token
def _token_valid(self) -> bool:
return (
self.access_token is not None
and self.token_expiry is not None
and datetime.now() < self.token_expiry
)
# Usage
oauth = OAuth2Client(
token_url="https://auth.example.com/oauth/token",
client_id="my-client",
client_secret="secret"
)
client = httpx.Client(
headers={"Authorization": f"Bearer {oauth.get_token()}"}
)
REST Patterns
CRUD Operations
import httpx
from dataclasses import dataclass, asdict
@dataclass
class Endpoint:
mac: str
group: str
description: str = ""
class EndpointAPI:
def __init__(self, base_url: str, auth: tuple[str, str]):
self.client = httpx.Client(
base_url=base_url,
auth=auth,
headers={"Accept": "application/json", "Content-Type": "application/json"}
)
def list(self, page: int = 1, size: int = 100) -> list[dict]:
"""GET /endpoints"""
response = self.client.get("/endpoints", params={"page": page, "size": size})
response.raise_for_status()
return response.json()["resources"]
def get(self, id: str) -> dict | None:
"""GET /endpoints/{id}"""
response = self.client.get(f"/endpoints/{id}")
if response.status_code == 404:
return None
response.raise_for_status()
return response.json()
def create(self, endpoint: Endpoint) -> str:
"""POST /endpoints"""
response = self.client.post("/endpoints", json=asdict(endpoint))
response.raise_for_status()
# Return ID from Location header
return response.headers["Location"].split("/")[-1]
def update(self, id: str, endpoint: Endpoint) -> None:
"""PUT /endpoints/{id}"""
response = self.client.put(f"/endpoints/{id}", json=asdict(endpoint))
response.raise_for_status()
def delete(self, id: str) -> bool:
"""DELETE /endpoints/{id}"""
response = self.client.delete(f"/endpoints/{id}")
return response.status_code == 204
def close(self):
self.client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Usage
with EndpointAPI("https://ise-01:9060/ers/config", ("admin", "password")) as api:
endpoints = api.list()
endpoint = api.get("123")
new_id = api.create(Endpoint(mac="00:11:22:33:44:55", group="Employees"))
Pagination
import httpx
from typing import Iterator
def paginate(client: httpx.Client, url: str, page_size: int = 100) -> Iterator[dict]:
"""Iterate through paginated API results."""
page = 1
while True:
response = client.get(url, params={"page": page, "size": page_size})
response.raise_for_status()
data = response.json()
resources = data.get("resources", [])
for item in resources:
yield item
# Check if more pages
total = data.get("total", 0)
if page * page_size >= total:
break
page += 1
# Usage
client = httpx.Client(base_url="https://ise-01:9060/ers/config", auth=("admin", "pass"))
for endpoint in paginate(client, "/endpoint"):
print(endpoint["mac"])
Error Handling
import httpx
class APIError(Exception):
def __init__(self, status_code: int, message: str, response: httpx.Response):
self.status_code = status_code
self.message = message
self.response = response
super().__init__(f"{status_code}: {message}")
class APIClient:
def __init__(self, base_url: str, auth: tuple[str, str]):
self.client = httpx.Client(base_url=base_url, auth=auth)
def request(self, method: str, url: str, **kwargs) -> dict:
"""Make request with error handling."""
try:
response = self.client.request(method, url, **kwargs)
except httpx.ConnectError as e:
raise APIError(0, f"Connection failed: {e}", None)
except httpx.TimeoutException as e:
raise APIError(0, f"Request timed out: {e}", None)
if response.is_success:
return response.json() if response.content else {}
# Parse error message from response
try:
error_data = response.json()
message = error_data.get("message", error_data.get("error", str(error_data)))
except Exception:
message = response.text or f"HTTP {response.status_code}"
raise APIError(response.status_code, message, response)
def get(self, url: str, **kwargs) -> dict:
return self.request("GET", url, **kwargs)
def post(self, url: str, **kwargs) -> dict:
return self.request("POST", url, **kwargs)
# Usage
client = APIClient("https://ise-01:9060/ers/config", ("admin", "password"))
try:
data = client.get("/endpoint/invalid-id")
except APIError as e:
if e.status_code == 404:
print("Endpoint not found")
else:
print(f"API error: {e}")
Retry Logic
import httpx
import time
from functools import wraps
def retry(max_attempts: int = 3, backoff: float = 1.0, exceptions: tuple = (httpx.TransportError,)):
"""Decorator for retry with exponential backoff."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except exceptions as e:
last_error = e
if attempt < max_attempts - 1:
sleep_time = backoff * (2 ** attempt)
time.sleep(sleep_time)
raise last_error
return wrapper
return decorator
class APIClient:
@retry(max_attempts=3, backoff=1.0)
def get(self, url: str) -> dict:
response = self.client.get(url)
response.raise_for_status()
return response.json()
Complete API Client
import httpx
from dataclasses import dataclass
from typing import Iterator, Any
import time
@dataclass
class APIConfig:
base_url: str
username: str
password: str
verify_ssl: bool = True
timeout: float = 30.0
max_retries: int = 3
class ISEClient:
"""ISE ERS API client."""
def __init__(self, config: APIConfig):
self.config = config
self.client = httpx.Client(
base_url=f"{config.base_url}/ers/config",
auth=(config.username, config.password),
verify=config.verify_ssl,
timeout=config.timeout,
headers={
"Accept": "application/json",
"Content-Type": "application/json"
}
)
def _request(self, method: str, url: str, **kwargs) -> dict:
"""Make request with retry logic."""
last_error = None
for attempt in range(self.config.max_retries):
try:
response = self.client.request(method, url, **kwargs)
response.raise_for_status()
return response.json() if response.content else {}
except httpx.TransportError as e:
last_error = e
if attempt < self.config.max_retries - 1:
time.sleep(2 ** attempt)
except httpx.HTTPStatusError as e:
if e.response.status_code in (502, 503, 504):
last_error = e
if attempt < self.config.max_retries - 1:
time.sleep(2 ** attempt)
else:
raise
raise last_error
def get_endpoints(self, page_size: int = 100) -> Iterator[dict]:
"""Get all endpoints with pagination."""
page = 1
while True:
data = self._request("GET", "/endpoint", params={"page": page, "size": page_size})
resources = data.get("SearchResult", {}).get("resources", [])
for resource in resources:
# Get full endpoint details
endpoint = self._request("GET", f"/endpoint/{resource['id']}")
yield endpoint.get("ERSEndPoint", endpoint)
total = data.get("SearchResult", {}).get("total", 0)
if page * page_size >= total:
break
page += 1
def get_endpoint(self, id: str) -> dict | None:
"""Get single endpoint by ID."""
try:
response = self._request("GET", f"/endpoint/{id}")
return response.get("ERSEndPoint", response)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
return None
raise
def get_endpoint_by_mac(self, mac: str) -> dict | None:
"""Get endpoint by MAC address."""
data = self._request("GET", "/endpoint", params={"filter": f"mac.EQ.{mac}"})
resources = data.get("SearchResult", {}).get("resources", [])
if resources:
return self.get_endpoint(resources[0]["id"])
return None
def create_endpoint(self, mac: str, group: str, **kwargs) -> str:
"""Create new endpoint, return ID."""
payload = {
"ERSEndPoint": {
"mac": mac,
"groupId": self._get_group_id(group),
**kwargs
}
}
response = self.client.post("/endpoint", json=payload)
response.raise_for_status()
return response.headers["Location"].split("/")[-1]
def delete_endpoint(self, id: str) -> bool:
"""Delete endpoint by ID."""
response = self.client.delete(f"/endpoint/{id}")
return response.status_code == 204
def _get_group_id(self, group_name: str) -> str:
"""Look up group ID by name."""
data = self._request("GET", "/endpointgroup", params={"filter": f"name.EQ.{group_name}"})
resources = data.get("SearchResult", {}).get("resources", [])
if not resources:
raise ValueError(f"Group not found: {group_name}")
return resources[0]["id"]
def close(self):
self.client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
# Usage
config = APIConfig(
base_url="https://ise-01:9060",
username="admin",
password="password",
verify_ssl=False
)
with ISEClient(config) as client:
# List all endpoints
for endpoint in client.get_endpoints():
print(f"{endpoint['mac']} - {endpoint.get('groupId')}")
# Look up specific endpoint
ep = client.get_endpoint_by_mac("00:11:22:33:44:55")
if ep:
print(f"Found: {ep}")
Next Module
Async Programming - asyncio, aiohttp, concurrent patterns.