Skip to content

API Documentation

github_bot_api

Event dataclass

Represents a GitHub webhook event.

Source code in github_bot_api/event.py
@dataclass
class Event:
    """
    Represents a GitHub webhook event.
    """

    name: str
    """ The name of the event. Could be `pull_request`, for example."""

    delivery_id: str
    """The delivery ID of the event."""

    signature: t.Optional[str]
    """The signature of the event. Will only be set if a Webhook-secret is configured on the
    client side (e.g. in [`Webhook.secret`][github_bot_api.webhook.Webhook] / if the *webhook_secret* parameter is
    passed to [`accept_event()`][github_bot_api.event.accept_event])."""

    user_agent: str
    """The user agent invoking the webhook."""

    payload: t.Dict[str, t.Any]
    """The event payload."""
delivery_id instance-attribute
delivery_id: str

The delivery ID of the event.

name instance-attribute
name: str

The name of the event. Could be pull_request, for example.

payload instance-attribute
payload: Dict[str, Any]

The event payload.

signature instance-attribute
signature: Optional[str]

The signature of the event. Will only be set if a Webhook-secret is configured on the client side (e.g. in Webhook.secret / if the webhook_secret parameter is passed to accept_event()).

user_agent instance-attribute
user_agent: str

The user agent invoking the webhook.

GithubApp dataclass

Represents a GitHub application and all the required details.

Source code in github_bot_api/app.py
@dataclasses.dataclass
class GithubApp:
    """
    Represents a GitHub application and all the required details.
    """

    PUBLIC_GITHUB_V3_API_URL = "https://api.github.com"

    user_agent: str
    """User agent of the application. This will be respected in #get_user_agent()."""

    app_id: int
    """GitHub Application ID."""

    private_key: str
    """RSA private key to sign the JWT with."""

    v3_api_url: str = PUBLIC_GITHUB_V3_API_URL
    """GitHub API base URL. Defaults to the public GitHub API."""

    def __post_init__(self):
        self._jwt_supplier = JwtSupplier(self.app_id, self.private_key)
        self._lock = threading.Lock()
        self._installation_tokens: t.Dict[int, InstallationTokenSupplier] = {}

    def _get_base_github_client_settings(self) -> GithubClientSettings:
        return GithubClientSettings(self.v3_api_url, self.get_user_agent())

    def get_user_agent(self, installation_id: t.Optional[int] = None) -> str:
        """
        Create a user agent string for the PyGithub client, including the installation if specified.
        """

        user_agent = f"{self.user_agent} PyGithub/python (app_id={self.app_id}"
        if installation_id:
            user_agent += f", installation_id={installation_id})"
        return user_agent

    @property
    def jwt(self) -> TokenInfo:
        """
        Returns the JWT for your GitHub application. The JWT is the token to use with GitHub application APIs.
        """

        return self._jwt_supplier()

    @property
    def jwt_supplier(self) -> JwtSupplier:
        """
        Returns a new #JwtSupplier that is used for generating JWT tokens for your GitHub application.
        """

        return JwtSupplier(self.app_id, self.private_key)

    def app_client(self, settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None) -> "github.Github":
        """
        Returns a PyGithub client for your GitHub application.

        Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
        client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
        operation you perform.

        This requires you to install `PyGithub>=1.58`.
        """

        if isinstance(settings, dict):
            settings = GithubClientSettings(**settings)
        elif settings is None:
            settings = GithubClientSettings()

        settings = self._get_base_github_client_settings().update(settings)
        return settings.make_client(jwt=self.jwt.value)

    def __requestor(self, auth_header: str, installation_id: int) -> t.Dict[str, str]:
        return requests.post(
            self.v3_api_url.rstrip("/") + f"/app/installations/{installation_id}/access_tokens",
            headers={"Authorization": auth_header, "User-Agent": user_agent},
        ).json()

    def get_installation_token_supplier(self, installation_id: int) -> InstallationTokenSupplier:
        """
        Create an #InstallationTokenSupplier for your GitHub application to act within the scope of the given
        *installation_id*.
        """

        with self._lock:
            return self._installation_tokens.setdefault(
                installation_id,
                InstallationTokenSupplier(
                    self._jwt_supplier,
                    installation_id,
                    self.__requestor,
                ),
            )

    def installation_token(self, installation_id: int) -> TokenInfo:
        """
        A short-hand to retrieve a new installation token for the given *installation_id*.
        """

        return self.get_installation_token_supplier(installation_id)()

    def installation_client(
        self,
        installation_id: int,
        settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None,
    ) -> "github.Github":
        """
        Returns a PyGithub client for your GitHub application to act in the scope of the given *installation_id*.

        Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
        client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
        operation you perform.

        This requires you to install `PyGithub>=1.58`.
        """

        if isinstance(settings, dict):
            settings = GithubClientSettings(**settings)
        elif settings is None:
            settings = GithubClientSettings()

        token = self.installation_token(installation_id).value
        settings = self._get_base_github_client_settings().update(settings)
        return settings.make_client(login_or_token=token)
