API Integration with AI

Practical Exercise 7

Hands-On

Use AI to integrate third-party APIs efficiently

Practical Objectives

1

Have AI read and understand API documentation

2

Generate type-safe API client code

3

Implement proper error handling

4

Write integration tests with mocking

Task 1: Feed AI the API Documentation

We'll integrate with a Weather API. Here's the spec:

# Weather API Documentation

## Endpoints

### GET /weather/current
Get current weather for a location.

**Parameters:**
- city (string, required): City name
- units (string, optional): "metric" or "imperial", default: "metric"

**Response 200:**
{
  "city": "Paris",
  "temperature": 22.5,
  "humidity": 65,
  "description": "partly cloudy",
  "wind_speed": 12.3
}

**Response 404:** City not found
**Response 429:** Rate limit exceeded (100 req/hour)

Task 2: Generate API Client

API Client PromptGenerate a Python API client for this Weather API: [paste API documentation] Requirements: 1. Use httpx for async HTTP requests 2. Create Pydantic models for request/response validation 3. Implement retry logic (3 retries with exponential backoff) 4. Handle all error codes (404, 429, 5xx) 5. Add type hints everywhere 6. Include logging for debugging 7. Use environment variable for API key Structure: - models.py: Pydantic models - client.py: WeatherClient class - exceptions.py: Custom exceptions

Task 3: Define Data Models

AI should generate models like this:

# models.py
from pydantic import BaseModel, Field
from enum import Enum

class Units(str, Enum):
    METRIC = "metric"
    IMPERIAL = "imperial"

class WeatherResponse(BaseModel):
    city: str
    temperature: float
    humidity: int = Field(ge=0, le=100)
    description: str
    wind_speed: float = Field(ge=0)

class WeatherRequest(BaseModel):
    city: str = Field(min_length=1, max_length=100)
    units: Units = Units.METRIC
Prompt: "Add validation - temperature must be between -100 and 60°C, city must be alphanumeric with spaces."

Task 4: Implement Error Handling

Error Handling PromptAdd comprehensive error handling to the WeatherClient: 1. Create custom exceptions: - WeatherAPIError (base) - CityNotFoundError (404) - RateLimitError (429) - WeatherServiceError (5xx) 2. Implement retry logic: - Retry on 5xx and network errors - Don't retry on 4xx - Exponential backoff: 1s, 2s, 4s 3. Add circuit breaker: - Open after 5 consecutive failures - Half-open after 30 seconds - Close after successful request Include proper logging for each scenario.

Task 5: Complete Client Implementation

# client.py
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential

class WeatherClient:
    def __init__(self, api_key: str, base_url: str = "https://api.weather.com"):
        self.api_key = api_key
        self.base_url = base_url
        self._client = httpx.AsyncClient(
            base_url=base_url,
            headers={"X-API-Key": api_key},
            timeout=10.0
        )

    @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, max=10))
    async def get_current_weather(self, city: str, units: Units = Units.METRIC) -> WeatherResponse:
        response = await self._client.get(
            "/weather/current",
            params={"city": city, "units": units.value}
        )

        if response.status_code == 404:
            raise CityNotFoundError(f"City not found: {city}")
        if response.status_code == 429:
            raise RateLimitError("Rate limit exceeded")
        response.raise_for_status()

        return WeatherResponse(**response.json())

Task 6: Write Integration Tests

Test Generation PromptGenerate pytest tests for WeatherClient: Test cases: 1. Successful weather fetch 2. City not found (404) raises CityNotFoundError 3. Rate limit (429) raises RateLimitError 4. Server error (500) triggers retry 5. Network timeout triggers retry 6. Invalid response data raises validation error Use: - pytest-asyncio for async tests - respx for mocking HTTP requests - Fixtures for client setup Mock the external API - don't make real requests.

Task 6: Example Test Code

import pytest
import respx
from httpx import Response

@pytest.fixture
def weather_client():
    return WeatherClient(api_key="test-key")

@pytest.mark.asyncio
@respx.mock
async def test_get_current_weather_success(weather_client):
    respx.get("https://api.weather.com/weather/current").mock(
        return_value=Response(200, json={
            "city": "Paris",
            "temperature": 22.5,
            "humidity": 65,
            "description": "sunny",
            "wind_speed": 10.0
        })
    )

    result = await weather_client.get_current_weather("Paris")

    assert result.city == "Paris"
    assert result.temperature == 22.5

@pytest.mark.asyncio
@respx.mock
async def test_city_not_found_raises_error(weather_client):
    respx.get("https://api.weather.com/weather/current").mock(
        return_value=Response(404)
    )

    with pytest.raises(CityNotFoundError):
        await weather_client.get_current_weather("FakeCity")

Deliverables Checklist

Models

  • Pydantic models defined
  • Validation rules added
  • Enums for fixed values

Client

  • Async HTTP with httpx
  • Retry with backoff
  • Proper timeout handling

Errors

  • Custom exception hierarchy
  • All status codes handled
  • Logging implemented

Tests

  • Mocked HTTP requests
  • Success and error paths
  • All tests passing

Integration Complete!

You've built a production-ready API client

Course Complete - Keep practicing AIDD!

Slide Overview