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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56246fcde0f1a60a7b1473d68546458ab5e07c4ea3f2d66e3b3103abb0a0f0ed
|
|
| MD5 |
c2193b935ae759878774a0748f2f09ee
|
|
| BLAKE2b-256 |
9226a5bcff717f2605ef5cc1a29e6de9cb98a998e638d71a77e8fff6a6acdb6a
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f834dceb31e5af6796ddc57fd432a2809be615da8d90c65bcc2fc5dc9d61f45
|
|
| MD5 |
3f98b7c491f0f1e6db75151e4d1ecd04
|
|
| BLAKE2b-256 |
c16cda5ccf8d1e921550671353dbbfdd2b0f91ddc093277d9addfcffdf48af4e
|