app_id instance-attribute
app_id: int

GitHub Application ID.

jwt property
jwt: TokenInfo

Returns the JWT for your GitHub application. The JWT is the token to use with GitHub application APIs.

jwt_supplier property
jwt_supplier: JwtSupplier

Returns a new #JwtSupplier that is used for generating JWT tokens for your GitHub application.

private_key instance-attribute
private_key: str

RSA private key to sign the JWT with.

user_agent instance-attribute
user_agent: str

User agent of the application. This will be respected in #get_user_agent().

v3_api_url class-attribute instance-attribute
v3_api_url: str = PUBLIC_GITHUB_V3_API_URL

GitHub API base URL. Defaults to the public GitHub API.

app_client
app_client(
    settings: Union[
        GithubClientSettings, Dict[str, Any], None
    ] = None
) -> Github

Returns a PyGithub client for your GitHub application.

Note that the client's token will expire after 10 minutes and you will have to create a new client or update the client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic operation you perform.

This requires you to install PyGithub>=1.58.

Source code in github_bot_api/app.py
def app_client(self, settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None) -> "github.Github":
    """
    Returns a PyGithub client for your GitHub application.

    Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
    client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
    operation you perform.

    This requires you to install `PyGithub>=1.58`.
    """

    if isinstance(settings, dict):
        settings = GithubClientSettings(**settings)
    elif settings is None:
        settings = GithubClientSettings()

    settings = self._get_base_github_client_settings().update(settings)
    return settings.make_client(jwt=self.jwt.value)
get_installation_token_supplier
get_installation_token_supplier(
    installation_id: int,
) -> InstallationTokenSupplier

Create an #InstallationTokenSupplier for your GitHub application to act within the scope of the given installation_id.

Source code in github_bot_api/app.py
def get_installation_token_supplier(self, installation_id: int) -> InstallationTokenSupplier:
    """
    Create an #InstallationTokenSupplier for your GitHub application to act within the scope of the given
    *installation_id*.
    """

    with self._lock:
        return self._installation_tokens.setdefault(
            installation_id,
            InstallationTokenSupplier(
                self._jwt_supplier,
                installation_id,
                self.__requestor,
            ),
        )
get_user_agent
get_user_agent(
    installation_id: Optional[int] = None,
) -> str

Create a user agent string for the PyGithub client, including the installation if specified.

Source code in github_bot_api/app.py
def get_user_agent(self, installation_id: t.Optional[int] = None) -> str:
    """
    Create a user agent string for the PyGithub client, including the installation if specified.
    """

    user_agent = f"{self.user_agent} PyGithub/python (app_id={self.app_id}"
    if installation_id:
        user_agent += f", installation_id={installation_id})"
    return user_agent
installation_client
installation_client(
    installation_id: int,
    settings: Union[
        GithubClientSettings, Dict[str, Any], None
    ] = None,
) -> Github

Returns a PyGithub client for your GitHub application to act in the scope of the given installation_id.

Note that the client's token will expire after 10 minutes and you will have to create a new client or update the client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic operation you perform.

This requires you to install PyGithub>=1.58.

