Skip to main content

uv Ruff

Monalisten

Monalisten is a Python 3.10+ asynchronous library that helps you handle webhook events received from GitHub in an easy way. It is built on top of the amazing githubkit and httpx libraries and relies on SSE (with httpx-sse) to stream events without exposing any endpoints.

Installation

monalisten is available on PyPI and can be installed with any package manager:

pip install monalisten
# or
poetry add monalisten
# or
uv add monalisten

You can also install it from source:

pip install git+https://github.com/trag1c/monalisten.git

Usage

Foreword on how this works

GitHub webhooks can only send event data to publicly accessible HTTP endpoints. If your environment is behind a firewall, or a NAT, or you simply don't want to set up a server, you can use a relay service, like smee.io. It generates a unique relay URL to which GitHub sends requests to, and the relay then streams them to your local client via SSE. Monalisten connects to the relay's SSE URL and receives events as they arrive without any direct incoming connection to your machine.

Basic example

import asyncio

from monalisten import Monalisten, events

client = Monalisten("https://smee.io/aBCDef1gHijKLM2N", token="foobar")


@client.event.push
async def log_push(event: events.Push) -> None:
    actor = event.sender.login if event.sender else "Someone"
    print(f"{actor} pushed to the repo!")


asyncio.run(client.listen())

Monalisten heavily relies on the githubkit SDK for parsing and verifying payloads. The monalisten.events module (meant for type annotations) is actually a re-export of two githubkit modules!

One event, multiple hooks

You can decorate several functions with the same event, and both of them will be registered:

@client.event.pull_request
async def log_opened_pr(event: events.PullRequest) -> None:
    if event.action != "opened":
        return
    print(f"New PR: #{event.number}")


@client.event.pull_request
async def log_pr_action(event: events.PullRequest) -> None:
    print(f"Something happened to PR #{event.number}!")

When an event type has several hooks attached, they're all run concurrently.

One hook, multiple events

You can decorate the same function with multiple events:

@client.event.pull_request
@client.event.push
async def log_things(event: events.PullRequest | events.Push) -> None:
    if "PullRequest" in type(event).__name__:
        print("Something happened to a PR!")
    else:
        print("Someone pushed!")

Action-specific hooks (subhooks)

Monalisten allows registering hooks for a specific event action. The example in One event, multiple hooks could be rewritten as:

@client.event.pull_request.opened
async def log_opened_pr(event: events.PullRequestOpened) -> None:
    print(f"New PR: #{event.number}")


@client.event.pull_request
async def log_pr_action(event: events.PullRequest) -> None:
    print(f"Something happened to PR #{event.number}!")

Note that some events:

  • don't have any actions (e.g. fork or push), or
  • only have one action (e.g. watch or commit_comment), in which case @client.event.event_name.action_name and @client.event.event_name are eqiuvalent.

Wildcard hooks

You can define a hook to be triggered for ALL events by using the any event:

@client.event.any
async def log(event: events.Any) -> None:
    print(f"Something definitely happened... a {type(event).__name__} perhaps")

Internal events

Other than GitHub events, hooks can be created for handling a few internal events reported by Monalisten itself, such as:

  • an HTTP client is created in .listen() (ready)
  • an authentication issue arises (auth_issue)
  • an error occurs (error)

Internal event hooks are defined with the Monalisten.internal namespace. The internal error event is the only one with default behavior—it will raise an exception and halt the client. The other two are simply ignored if no hook is defined.

ready

Triggered when an internal HTTP client is created, right before streaming events from source. The expected hook signature is async () -> None.

@client.internal.ready
async def on_ready() -> None:
    print("🚀 Monalisten is ready!")

auth_issue

During its authentication step, Monalisten can report issues for unexpected state. Reading those requires defining an auth issue hook. The expected hook signature is async (AuthIssue) -> None.

import json
from pathlib import Path

from monalisten import AuthIssue

saved_events_dir = Path("/path/to/logs")


@client.internal.auth_issue
async def log_and_save(issue: AuthIssue) -> None:
    data = issue.payload
    event_guid = data.get("x-github-delivery", "missing-guid")
    print(f"Auth issue in event {data}: token {issue.kind.value}")
    (saved_events_dir / f"{event_guid}.json").write_text(json.dumps(data))

Monalisten will report auth issues in the following cases:

  • the client sets a token, but:

    • the received event doesn't have a signature header
    • the received event's signature cannot be validated with the client's token

    (the event is not processed in both cases)

  • the client doesn't set a token, but the received event has a signature header (the event is still processed)

error

Monalisten can raise an exception in three contexts:

  • during setup, when attempting to register a hook under the bare Monalisten.event or Monalisten.internal namespaces,
  • during event preprocessing, when the event payload is missing crucial fields, e.g. an event type header or a body,
  • during event processing, when an error is raised in a user-defined hook.

Only preprocessing and processing errors can have hooks set up (since setup errors are raised before the client is ready). The expected hook signature is async (Error) -> None.

from monalisten import Error
from pydantic import ValidationError


@client.internal.error
async def print_error_summary(error: Error) -> None:
    if error.payload:
        event_guid = error.payload.get("x-github-delivery", "<missing-guid>")
        print(f"An error occurred in event {event_guid}: {str(error.exc)}")
    else:
        # payload is not present if the error comes from a `ready` hook
        print("An error occurred at startup!")

    if not isinstance(cause := error.exc.__cause__, ValidationError):
        return

    print("Pydantic errors detected:")
    for err in cause.errors():
        print("-", err["msg"])
        print(" ", err["loc"])

