Skip to main content

Lightweight async message-passing building blocks: an action bus and an event bus

Project description

lapwing

lapwing is a library providing lightweight async message-passing building blocks: an action bus and an event bus.

Why lapwing? The lapwing is a bird renowned for its piercing, far-carrying calls — it detects disturbances and signals them immediately to others nearby. This mirrors the library's purpose: one part of a system raises a signal, and the right listeners respond, without the sender knowing or caring who they are.

Core Concepts

The library addresses a common architectural need: decoupling the sender of a message from its handler. Instead of calling functions directly, you define typed messages and register handlers separately, keeping concerns isolated and code testable.

All operations are async-first and return asyncio.Task, giving the caller control over when to await.

Key Primitives

Action: A subclass carrying a phantom type parameter T — used only by the type checker to annotate what the handler returns. Define one per action.

Event: A subclass representing something that has happened. Multiple listeners may react to the same event.

ActionBus: Dispatches an action to exactly one registered async handler. Raises NoHandlerError eagerly if no handler is registered. Supports an optional middleware pipeline.

EventBus: Broadcasts an event to all registered async listeners concurrently via asyncio.TaskGroup. Succeeds silently if no listeners are registered. Raises ExceptionGroup if any listener fails. Supports an optional middleware pipeline applied independently to each listener.

Usage

ActionBus

Middlewares wrap the handler pipeline in list order — middlewares[0] is outermost.

NOTE: Middlewares apply to all action types on a bus and must preserve the return type of each concrete action's handler.

from dataclasses import dataclass

from lapwing import Action, ActionBus


@dataclass
class CreateUser(Action[int]):
    username: str
    email: str


async def logging_middleware(action, call_next):
    print(f"Dispatching {type(action).__name__}")
    result = await call_next(action)
    print("Done")
    return result


bus = ActionBus(middlewares=[logging_middleware])


@bus.handler(CreateUser)
async def handle_create_user(action: CreateUser) -> int:
    user = await db.insert(action.username, action.email)
    return user.id


user_id = await bus.dispatch(CreateUser(username="alice", email="alice@example.com"))

EventBus

Middlewares wrap each listener independently in list order — middlewares[0] is outermost.

NOTE: Middlewares apply to every listener on a bus and must preserve the return type of each concrete event's listener (None). Each listener gets its own pipeline; a short-circuit or failure in one does not affect others.

from dataclasses import dataclass

from lapwing import Event, EventBus


@dataclass
class UserCreated(Event):
    user_id: int


async def logging_middleware(event, call_next):
    print(f"Handling {type(event).__name__}")
    await call_next(event)


bus = EventBus(middlewares=[logging_middleware])


@bus.listener(UserCreated)
async def send_welcome_email(event: UserCreated) -> None:
    await mailer.send_welcome(event.user_id)


@bus.listener(UserCreated)
async def write_audit_log(event: UserCreated) -> None:
    await audit.log(f"User {event.user_id} created")


await bus.emit(UserCreated(user_id=42))

Installation

uv add lapwing

Requirements

Python 3.13+

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lapwing-1.1.0.tar.gz (19.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lapwing-1.1.0-py3-none-any.whl (5.9 kB view details)

Uploaded Python 3

File details

Details for the file lapwing-1.1.0.tar.gz.

File metadata

  • Download URL: lapwing-1.1.0.tar.gz
  • Upload date:
  • Size: 19.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for lapwing-1.1.0.tar.gz
Algorithm Hash digest
SHA256 56246fcde0f1a60a7b1473d68546458ab5e07c4ea3f2d66e3b3103abb0a0f0ed
MD5 c2193b935ae759878774a0748f2f09ee
BLAKE2b-256 9226a5bcff717f2605ef5cc1a29e6de9cb98a998e638d71a77e8fff6a6acdb6a

See more details on using hashes here.

File details

Details for the file lapwing-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: lapwing-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 5.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.2 {"installer":{"name":"uv","version":"0.11.2","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for lapwing-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f834dceb31e5af6796ddc57fd432a2809be615da8d90c65bcc2fc5dc9d61f45
MD5 3f98b7c491f0f1e6db75151e4d1ecd04
BLAKE2b-256 c16cda5ccf8d1e921550671353dbbfdd2b0f91ddc093277d9addfcffdf48af4e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page