Source code in github_bot_api/app.py
def installation_client(
    self,
    installation_id: int,
    settings: t.Union[GithubClientSettings, t.Dict[str, t.Any], None] = None,
) -> "github.Github":
    """
    Returns a PyGithub client for your GitHub application to act in the scope of the given *installation_id*.

    Note that the client's token will expire after 10 minutes and you will have to create a new client or update the
    client's token with the value returned by #jwt. It is recommended that you create a new client for each atomic
    operation you perform.

    This requires you to install `PyGithub>=1.58`.
    """

    if isinstance(settings, dict):
        settings = GithubClientSettings(**settings)
    elif settings is None:
        settings = GithubClientSettings()

    token = self.installation_token(installation_id).value
    settings = self._get_base_github_client_settings().update(settings)
    return settings.make_client(login_or_token=token)
installation_token
installation_token(installation_id: int) -> TokenInfo

A short-hand to retrieve a new installation token for the given installation_id.

Source code in github_bot_api/app.py
def installation_token(self, installation_id: int) -> TokenInfo:
    """
    A short-hand to retrieve a new installation token for the given *installation_id*.
    """

    return self.get_installation_token_supplier(installation_id)()

Webhook dataclass

Represents a GitHub webhook that listens on an HTTP endpoint for events. Event handlers can be registered using the #@on() decorator or #register() method.

Source code in github_bot_api/webhook.py
@dataclass
class Webhook:
    """
    Represents a GitHub webhook that listens on an HTTP endpoint for events. Event handlers can be
    registered using the #@on() decorator or #register() method.
    """

    #: The webhook secret, if also configured on GitHub. When specified, the payload signature
    #: is checked before an event is accepted by the underlying HTTP framework.
    secret: t.Optional[str]

    handlers: t.List[EventHandler] = field(default_factory=list)

    @t.overload
    def listen(self, event: str) -> t.Callable[[T], T]:
        """
        Decorator to register an event handler function for the specified *event*. The event name
        can be an fnmatch pattern.
        """

    @t.overload
    def listen(self, event: str, func: t.Callable[[Event], bool]) -> None:
        """
        Directly register an event handler function.
        """

    def listen(self, event, func=None):
        if func is None:

            def wrapper(func):
                assert func is not None
                self.listen(event, func)
                return func

            return wrapper
        else:
            self.handlers.append(EventHandler(event, func))

    def dispatch(self, event: Event) -> bool:
        """
        Dispatch an event on the first handler that matches it.

        Returns #True only if the event was handled by a handler.
        """

        matched = False

        for handler in self.handlers:
            if fnmatch.fnmatch(event.name, handler.event):
                matched = True
                if handler.func(event):
                    return True
        logger.info(f'Event %r (id: %r) goes {"unhandled" if matched else "unmatched"}.', event.name, event.delivery_id)

        return matched
dispatch
dispatch(event: Event) -> bool

Dispatch an event on the first handler that matches it.

Returns #True only if the event was handled by a handler.

Source code in github_bot_api/webhook.py
def dispatch(self, event: Event) -> bool:
    """
    Dispatch an event on the first handler that matches it.

    Returns #True only if the event was handled by a handler.
    """

    matched = False

    for handler in self.handlers:
        if fnmatch.fnmatch(event.name, handler.event):
            matched = True
            if handler.func(event):
                return True
    logger.info(f'Event %r (id: %r) goes {"unhandled" if matched else "unmatched"}.', event.name, event.delivery_id)

    return matched

accept_event

accept_event(
    headers: Mapping[str, str],
    raw_body: bytes,
    webhook_secret: Optional[str] = None,
) -> Event

Converts thee HTTP headers and the raw_body to an #Event object.

Parameters:

Name Type Description Default
headers Mapping[str, str]

The HTTP headers. Must have X-Github-Event, X-Github-Delivery, User-Agent, Content-Type. May have X-Hub-Signature or X-Hub-Signature-256.

required
raw_body bytes

The raw request body for the event. This is converted into a JSON payload.

required
webhook_secret Optional[str]

If specified, the X-Hub-Signature or X-Hub-Signature-256 headers are used to verify the signature of the payload. If not specified, the client does not validate the signature.

