GitHub API Client

The GitHubAPIClient provides a typed, feature-rich wrapper around the GitHub REST and GraphQL APIs with built-in rate limit handling, pagination support, and GitHub Enterprise Server (GHES) compatibility.

Features

  • Typed API access: Full type hints for better IDE support and type safety

  • Automatic rate limit handling: Detects rate limits and waits automatically (configurable)

  • Exponential backoff: Retries failed requests with configurable backoff

  • Conditional requests: ETag support for efficient caching

  • GHES support: Custom base URL for GitHub Enterprise Server

  • Pagination helpers: Simplified iteration over paginated results

  • GraphQL support: Execute GraphQL queries with typed responses

Basic Usage

Initialize the Client

from github_action_toolkit import GitHubAPIClient

# Using environment variable (GITHUB_TOKEN)
client = GitHubAPIClient()

# Or with explicit token
client = GitHubAPIClient(token="ghp_your_token_here")

# For GitHub Enterprise Server
client = GitHubAPIClient(
    token="ghp_your_token",
    base_url="https://github.mycompany.com/api/v3"
)

Get Repository Information

# Get a repository
repo = client.get_repo("owner/repo")
print(f"Repository: {repo.full_name}")
print(f"Stars: {repo.stargazers_count}")
print(f"Language: {repo.language}")

Get User Information

# Get authenticated user
user = client.get_user()
print(f"Logged in as: {user.login}")

# Get specific user
user = client.get_user("octocat")
print(f"User: {user.name}")

Get Organization Information

org = client.get_organization("github")
print(f"Organization: {org.name}")
print(f"Members: {org.public_members_count}")

Pagination

The client provides a convenient method to iterate through paginated results:

# Paginate through issues
repo = client.get_repo("owner/repo")
for issue in client.paginate(repo.get_issues):
    print(f"Issue #{issue.number}: {issue.title}")

# Paginate with parameters
for issue in client.paginate(repo.get_issues, state="closed", labels=["bug"]):
    print(f"Closed bug #{issue.number}: {issue.title}")

# Paginate through pull requests
for pr in client.paginate(repo.get_pulls, state="open"):
    print(f"PR #{pr.number}: {pr.title}")

Rate Limit Handling

The client automatically handles rate limits:

# Automatic waiting (default behavior)
client = GitHubAPIClient(token="ghp_token", rate_limit_wait=True)

# Without automatic waiting (raises RateLimitError)
client = GitHubAPIClient(token="ghp_token", rate_limit_wait=False)

# Check current rate limit status
rate_limit = client.get_rate_limit()
print(f"Core API: {rate_limit['core']['remaining']}/{rate_limit['core']['limit']}")
print(f"Search API: {rate_limit['search']['remaining']}/{rate_limit['search']['limit']}")
print(f"GraphQL API: {rate_limit['graphql']['remaining']}/{rate_limit['graphql']['limit']}")

Raw HTTP Requests

For advanced use cases, you can make raw HTTP requests:

# Simple GET request
response = client.request("GET", "/user/repos")
repos = response.json()

# POST request with data
response = client.request(
    "POST",
    "/repos/owner/repo/issues",
    json={
        "title": "Issue title",
        "body": "Issue description",
        "labels": ["bug"]
    }
)
issue = response.json()

Conditional Requests with ETags

Use ETags to reduce bandwidth and API calls:

# First request - gets data and caches ETag
response1 = client.request("GET", "/repos/owner/repo", use_etag=True)
data1 = response1.json()

# Second request - uses cached data if not modified (304)
response2 = client.request("GET", "/repos/owner/repo", use_etag=True)
data2 = response2.json()  # Returns cached data if unchanged

GraphQL Queries

Execute GraphQL queries for more efficient data fetching:

# Simple query
query = """
query {
  viewer {
    login
    name
    email
  }
}
"""
result = client.graphql(query)
print(f"User: {result['viewer']['login']}")

# Query with variables
query = """
query($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    name
    description
    stargazerCount
    issues(first: 5, states: OPEN) {
      nodes {
        title
        number
      }
    }
  }
}
"""
variables = {"owner": "github", "name": "hub"}
result = client.graphql(query, variables=variables)
repo_data = result["repository"]
print(f"Stars: {repo_data['stargazerCount']}")
for issue in repo_data["issues"]["nodes"]:
    print(f"Issue #{issue['number']}: {issue['title']}")

