event
Version: 8.0.14
Observer-pattern mix-ins that add priority-ordered, sync/async event dispatch with optional bubbling to any class
Table of Contents
API Reference
Core Classes
Event
Immutable dataclass representing an event.
@dataclass(frozen=True, slots=True)
class Event:
name: str
payload: Mapping[str, Any] | None = None
metadata: Mapping[str, Any] | None = None
source: Any | None = None
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
Methods:
with_update(**changes)- Returns a copy with updated fieldsderive(name=None, **changes)- Creates a derived event, optionally with a new name
EventDispatcherMixin
Mixin providing event dispatching capabilities.
Methods:
add_event_listener(name, callback, *, once=False, priority=DEFAULT_PRIORITY)- Register a listenerremove_event_listener(name, callback)- Remove a listener (returns bool)clear_event_listeners(name=None)- Clear listeners (all or for specific event)has_event_listeners(name)- Check if event has listenersdispatch(event, *, payload=None, metadata=None, source=_UNSET)- Dispatch synchronouslydispatch_async(event, *, payload=None, metadata=None, source=_UNSET)- Dispatch asynchronouslydispatch_event(...)- Alias fordispatchdispatch_event_async(...)- Alias fordispatch_async
EventListenerMixin
Mixin for declarative event listeners using decorators.
Class Method:
@on(event_name, *, priority=DEFAULT_PRIORITY, once=False)- Decorator to mark methods as listeners
Methods:
bind_to_dispatcher(dispatcher)- Bind all decorated methods to a dispatcherunbind_from_dispatcher()- Unbind from current dispatcherget_bound_dispatcher()- Get the currently bound dispatcher
EventPriority
Enum for common priority values.
class EventPriority(IntEnum):
LOW = -100
NORMAL = 0
HIGH = 100
Constant:
DEFAULT_PRIORITY = EventPriority.NORMAL
Type Aliases
EventCallback = Callable[[Event], Awaitable[None] | None]
Dataclasses
ListenerRecord
Internal dataclass storing listener information.
@dataclass(slots=True)
class ListenerRecord:
callback: EventCallback
once: bool
priority: int
order: int
ListenerSpec
Internal dataclass for decorator metadata.
@dataclass(frozen=True, slots=True)
class ListenerSpec:
name: str
priority: int
once: bool
ListenerState
Internal class managing listener binding state.
class ListenerState:
dispatcher: EventDispatcherMixin | None
bindings: List[tuple[str, EventCallback]]
Usage Patterns
Synchronous Listener
def my_listener(event: Event) -> None:
print(event.name)
Asynchronous Listener
async def my_async_listener(event: Event) -> None:
await some_async_operation()
Decorator Pattern
class MyListener(EventListenerMixin):
@EventListenerMixin.on("event.name", priority=EventPriority.HIGH)
def handle_event(self, event: Event) -> None:
pass
Custom Priority
dispatcher.add_event_listener("event", callback, priority=50)
Once Listener
dispatcher.add_event_listener("event", callback, once=True)
Examples
Plugin System
from wexample_event import Event, EventDispatcherMixin, EventListenerMixin
class Application(EventDispatcherMixin):
def start(self) -> None:
self.dispatch("app.startup")
print("Application started")
def stop(self) -> None:
self.dispatch("app.shutdown")
print("Application stopped")
class LoggingPlugin(EventListenerMixin):
@EventListenerMixin.on("app.startup")
def on_startup(self, event: Event) -> None:
print("[LOG] Application is starting...")
@EventListenerMixin.on("app.shutdown")
def on_shutdown(self, event: Event) -> None:
print("[LOG] Application is shutting down...")
class MetricsPlugin(EventListenerMixin):
@EventListenerMixin.on("app.startup")
def on_startup(self, event: Event) -> None:
print("[METRICS] Recording startup time")
# Setup
app = Application()
logging_plugin = LoggingPlugin()
metrics_plugin = MetricsPlugin()
logging_plugin.bind_to_dispatcher(app)
metrics_plugin.bind_to_dispatcher(app)
# Run
app.start()
app.stop()
State Change Notifications
from wexample_event import Event, EventDispatcherMixin
class DataStore(EventDispatcherMixin):
def __init__(self) -> None:
self._data: dict = {}
def set(self, key: str, value: any) -> None:
old_value = self._data.get(key)
self._data[key] = value
self.dispatch(
"data.changed",
payload={"key": key, "old": old_value, "new": value}
)
def get(self, key: str) -> any:
return self._data.get(key)
# Create store and add listeners
store = DataStore()
def on_data_changed(event: Event) -> None:
payload = event.payload
print(f"Data changed: {payload['key']} = {payload['new']}")
store.add_event_listener("data.changed", on_data_changed)
# Use the store
store.set("username", "alice")
store.set("username", "bob")
Request/Response Pipeline
from wexample_event import Event, EventDispatcherMixin, EventPriority
class RequestPipeline(EventDispatcherMixin):
def process(self, request: dict) -> dict:
# Create event with mutable response
response = {"status": "pending", "data": request}
self.dispatch(
"request.process",
payload={"request": request, "response": response}
)
return response
pipeline = RequestPipeline()
# Authentication middleware (high priority)
def authenticate(event: Event) -> None:
response = event.payload["response"]
request = event.payload["request"]
if not request.get("auth_token"):
response["status"] = "error"
response["message"] = "Authentication required"
else:
response["authenticated"] = True
# Validation middleware (normal priority)
def validate(event: Event) -> None:
response = event.payload["response"]
if response["status"] == "error":
return # Skip if already errored
request = event.payload["request"]
if not request.get("data"):
response["status"] = "error"
response["message"] = "Data required"
# Processing (low priority)
def process_data(event: Event) -> None:
response = event.payload["response"]
if response["status"] == "error":
return
response["status"] = "success"
response["processed"] = True
pipeline.add_event_listener("request.process", authenticate, priority=EventPriority.HIGH)
pipeline.add_event_listener("request.process", validate, priority=EventPriority.NORMAL)
pipeline.add_event_listener("request.process", process_data, priority=EventPriority.LOW)
# Process requests
result1 = pipeline.process({"auth_token": "abc", "data": {"value": 123}})
print(result1) # Success
result2 = pipeline.process({"data": {"value": 456}})
print(result2) # Error: Authentication required
Event Inheritance
from wexample_event import Event, EventDispatcherMixin, EventListenerMixin
class BaseListener(EventListenerMixin):
@EventListenerMixin.on("base.event")
def handle_base(self, event: Event) -> None:
print("Base handler")
class ExtendedListener(BaseListener):
@EventListenerMixin.on("extended.event")
def handle_extended(self, event: Event) -> None:
print("Extended handler")
dispatcher = EventDispatcherMixin()
listener = ExtendedListener()
listener.bind_to_dispatcher(dispatcher)
# Both base and extended events work
dispatcher.dispatch("base.event") # Prints: Base handler
dispatcher.dispatch("extended.event") # Prints: Extended handler
Dynamic Event Names
from wexample_event import Event, EventDispatcherMixin
class EventBus(EventDispatcherMixin):
def emit(self, event_type: str, **data) -> None:
self.dispatch(f"event.{event_type}", payload=data)
bus = EventBus()
# Add wildcard-style handlers
def handle_user_events(event: Event) -> None:
print(f"User event: {event.name}")
bus.add_event_listener("event.user.login", handle_user_events)
bus.add_event_listener("event.user.logout", handle_user_events)
# Emit events
bus.emit("user.login", username="alice")
bus.emit("user.logout", username="alice")
Quickstart
Basic Event Dispatching
from wexample_event.dataclass.event import Event
from wexample_event.common.dispatcher import EventDispatcherMixin
# Create a dispatcher
class MyApp(EventDispatcherMixin):
pass
app = MyApp()
# Add a listener
def on_user_login(event: Event) -> None:
print(f"User logged in: {event.payload['username']}")
app.add_event_listener("user.login", on_user_login)
# Dispatch an event
app.dispatch("user.login", payload={"username": "john"})
Using the Listener Mixin
from wexample_event.dataclass.event import Event
from wexample_event.common.listener import EventListenerMixin
from wexample_event.common.dispatcher import EventDispatcherMixin
class MyDispatcher(EventDispatcherMixin):
pass
class MyListener(EventListenerMixin):
@EventListenerMixin.on("user.login")
def handle_login(self, event: Event) -> None:
print(f"Login handled: {event.payload['username']}")
@EventListenerMixin.on("user.logout")
def handle_logout(self, event: Event) -> None:
print("User logged out")
# Setup
dispatcher = MyDispatcher()
listener = MyListener()
listener.bind_to_dispatcher(dispatcher)
# Dispatch events
dispatcher.dispatch("user.login", payload={"username": "jane"})
dispatcher.dispatch("user.logout")
Async Events
import asyncio
from wexample_event.dataclass.event import Event
from wexample_event.common.dispatcher import EventDispatcherMixin
class AsyncApp(EventDispatcherMixin):
pass
app = AsyncApp()
async def async_handler(event: Event) -> None:
await asyncio.sleep(0.1)
print(f"Async handler: {event.name}")
app.add_event_listener("async.event", async_handler)
# Use dispatch_async for async handlers
await app.dispatch_async("async.event")
Priority and Once
from wexample_event.dataclass.event import Event
from wexample_event.common.priority import EventPriority
from wexample_event.common.dispatcher import EventDispatcherMixin
app = EventDispatcherMixin()
# High priority listener (runs first)
def high_priority(event: Event) -> None:
print("High priority")
# Low priority listener (runs last)
def low_priority(event: Event) -> None:
print("Low priority")
# One-time listener
def once_handler(event: Event) -> None:
print("This runs only once")
app.add_event_listener("test", high_priority, priority=EventPriority.HIGH)
app.add_event_listener("test", low_priority, priority=EventPriority.LOW)
app.add_event_listener("test", once_handler, once=True)
app.dispatch("test") # All three run
app.dispatch("test") # Only high and low priority run
Installation
pip install wexample-event
Requires Python >=3.10.
Tests
This project uses pytest for testing and pytest-cov for code coverage analysis.
Installation
First, install the required testing dependencies:
.venv/bin/python -m pip install pytest pytest-cov
Basic Usage
Run all tests with coverage:
.venv/bin/python -m pytest --cov --cov-report=html
Common Commands
# Run tests with coverage for a specific module
.venv/bin/python -m pytest --cov=your_module
# Show which lines are not covered
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
# Generate an HTML coverage report
.venv/bin/python -m pytest --cov=your_module --cov-report=html
# Combine terminal and HTML reports
.venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
# Run specific test file with coverage
.venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
Viewing HTML Reports
After generating an HTML report, open htmlcov/index.html in your browser to view detailed line-by-line coverage information.
Coverage Threshold
To enforce a minimum coverage percentage:
.venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
This will cause the test suite to fail if coverage drops below 80%.
Known Limitations & Roadmap
Current limitations and planned features are tracked in the GitHub issues.
See the project roadmap for upcoming features and improvements.
Status & Compatibility
Maturity: Production-ready
Python Support: >=3.10
OS Support: Linux, macOS, Windows
Status: Actively maintained
Useful Links
- Homepage: https://github.com/wexample/python-event
- Issue Tracker: https://github.com/wexample/python-event/issues
- Discussions: https://github.com/wexample/python-event/discussions
- PyPI: pypi.org/project/wexample-event
Metadata
Release files for wexample-event 8.0.14
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| wexample_event-8.0.14.tar.gz | 18.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wexample_event-8.0.14-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 30.1 kB
Release files / wexample_event-8.0.14.tar.gz
| Download URL | wexample_event-8.0.14.tar.gz |
|---|---|
| Size | 18.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a5090a257ac742f6b02949a33393731d859f4f3f882b13539b4b4053aaa12249
|
|
BLAKE2b-256 checksum How to use checksums |
5caefd6bab9cf7db660e75b6e18cae57fd62b21458ebb0bd8286c0ce9c3a36f3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.29.3 CPython/3.12.3 Linux/6.1.0-37-amd64
|
Release files / wexample_event-8.0.14-py3-none-any.whl
| Download URL | wexample_event-8.0.14-py3-none-any.whl |
|---|---|
| Size | 11.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5a374d2d34e71f70bac0400cedccd730efa0342398a7e966e0ea04c957a530b7
|
|
BLAKE2b-256 checksum How to use checksums |
228c6109c818efeae995c886ae0ebd5473538c931c470926c36ded3767e91da6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
pdm/2.29.3 CPython/3.12.3 Linux/6.1.0-37-amd64
|
Security for wexample-event 8.0.14
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.