CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-gidgethub

An asynchronous GitHub API library designed as a sans-I/O library for GitHub API access

Pending
Overview
Eval results
Files

apps.mddocs/

GitHub Apps

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.

Capabilities

JWT Token Generation

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.
    """

Installation Access Tokens

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.
    """

Usage Examples

Basic GitHub App Authentication

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())

JWT Token Usage

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())

Complete GitHub App Workflow

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())

Webhook Event Handling with Apps

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"
            }
        )

Error Handling

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())

Private Key Management

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}")

GitHub App Permissions

When creating a GitHub App, you need to configure permissions for what the app can access:

Repository Permissions

  • Contents: Read/write repository files
  • Issues: Create and manage issues
  • Pull requests: Create and manage PRs
  • Metadata: Access repository metadata
  • Deployments: Create deployments
  • Statuses: Create commit statuses
  • Checks: Create check runs

Organization Permissions

  • Members: Access organization membership
  • Administration: Manage organization settings

Account Permissions

  • Email addresses: Access user email addresses
  • Profile: Access user profile information

Types

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

docs

actions.md

api-client.md

apps.md

exceptions.md

http-implementations.md

index.md

routing.md

sansio.md

tile.json