Every public symbol exported from codechu_events. Import path:
from codechu_events import (
Bus,
Subscription,
SubscriberLimitExceeded,
MAX_SUBSCRIBERS,
QUEUE_MAX,
DEFAULT_HEARTBEAT_SEC,
__version__,
)There are no module-level singletons or implicit defaults. Callers
construct a Bus() and own its lifetime.
Thread-safe multi-channel pub/sub bus. Multiple Bus instances coexist
in a process; each has its own subscriber list, counters and locks.
Construct an independent bus.
| Parameter | Type | Default | Purpose |
|---|---|---|---|
max_subscribers |
int |
64 |
Per-bus subscriber cap. Exceeding raises SubscriberLimitExceeded. |
queue_max |
int |
200 |
Per-subscription queue depth. Each subscription's bounded queue is sized to this value. (Changed in 0.3.0: previously this argument was accepted but ignored.) |
replay_size |
int |
0 |
Ring-buffer length for the recent-event history. 0 disables it. When > 0, the bus retains the last N emitted events and consumers may request them via subscribe(..., replay=True). |
All arguments are keyword-only.
bus.subscribe(types=None, *, heartbeat_sec=DEFAULT_HEARTBEAT_SEC, subscription_class=Subscription, filter=None, replay=False) -> Subscription
Create a new subscription on this bus.
| Parameter | Type | Default | Purpose |
|---|---|---|---|
types |
list[str] | None |
None → ["*"] |
Glob channel filter list. See Channel glob syntax. |
heartbeat_sec |
float |
5.0 |
Idle keepalive interval. 0 disables it. |
subscription_class |
type[Subscription] |
Subscription |
Override to plug in a custom subclass (e.g. field-based filter). |
filter |
Callable[[dict], bool] | None |
None |
Optional predicate applied after the glob match. Events the predicate rejects are dropped and counted in sub.filtered (separate from queue-full sub.dropped). A filter that raises is treated as a rejection. |
replay |
bool |
False |
If True and the bus was constructed with replay_size > 0, all matching buffered events are delivered to this subscription ahead of live events and outside the queue's backpressure path — they ride a separate replay list and are yielded first by iter() / aiter(). |
Raises SubscriberLimitExceeded if the bus is at its cap. The caller
must eventually call bus.unsubscribe(sub) — prefer subscribe_ctx
for automatic cleanup.
bus.subscribe_ctx(types=None, *, heartbeat_sec=DEFAULT_HEARTBEAT_SEC, subscription_class=Subscription, filter=None, replay=False)
Context manager wrapping subscribe (same kwargs). The subscription is
removed and closed on exit, including on exceptions:
with bus.subscribe_ctx(["scan.*"]) as sub:
bus.emit("scan.started")
for ev in sub:
...Remove a subscription. Idempotent — calling twice is a no-op. Also
invokes sub.close() so any pending iterator terminates cleanly.
Publish an event to every subscription whose filter matches. Never
blocks the publisher: each subscription has a bounded queue and uses
put_nowait; on overflow the event is dropped for that subscriber and
its dropped counter increments.
The emitted dict always carries:
event— theevent_typestringts—time.time()at publish
plus the user-supplied **fields. Names starting with _ are
reserved for the library (_keepalive, _closed); do not emit them
yourself.
Snapshot for monitoring/debug:
{
"subscribers": 3,
"max_subscribers": 64,
"total_emitted": 1284,
"replay_size": 0,
"replay_buffered": 0,
"details": [
{
"types": ["scan.*"],
"queue_depth": 0,
"queue_max": 200,
"received": 412,
"dropped": 0,
"filtered": 0,
"age_sec": 12.4,
},
...
],
}total_emitted is the number of emit() calls on this bus, not the
number of deliveries.
Active subscription count on this bus.
Close every subscription and zero total_emitted. Intended as a
shutdown/test helper. Note: with v0.2 the recommended test pattern is
to construct a fresh Bus() per test rather than mutating a shared
one — see RECIPES.md.
A per-subscriber bounded queue + filter + iter API. Created via
Bus.subscribe() or Bus.subscribe_ctx(); do not instantiate
directly.
| Attribute | Type | Meaning |
|---|---|---|
types |
list[str] |
Active glob patterns. Mutable, but consider this advanced use. |
queue |
queue.Queue |
Underlying FIFO. Inspect qsize() for depth. |
queue_max |
int |
Capacity of queue (mirrors Bus(queue_max=…)). |
dropped |
int |
Events rejected because the queue was full (backpressure). |
filtered |
int |
Events rejected by the user-supplied filter callback. Distinct from dropped. |
received |
int |
Events successfully enqueued. |
created_at |
float |
time.time() at construction. |
heartbeat_sec |
float |
Idle keepalive interval. |
Returns whether this subscription should receive the event. Default
implementation: glob match against self.types. Subclasses may
override to implement regex or field-based filtering. The event
arg is None during cheap type-only checks and the full event dict
during final delivery — useful when a subclass needs to inspect
payload fields.
Called by Bus.emit; not part of the user-facing surface. Non-blocking.
Order of checks:
- If a user-supplied
filtercallback was provided and rejects the event (or raises),sub.filteredincrements and the event is dropped. - Otherwise,
put_nowaitinto the bounded queue. Onqueue.Full,sub.droppedincrements.sub.receivedcounts successful enqueues.
When the subscription was created with replay=True on a bus with
replay_size > 0, the matching past events are buffered on the
subscription at construction time and delivered by iter() / aiter()
before any live event is read from the queue. Replay events:
- bypass the bounded queue and its
droppedaccounting, - still honor the subscription's glob
matches()andfilterpredicate (so a replay event the filter rejects is not delivered, but it is not counted infilteredeither — filter accounting is a live-publish concern), - are consumed once: a second pass over the iterator (after
close()) will not re-deliver them.
Send a _closed sentinel so any pending iter() / aiter() consumer
exits its loop. Called automatically by Bus.unsubscribe.
__iter__ returns self.iter() with defaults. Use sub.iter()
directly to customize:
| Parameter | Type | Default | Purpose |
|---|---|---|---|
timeout |
float | None |
None |
Stop iterating when no event arrives within this many seconds. None waits forever. |
heartbeat |
bool |
True |
If True and heartbeat_sec > 0, yield {"event": "_keepalive", "ts": ...} on idle. |
When the subscription is closed the iterator finishes after draining remaining events.
Async generator backed by loop.run_in_executor around the blocking
queue.get. Keepalive semantics match the sync path. The loop exits
when close() is called.
- Disabled if
heartbeat_sec == 0orheartbeat=Falseis passed toiter()/aiter(). - When enabled, on every idle window of
heartbeat_secseconds the iterator yields{"event": "_keepalive", "ts": time.time()}— consumers should treat this as "still alive, no real event" and ignore it for application logic. - Real events reset the idle window.
class SubscriberLimitExceeded(Exception):
"""Raised when subscribe() is called after MAX_SUBSCRIBERS is reached."""Raised by Bus.subscribe() (and therefore subscribe_ctx) when the
bus is at its max_subscribers cap.
| Name | Default | Meaning |
|---|---|---|
MAX_SUBSCRIBERS |
64 |
Default per-bus subscriber cap. Override per-instance via Bus(max_subscribers=...). |
QUEUE_MAX |
200 |
Per-subscription queue depth. |
DEFAULT_HEARTBEAT_SEC |
5.0 |
Default idle keepalive interval. 0 disables. |
__version__ |
"0.3.0" |
Package version string. |
The filter list passed to subscribe() uses fnmatch case-sensitive
glob semantics (fnmatch.fnmatchcase). An event matches the
subscription if any pattern in the list matches its event_type.
| Pattern | Matches | Does not match |
|---|---|---|
"*" |
every event (scan.started, ui.click, metric.fps) |
— |
"scan.*" |
scan.started, scan.finished, scan.foo |
scan (no dot), scan.foo.bar (* matches one segment greedily, including dots — see note) |
"ui.click" |
ui.click exactly |
ui.clicked, UI.click, ui.click.ok |
"scan.*", "ui.*" |
scan.started, ui.click |
metric.fps |
"*.error" |
scan.error, db.error |
error |
"scan.[sf]*" |
scan.started, scan.finished |
scan.progress |
*is greedy across dots.fnmatchtreats.as a normal character, soscan.*will also matchscan.foo.bar. If you need strict single-segment matching, overrideSubscription.matches.- Case-sensitive.
scan.*does not matchScan.started. - Character classes work.
[abc],[!abc],[a-z]are all honored. - Empty / None filter becomes
["*"](match everything).
Names beginning with _ are library-internal:
_keepalive— emitted by the iterator on idle._closed— internal sentinel injected byclose(); never delivered to consumers (the iterator returns instead).
Do not emit() these from application code.