Skip to content

Chapter 20 · Capstone: GitHub API Client ​

Chapter goals:Comprehensively apply learned knowledge to build a complete GitHub API client.

20.1 Project Structure ​

github_client/
├── client.py        # Main client
├── exceptions.py    # Custom exceptions
├── config.py        # Configuration management
└── tests/
    └── test_client.py

20.2 Exception Definitions ​

python
# exceptions.py
class GitHubAPIError(Exception):
    """Generic GitHub API error"""
    pass

class RateLimitError(GitHubAPIError):
    """Rate limit error"""
    def __init__(self, reset_time: int):
        self.reset_time = reset_time
        super().__init__(f'Rate limited, resets in {reset_time} seconds')

class AuthenticationError(GitHubAPIError):
    """Authentication failure"""
    pass

20.3 Client Implementation ​

python
# client.py
import httpx
from typing import Optional, Dict, List
from .exceptions import GitHubAPIError, RateLimitError, AuthenticationError

class GitHubClient:
    def __init__(self, token: Optional[str] = None):
        self.base_url = 'https://api.github.com'
        self.client = httpx.Client(
            base_url=self.base_url,
            headers={'Accept': 'application/vnd.github.v3+json'},
            timeout=10.0,
            limits=httpx.Limits(max_connections=50)
        )
        if token:
            self.client.headers['Authorization'] = f'token {token}'
    
    def get_user(self, username: str) -> Dict:
        resp = self.client.get(f'/users/{username}')
        self._handle_response(resp)
        return resp.json()
    
    def list_repos(self, username: str, page: int = 1, per_page: int = 30) -> List[Dict]:
        params = {'page': page, 'per_page': per_page}
        resp = self.client.get(f'/users/{username}/repos', params=params)
        self._handle_response(resp)
        return resp.json()
    
    def search_repos(self, query: str, sort: str = 'stars') -> Dict:
        params = {'q': query, 'sort': sort, 'order': 'desc'}
        resp = self.client.get('/search/repositories', params=params)
        self._handle_response(resp)
        return resp.json()
    
    def _handle_response(self, resp: httpx.Response):
        if resp.status_code == 403 and 'rate limit' in resp.text.lower():
            reset = int(resp.headers.get('X-RateLimit-Reset', 0))
            raise RateLimitError(reset)
        elif resp.status_code == 401:
            raise AuthenticationError('Authentication failed')
        elif resp.status_code >= 400:
            raise GitHubAPIError(f'API error: {resp.status_code}')

20.4 Tests ​

python
# tests/test_client.py
import pytest
import respx
import httpx
from github_client import GitHubClient

@respx.mock
def test_get_user():
    respx.get("https://api.github.com/users/octocat").mock(
        return_value=httpx.Response(200, json={"login": "octocat"})
    )
    
    client = GitHubClient()
    user = client.get_user("octocat")
    assert user["login"] == "octocat"

@respx.mock
def test_rate_limit_error():
    respx.get("https://api.github.com/users/test").mock(
        return_value=httpx.Response(
            403,
            json={"message": "rate limit exceeded"},
            headers={"X-RateLimit-Reset": "1609459200"}
        )
    )
    
    client = GitHubClient()
    with pytest.raises(Exception) as exc_info:
        client.get_user("test")
    assert "rate limit" in str(exc_info.value).lower()

20.5 Usage Example ​

python
from github_client import GitHubClient

# Anonymous access (limited)
client = GitHubClient()
user = client.get_user('torvalds')
print(f'{user["name"]} has {user["public_repos"]} public repos')

# Authenticated access (higher limits)
client = GitHubClient(token='your-token')
repos = client.list_repos('github', per_page=10)
for repo in repos:
    print(f'{repo["name"]}: {repo["stargazers_count"]} stars')

20.6 Chapter Summary ​

  • Comprehensively apply Session, timeout, exception handling, async knowledge;
  • Encapsulate a unified error-handling layer;
  • Write complete test coverage.

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. What is the rate limit for unauthenticated GitHub API requests?

2. Which response header should you check when handling a 403 rate limit?

3. What does the respx.mock decorator do?

4. What features should a production-grade API client include?

🛠️ Hands-on Practice ​

  1. Add a get_issues() method to GitHubClient to query all issues for a user.
  2. Add caching functionality to return cached results for identical requests.
  3. Write a complete test suite covering both normal and error scenarios.