Testing with manual events

You can dispatch a webhook event directly to registered hooks without a GitHub repo or an SSE relay by using Monalisten.dispatch_event:

await client.dispatch_event(
    "push",
    {
        "after": ...,
        "base_ref": ...,
        "before": ...,
        "commits": [
            ...
        ],
        ...
    }
)

API reference

AuthIssue

class AuthIssue(NamedTuple):
    kind: AuthIssueKind
    payload: EventPayload

An object representing authentication issue events reported by the Monalisten client to auth_issue hooks.

AuthIssueKind

class AuthIssueKind(Enum):
    MISSING = "missing"
    UNEXPECTED = "unexpected"
    MISMATCH = "mismatch"

An enum representing authentication issue kinds that can be encountered by the Monalisten client. The table below describes scenarios in which the issue kinds can occur:

Issue kind Client token Received signature Verified
MISSING ✅ ❌ —
UNEXPECTED ❌ ✅ —
MISMATCH ✅ ✅ ❌

Error

class Error(NamedTuple):
    exc: Exception
    event_name: str | None
    payload: EventPayload

An object representing runtime (that is, preprocessing or processing) error events reported by the Monalisten client.

EventPayload

EventPayload = TypedDict(
    "EventPayload",
    {
        "body": dict[str, Any],
        "x-github-hook-id": str,
        "x-github-event": str,
        "x-github-delivery": str,
        "x-hub-signature": str,
        "x-hub-signature-256": str,
        "user-agent": str,
        "x-github-hook-installation-target-type": str,
        "x-github-hook-installation-target-id": str,
    },
)

Represents the raw event payload received from GitHub. Can be accessed in internal.auth_issue and internal.error hooks. It only lists body and headers considered "special" by GitHub (see the "Delivery headers" section of their Webhook events and payloads page), although other headers may be present.

Monalisten

class Monalisten:
    def __init__(self, source: str, *, token: str | None = None) -> None: ...

Creates a Monalisten client streaming events from source, optionally secured by the secret token.

Monalisten.listen

class Monalisten:
    async def listen(self) -> None: ...

Instantiates an internal HTTP client and starts streaming events from source.

Monalisten.dispatch_event

class Monalisten:
    async def dispatch_event(
        self,
        event_type: str,
        body: dict[str, Any],
        *,
        headers: dict[str, Any] | None = None,
    ) -> None: ...

Dispatches a webhook event directly to registered hooks. Useful for testing hooks without a real GitHub webhook or SSE relay. Authentication is skipped by default; set a signature header to test it.

Monalisten.event

A namespace storing GitHub event registrars. Valid event names are all GitHub event names and any.

Every hook is expected to have the signature of async (events.Any) -> None (narrowed down to the specific event type).

Monalisten.internal

A namespace storing internal event registrars. Valid event names are ready, auth_issue, and error.

See the Internal events section for expected hook signatures for each event.

MonalistenPreprocessingError

class MonalistenPreprocessingError(Exception): ...

An exception for preprocessing errors. Triggered when the received event payload is missing crucial fields, e.g. an event type header or a body.

MonalistenSetupError

class MonalistenSetupError(Exception): ...

An exception for setup errors. Triggered when attempting to register a hook under the bare Monalisten.event or Monalisten.internal namespaces.

GitHub event name reference

For a list of valid event names under the Monalisten.event namespace, rely on your LSP's autocomplete (if you use one), or see GitHub's documentation page on Webhook events and payloads. Each event may optionally include a list of possible actions to use in subhooks.

monalisten.event reference

For a list of type names that can be used as event annotations, see the src/monalisten/events.py file, or, if you use one, rely on your LSP's autocomplete!

License

monalisten is licensed under the MIT License. © trag1c, 2025

Metadata

Release files for monalisten 0.4.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for monalisten 0.4.1
File Size Uploaded Provenance Actions
monalisten-0.4.1.tar.gz 22.3 kB Without provenance Details for monalisten-0.4.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for monalisten 0.4.1
File Interpreter ABI Platform Actions
monalisten-0.4.1-py3-none-any.whl Python 3 none any Details for monalisten-0.4.1-py3-none-any.whl

Total release size: 46.7 kB

Release files / monalisten-0.4.1.tar.gz

Download URL monalisten-0.4.1.tar.gz
Size 22.3 kB
Tags Source
SHA-256 checksum
How to use checksums
2d597e097d5ecb4be280ca8b1a4f21338db29911b7790e1dadb82fcaf4d65137
BLAKE2b-256 checksum
How to use checksums
ad23853920b19aa31635d97f7da3ce90900152ce44d60cc0b87862dd58967551
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / monalisten-0.4.1-py3-none-any.whl

Download URL monalisten-0.4.1-py3-none-any.whl
Size 24.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f4107c4952bf4b40961b66b9a1eed2eaa25d67093109357fc9ee1add05dee935
BLAKE2b-256 checksum
How to use checksums
965c87324d16e4c9989c4d60e29eb6b77066a50471e6a1d18cf34fa4ba090449
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for monalisten 0.4.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page