An asynchronous GitHub API library designed as a sans-I/O library for GitHub API access
—
Support for GitHub Apps authentication including JWT creation and installation access token retrieval for app-based integrations. GitHub Apps provide a more secure and scalable way to integrate with GitHub compared to OAuth tokens.
Create JSON Web Tokens (JWT) for GitHub App authentication.
def get_jwt(
*,
app_id: str,
private_key: str,
expiration: int = 10 * 60
) -> str:
"""
Construct the JWT (JSON Web Token) for GitHub App authentication.
Parameters:
- app_id: GitHub App ID (numeric string)
- private_key: RSA private key in PEM format
- expiration: Token expiration time in seconds (default: 600, max: 600)
Returns:
- JWT bearer token string
Note: JWT tokens are used to authenticate as the GitHub App itself,
not as an installation of the app.
"""Obtain installation access tokens for GitHub App installations.
async def get_installation_access_token(
gh: GitHubAPI,
*,
installation_id: str,
app_id: str,
private_key: str
) -> Dict[str, Any]:
"""
Obtain a GitHub App's installation access token.
Parameters:
- gh: GitHubAPI instance for making the request
- installation_id: Installation ID (numeric string)
- app_id: GitHub App ID (numeric string)
- private_key: RSA private key in PEM format
Returns:
- Dictionary containing:
- "token": Installation access token string
- "expires_at": ISO 8601 expiration datetime string
Note: Installation access tokens allow acting on behalf of the
app installation with permissions granted to the app.
"""import asyncio
from gidgethub.aiohttp import GitHubAPI
from gidgethub.apps import get_jwt, get_installation_access_token
import aiohttp
async def github_app_example():
# GitHub App credentials
app_id = "12345"
private_key = """-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"""
installation_id = "67890"
async with aiohttp.ClientSession() as session:
gh = GitHubAPI(session, "my-app/1.0")
# Get installation access token
token_response = await get_installation_access_token(
gh,
installation_id=installation_id,
app_id=app_id,
private_key=private_key
)
access_token = token_response["token"]
expires_at = token_response["expires_at"]
print(f"Token expires at: {expires_at}")
# Create new client with installation token
gh_installed = GitHubAPI(session, "my-app/1.0", oauth_token=access_token)
# Now you can act on behalf of the installation
repos = []
async for repo in gh_installed.getiter("/installation/repositories"):
repos.append(repo["name"])
print(f"App has access to {len(repos)} repositories")
asyncio.run(github_app_example())import asyncio
from gidgethub.aiohttp import GitHubAPI
from gidgethub.apps import get_jwt
import aiohttp
async def jwt_example():
app_id = "12345"
private_key = """-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...
-----END RSA PRIVATE KEY-----"""
# Generate JWT token
jwt_token = get_jwt(app_id=app_id, private_key=private_key)
async with aiohttp.ClientSession() as session:
gh = GitHubAPI(session, "my-app/1.0")
# Use JWT to authenticate as the app
app_info = await gh.getitem("/app", jwt=jwt_token)
print(f"App name: {app_info['name']}")
print(f"App owner: {app_info['owner']['login']}")
# Get app installations
installations = []
async for installation in gh.getiter("/app/installations", jwt=jwt_token):
installations.append({
"id": installation["id"],
"account": installation["account"]["login"],
"permissions": installation["permissions"]
})
print(f"App has {len(installations)} installations")
asyncio.run(jwt_example())import asyncio
from gidgethub.aiohttp import GitHubAPI
from gidgethub.apps import get_jwt, get_installation_access_token
import aiohttp
class GitHubAppClient:
def __init__(self, app_id: str, private_key: str):
self.app_id = app_id
self.private_key = private_key
self._jwt_token = None
self._installation_tokens = {}
def _get_jwt(self):
"""Get or refresh JWT token."""
# In production, you'd want to cache and refresh JWT tokens
return get_jwt(app_id=self.app_id, private_key=self.private_key)
async def get_installation_client(self, session, installation_id: str):
"""Get a GitHubAPI client for a specific installation."""
gh = GitHubAPI(session, "my-app/1.0")
# Get installation access token
token_response = await get_installation_access_token(
gh,
installation_id=installation_id,
app_id=self.app_id,
private_key=self.private_key
)
# Return client with installation token
return GitHubAPI(
session,
"my-app/1.0",
oauth_token=token_response["token"]
)
async def get_app_client(self, session):
"""Get a GitHubAPI client authenticated as the app."""
jwt_token = self._get_jwt()
return GitHubAPI(session, "my-app/1.0", jwt=jwt_token)
async def app_workflow_example():
app_client = GitHubAppClient(
app_id="12345",
private_key=open("app-private-key.pem").read()
)
async with aiohttp.ClientSession() as session:
# Get app-level client
app_gh = await app_client.get_app_client(session)
# List all installations
installations = []
async for installation in app_gh.getiter("/app/installations"):
installations.append(installation)
# Process each installation
for installation in installations:
installation_id = str(installation["id"])
account_name = installation["account"]["login"]
print(f"Processing installation for {account_name}")
# Get installation-specific client
install_gh = await app_client.get_installation_client(
session, installation_id
)
# List repositories for this installation
async for repo in install_gh.getiter("/installation/repositories"):
print(f" Repository: {repo['full_name']}")
# Example: Create an issue
await install_gh.post(
f"/repos/{repo['full_name']}/issues",
data={
"title": "Automated issue from GitHub App",
"body": "This issue was created by our GitHub App!"
}
)
# asyncio.run(app_workflow_example())from gidgethub.routing import Router
from gidgethub.apps import get_installation_access_token
import aiohttp
router = Router()
@router.register("installation", action="created")
async def handle_new_installation(event):
"""Handle new app installation."""
installation = event.data["installation"]
print(f"New installation: {installation['account']['login']}")
# You might want to store installation info in a database
# or perform initial setup tasks
@router.register("pull_request", action="opened")
async def auto_review_pr(event):
"""Automatically review pull requests."""
installation_id = str(event.data["installation"]["id"])
repo_name = event.data["repository"]["full_name"]
pr_number = event.data["pull_request"]["number"]
# Get installation token
async with aiohttp.ClientSession() as session:
gh = GitHubAPI(session, "pr-reviewer-app/1.0")
token_response = await get_installation_access_token(
gh,
installation_id=installation_id,
app_id="your_app_id",
private_key="your_private_key"
)
# Create installation client
install_gh = GitHubAPI(
session,
"pr-reviewer-app/1.0",
oauth_token=token_response["token"]
)
# Add review comment
await install_gh.post(
f"/repos/{repo_name}/pulls/{pr_number}/reviews",
data={
"body": "Thanks for the PR! Our automated review is complete.",
"event": "COMMENT"
}
)import asyncio
from gidgethub.aiohttp import GitHubAPI
from gidgethub.apps import get_installation_access_token
from gidgethub import HTTPException
import aiohttp
import jwt
async def robust_app_auth():
async with aiohttp.ClientSession() as session:
gh = GitHubAPI(session, "my-app/1.0")
try:
token_response = await get_installation_access_token(
gh,
installation_id="12345",
app_id="67890",
private_key="invalid_key"
)
except HTTPException as exc:
if exc.status_code == 401:
print("Authentication failed - check app ID and private key")
elif exc.status_code == 404:
print("Installation not found - check installation ID")
else:
print(f"HTTP error: {exc.status_code}")
except jwt.InvalidTokenError:
print("Invalid JWT token - check private key format")
except Exception as exc:
print(f"Unexpected error: {exc}")
asyncio.run(robust_app_auth())import os
from pathlib import Path
def load_private_key():
"""Load private key from various sources."""
# Option 1: Environment variable
if "GITHUB_PRIVATE_KEY" in os.environ:
return os.environ["GITHUB_PRIVATE_KEY"]
# Option 2: File path from environment
if "GITHUB_PRIVATE_KEY_PATH" in os.environ:
key_path = Path(os.environ["GITHUB_PRIVATE_KEY_PATH"])
return key_path.read_text()
# Option 3: Default file location
default_path = Path("github-app-key.pem")
if default_path.exists():
return default_path.read_text()
raise ValueError("GitHub App private key not found")
# Usage
try:
private_key = load_private_key()
app_client = GitHubAppClient("12345", private_key)
except ValueError as exc:
print(f"Configuration error: {exc}")When creating a GitHub App, you need to configure permissions for what the app can access:
from typing import Any, Dict
from gidgethub.abc import GitHubAPI
# JWT payload structure (internal)
JWTPayload = Dict[str, Any] # {"iat": int, "exp": int, "iss": str}
# Installation access token response
InstallationTokenResponse = Dict[str, Any] # {"token": str, "expires_at": str}Install with Tessl CLI
npx tessl i tessl/pypi-gidgethub