Configuration Options

Retry Strategy

# Configure retry behavior
client = GitHubAPIClient(
    token="ghp_token",
    max_retries=5,           # Maximum retry attempts
    backoff_factor=2.0       # Exponential backoff multiplier
)

Error Handling

from github_action_toolkit import GitHubAPIClient, RateLimitError, APIError

client = GitHubAPIClient(token="ghp_token", rate_limit_wait=False)

try:
    repo = client.get_repo("owner/repo")
except RateLimitError as e:
    print(f"Rate limit exceeded. Reset at: {e.reset_time}")
except APIError as e:
    print(f"API error ({e.status_code}): {e}")

Advanced Usage

Access Underlying PyGithub Instance

For features not directly exposed, you can access the underlying PyGithub client:

# Access PyGithub directly
pygithub_client = client.github

# Use any PyGithub method
gists = pygithub_client.get_gists()
for gist in gists:
    print(gist.description)

Custom Headers

# Add custom headers to requests
response = client.request(
    "GET",
    "/repos/owner/repo/contents/file.txt",
    headers={
        "Accept": "application/vnd.github.v3.raw"
    }
)
content = response.text

Best Practices

  1. Reuse client instances: Create one client and reuse it across your application

  2. Use pagination: Don’t fetch all results at once for large datasets

  3. Enable rate limit waiting: Set rate_limit_wait=True for long-running scripts

  4. Use GraphQL for complex queries: Fetch multiple related resources in one request

  5. Use conditional requests: Enable ETags for frequently accessed, rarely changing data

  6. Handle errors gracefully: Catch RateLimitError and APIError appropriately

Examples

Check if Repository Exists

from github_action_toolkit import GitHubAPIClient, APIError

client = GitHubAPIClient()

try:
    repo = client.get_repo("owner/repo")
    print(f"Repository exists: {repo.full_name}")
except APIError as e:
    if e.status_code == 404:
        print("Repository not found")
    else:
        raise

Create an Issue

client = GitHubAPIClient()
repo = client.get_repo("owner/repo")

issue = repo.create_issue(
    title="Bug report",
    body="Description of the bug",
    labels=["bug", "high-priority"]
)
print(f"Created issue #{issue.number}: {issue.html_url}")

List Open Pull Requests

client = GitHubAPIClient()
repo = client.get_repo("owner/repo")

print("Open Pull Requests:")
for pr in client.paginate(repo.get_pulls, state="open"):
    print(f"  PR #{pr.number}: {pr.title}")
    print(f"    Author: {pr.user.login}")
    print(f"    Branch: {pr.head.ref}{pr.base.ref}")
    print()

Monitor Repository Activity

import time
from github_action_toolkit import GitHubAPIClient

client = GitHubAPIClient(rate_limit_wait=True)

# Monitor new issues every 5 minutes
last_check = time.time()

while True:
    repo = client.get_repo("owner/repo")
    
    for issue in client.paginate(repo.get_issues, since=last_check):
        print(f"New issue: #{issue.number} - {issue.title}")
    
    last_check = time.time()
    time.sleep(300)  # 5 minutes

API Reference

See the module docstrings for complete API documentation:

from github_action_toolkit import GitHubAPIClient

help(GitHubAPIClient)

API Rate Limiting

Automatic Rate Limit Handling

from github_action_toolkit import GitHubAPIClient, info

client = GitHubAPIClient()

# Automatically handles rate limits with retry
with client.with_rate_limit_handling(wait=True):
    repos = client.paginate(lambda: client.github.get_user().get_repos())
    for repo in repos:
        info(f'Processing {repo.full_name}')
        # API calls here are protected

Manual Rate Limit Checking

from github_action_toolkit import GitHubAPIClient, warning
import time

client = GitHubAPIClient()

def check_rate_limit():
    """Check and warn about rate limit."""
    rate_limit = client.get_rate_limit()
    remaining = rate_limit.core.remaining
    
    if remaining < 100:
        reset_time = rate_limit.core.reset.timestamp()
        wait_seconds = reset_time - time.time()
        warning(
            f'Only {remaining} API calls remaining. '
            f'Resets in {wait_seconds/60:.1f} minutes.',
            title='Rate Limit Low'
        )