None
Source code in github_bot_api/event.py
def accept_event(
    headers: t.Mapping[str, str],
    raw_body: bytes,
    webhook_secret: t.Optional[str] = None,
) -> Event:
    """
    Converts thee HTTP *headers* and the *raw_body* to an #Event object.

    Args:
        headers: The HTTP headers. Must have `X-Github-Event`, `X-Github-Delivery`, `User-Agent`, `Content-Type`.
                 May have `X-Hub-Signature` or `X-Hub-Signature-256`.
        raw_body: The raw request body for the event. This is converted into a JSON payload.
        webhook_secret: If specified, the `X-Hub-Signature` or `X-Hub-Signature-256` headers are used to verify
                        the signature of the payload. If not specified, the client does not validate the signature.
    """

    event_name = headers.get("X-GitHub-Event")
    delivery_id = headers.get("X-GitHub-Delivery")
    signature_1 = headers.get("X-Hub-Signature")
    signature_256 = headers.get("X-Hub-Signature-256")
    user_agent = headers.get("User-Agent")
    content_type = headers.get("Content-Type")

    if not event_name or not delivery_id or not user_agent or not content_type:
        raise InvalidRequest("missing required headers")
    if webhook_secret is not None and not signature_1 and not signature_256:
        raise InvalidRequest("webhook secret is configured but no signature header was received")

    mime_type, parameters = get_mime_components(content_type)
    if mime_type != "application/json":
        raise InvalidRequest(f"expected Content-Type: application/json, got {content_type}")
    encoding = dict(parameters).get("encoding", "UTF-8")

    if webhook_secret is not None:
        if signature_256:
            check_signature(signature_256, raw_body, webhook_secret.encode("ascii"), algo="sha256")
        elif signature_1:
            check_signature(signature_1, raw_body, webhook_secret.encode("ascii"), algo="sha1")
        else:
            raise RuntimeError

    return Event(
        event_name,
        delivery_id,
        signature_256 or signature_1,
        user_agent,
        json.loads(raw_body.decode(encoding)),
    )

github_bot_api.flask

Flask binding for handling GitHub webhook events.

Note that you need to install the flask module separately.

Example

from github_bot_api import Event, Webhook
from github_bot_api.flask import create_flask_app

def on_any_event(event: Event) -> bool:
  print(event)
  return True

webhook = Webhook(secret=None)
webhook.listen('*', on_any_event)

import os; os.environ['FLASK_ENV'] = 'development'
flask_app = create_flask_app(__name__, webhook)
flask_app.run()

create_event_handler

create_event_handler(
    webhook: Webhook,
) -> Callable[[], Tuple[Text, int, Dict[str, str]]]

Creates an event handler flask view that interprets the received HTTP request as a GitHub application event and dispatches it via #webhook.dispatch().

Source code in github_bot_api/flask.py
def create_event_handler(webhook: Webhook) -> t.Callable[[], t.Tuple[t.Text, int, t.Dict[str, str]]]:
    """
    Creates an event handler flask view that interprets the received HTTP request as a GitHub application
    event and dispatches it via #webhook.dispatch().
    """

    def event_handler():
        event = accept_event(
            t.cast(t.Mapping[str, str], flask.request.headers), flask.request.get_data(), webhook.secret
        )
        webhook.dispatch(event)
        return "", 202, {}

    return event_handler

create_flask_app

create_flask_app(
    name: str,
    webhook: Webhook,
    path: str = "/event-handler",
) -> Flask

Creates a new #flask.Flask application with a POST event handler under the given path (defaulting to /event-handler). This is a useful shorthand to attach your #Webhook to an HTTP server.

Source code in github_bot_api/flask.py
def create_flask_app(
    name: str,
    webhook: Webhook,
    path: str = "/event-handler",
) -> flask.Flask:
    """
    Creates a new #flask.Flask application with a `POST` event handler under the given *path* (defaulting
    to `/event-handler`). This is a useful shorthand to attach your #Webhook to an HTTP server.
    """

    flask_app = flask.Flask(name)
    flask_app.route(path, methods=["POST"])(create_event_handler(webhook))
    return flask_app