relaykit-tools 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of relaykit-tools might be problematic. Click here for more details.
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 CoderProandTester
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: relaykit-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A tiny, dependency-free event bus for Python applications.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: event-bus,event-driven,events,publish-subscribe,pubsub
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# RelayKit
|
|
23
|
+
|
|
24
|
+
**A tiny, dependency-free event bus for Python.**
|
|
25
|
+
|
|
26
|
+
RelayKit lets different parts of your application communicate through
|
|
27
|
+
events without needing to know about each other.
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- Simple publish/subscribe API
|
|
32
|
+
- Decorator-based event handlers
|
|
33
|
+
- One-time event listeners
|
|
34
|
+
- Wildcard listeners for observing events
|
|
35
|
+
- Remove listeners individually or clear them all
|
|
36
|
+
- Zero runtime dependencies
|
|
37
|
+
- Type-annotated Python API
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install relaykit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from relaykit import EventBus
|
|
49
|
+
|
|
50
|
+
bus = EventBus()
|
|
51
|
+
|
|
52
|
+
def greet(name):
|
|
53
|
+
print(f"Hello, {name}!")
|
|
54
|
+
|
|
55
|
+
bus.on("user.joined", greet)
|
|
56
|
+
bus.emit("user.joined", "Alex")
|
|
57
|
+
|
|
58
|
+
# Output: Hello, Alex!
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Use decorators
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from relaykit import EventBus
|
|
65
|
+
|
|
66
|
+
bus = EventBus()
|
|
67
|
+
|
|
68
|
+
@bus.on("order.created")
|
|
69
|
+
def process_order(order_id):
|
|
70
|
+
print(f"Processing order {order_id}")
|
|
71
|
+
|
|
72
|
+
bus.emit("order.created", 123)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## One-time listeners
|
|
76
|
+
|
|
77
|
+
A one-time listener removes itself before its first invocation.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
@bus.once("app.started")
|
|
81
|
+
def initialize():
|
|
82
|
+
print("Application initialized")
|
|
83
|
+
|
|
84
|
+
bus.emit("app.started")
|
|
85
|
+
bus.emit("app.started") # Does not invoke initialize again
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Wildcard listeners
|
|
89
|
+
|
|
90
|
+
Listen to events regardless of their names. Wildcard handlers receive
|
|
91
|
+
the event name as their first argument.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
@bus.on("*")
|
|
95
|
+
def log_event(event_name, *args, **kwargs):
|
|
96
|
+
print(f"Event: {event_name}")
|
|
97
|
+
|
|
98
|
+
bus.emit("user.created", 42)
|
|
99
|
+
bus.emit("user.deleted", 42)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Remove listeners
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
def handle_message(message):
|
|
106
|
+
print(message)
|
|
107
|
+
|
|
108
|
+
bus.on("message", handle_message)
|
|
109
|
+
bus.off("message", handle_message)
|
|
110
|
+
|
|
111
|
+
# Remove every listener for one event
|
|
112
|
+
bus.off("message")
|
|
113
|
+
|
|
114
|
+
# Remove every listener for every event
|
|
115
|
+
bus.clear()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Inspect listeners
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
bus.listener_count("message") # Listeners for one event
|
|
122
|
+
bus.listener_count() # All registered listeners
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Error handling
|
|
126
|
+
|
|
127
|
+
Handlers run in registration order. By default, if a handler raises
|
|
128
|
+
an exception, the exception propagates to the caller and subsequent
|
|
129
|
+
handlers are not invoked.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
try:
|
|
133
|
+
bus.emit("task.started")
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
print(f"Event handler failed: {exc}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Limitations
|
|
139
|
+
|
|
140
|
+
- Event dispatch is synchronous.
|
|
141
|
+
- The event bus is not thread-safe.
|
|
142
|
+
- Handler exceptions stop the current dispatch.
|
|
143
|
+
- Event names are case-sensitive.
|
|
144
|
+
- Wildcard listeners use `"*"` as their event name.
|
|
145
|
+
|
|
146
|
+
## Requirements
|
|
147
|
+
|
|
148
|
+
Python 3.10 or newer.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
|
|
2
|
+
# RelayKit
|
|
3
|
+
|
|
4
|
+
**A tiny, dependency-free event bus for Python.**
|
|
5
|
+
|
|
6
|
+
RelayKit lets different parts of your application communicate through
|
|
7
|
+
events without needing to know about each other.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Simple publish/subscribe API
|
|
12
|
+
- Decorator-based event handlers
|
|
13
|
+
- One-time event listeners
|
|
14
|
+
- Wildcard listeners for observing events
|
|
15
|
+
- Remove listeners individually or clear them all
|
|
16
|
+
- Zero runtime dependencies
|
|
17
|
+
- Type-annotated Python API
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install relaykit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from relaykit import EventBus
|
|
29
|
+
|
|
30
|
+
bus = EventBus()
|
|
31
|
+
|
|
32
|
+
def greet(name):
|
|
33
|
+
print(f"Hello, {name}!")
|
|
34
|
+
|
|
35
|
+
bus.on("user.joined", greet)
|
|
36
|
+
bus.emit("user.joined", "Alex")
|
|
37
|
+
|
|
38
|
+
# Output: Hello, Alex!
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Use decorators
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from relaykit import EventBus
|
|
45
|
+
|
|
46
|
+
bus = EventBus()
|
|
47
|
+
|
|
48
|
+
@bus.on("order.created")
|
|
49
|
+
def process_order(order_id):
|
|
50
|
+
print(f"Processing order {order_id}")
|
|
51
|
+
|
|
52
|
+
bus.emit("order.created", 123)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## One-time listeners
|
|
56
|
+
|
|
57
|
+
A one-time listener removes itself before its first invocation.
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
@bus.once("app.started")
|
|
61
|
+
def initialize():
|
|
62
|
+
print("Application initialized")
|
|
63
|
+
|
|
64
|
+
bus.emit("app.started")
|
|
65
|
+
bus.emit("app.started") # Does not invoke initialize again
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Wildcard listeners
|
|
69
|
+
|
|
70
|
+
Listen to events regardless of their names. Wildcard handlers receive
|
|
71
|
+
the event name as their first argument.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
@bus.on("*")
|
|
75
|
+
def log_event(event_name, *args, **kwargs):
|
|
76
|
+
print(f"Event: {event_name}")
|
|
77
|
+
|
|
78
|
+
bus.emit("user.created", 42)
|
|
79
|
+
bus.emit("user.deleted", 42)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Remove listeners
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
def handle_message(message):
|
|
86
|
+
print(message)
|
|
87
|
+
|
|
88
|
+
bus.on("message", handle_message)
|
|
89
|
+
bus.off("message", handle_message)
|
|
90
|
+
|
|
91
|
+
# Remove every listener for one event
|
|
92
|
+
bus.off("message")
|
|
93
|
+
|
|
94
|
+
# Remove every listener for every event
|
|
95
|
+
bus.clear()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Inspect listeners
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
bus.listener_count("message") # Listeners for one event
|
|
102
|
+
bus.listener_count() # All registered listeners
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Error handling
|
|
106
|
+
|
|
107
|
+
Handlers run in registration order. By default, if a handler raises
|
|
108
|
+
an exception, the exception propagates to the caller and subsequent
|
|
109
|
+
handlers are not invoked.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
try:
|
|
113
|
+
bus.emit("task.started")
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
print(f"Event handler failed: {exc}")
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Limitations
|
|
119
|
+
|
|
120
|
+
- Event dispatch is synchronous.
|
|
121
|
+
- The event bus is not thread-safe.
|
|
122
|
+
- Handler exceptions stop the current dispatch.
|
|
123
|
+
- Event names are case-sensitive.
|
|
124
|
+
- Wildcard listeners use `"*"` as their event name.
|
|
125
|
+
|
|
126
|
+
## Requirements
|
|
127
|
+
|
|
128
|
+
Python 3.10 or newer.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
|
|
2
|
+
[build-system]
|
|
3
|
+
requires = ["hatchling>=1.26"]
|
|
4
|
+
build-backend = "hatchling.build"
|
|
5
|
+
|
|
6
|
+
[project]
|
|
7
|
+
name = "relaykit-tools"
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "A tiny, dependency-free event bus for Python applications."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
keywords = [
|
|
15
|
+
"events",
|
|
16
|
+
"event-bus",
|
|
17
|
+
"pubsub",
|
|
18
|
+
"publish-subscribe",
|
|
19
|
+
"event-driven",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 3 - Alpha",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/relaykit"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.sdist]
|
|
38
|
+
include = [
|
|
39
|
+
"/src",
|
|
40
|
+
"/README.md",
|
|
41
|
+
"/LICENSE",
|
|
42
|
+
"/pyproject.toml",
|
|
43
|
+
]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
|
|
2
|
+
"""RelayKit: a tiny, dependency-free event bus for Python."""
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any, TypeVar, overload
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
Handler = Callable[..., Any]
|
|
10
|
+
T = TypeVar("T", bound=Handler)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EventBus:
|
|
14
|
+
"""Publish events and let independent components subscribe to them.
|
|
15
|
+
|
|
16
|
+
Regular handlers receive the emitted positional and keyword arguments.
|
|
17
|
+
Wildcard handlers registered for '*' receive the event name first.
|
|
18
|
+
Exceptions raised by handlers propagate to the caller.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._handlers: dict[str, list[Handler]] = {}
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _validate_event(event: str) -> None:
|
|
26
|
+
if not isinstance(event, str) or not event.strip():
|
|
27
|
+
raise ValueError("Event name must be a non-empty string.")
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _validate_handler(handler: Handler) -> None:
|
|
31
|
+
if not callable(handler):
|
|
32
|
+
raise TypeError("Handler must be callable.")
|
|
33
|
+
|
|
34
|
+
@overload
|
|
35
|
+
def on(self, event: str) -> Callable[[T], T]: ...
|
|
36
|
+
|
|
37
|
+
@overload
|
|
38
|
+
def on(self, event: str, handler: T) -> T: ...
|
|
39
|
+
|
|
40
|
+
def on(
|
|
41
|
+
self, event: str, handler: T | None = None
|
|
42
|
+
) -> T | Callable[[T], T]:
|
|
43
|
+
"""Register a handler. Can be used directly or as a decorator."""
|
|
44
|
+
self._validate_event(event)
|
|
45
|
+
|
|
46
|
+
def register(callback: T) -> T:
|
|
47
|
+
self._validate_handler(callback)
|
|
48
|
+
self._handlers.setdefault(event, []).append(callback)
|
|
49
|
+
return callback
|
|
50
|
+
|
|
51
|
+
if handler is None:
|
|
52
|
+
return register
|
|
53
|
+
|
|
54
|
+
return register(handler)
|
|
55
|
+
|
|
56
|
+
@overload
|
|
57
|
+
def once(self, event: str) -> Callable[[T], T]: ...
|
|
58
|
+
|
|
59
|
+
@overload
|
|
60
|
+
def once(self, event: str, handler: T) -> T: ...
|
|
61
|
+
|
|
62
|
+
def once(
|
|
63
|
+
self, event: str, handler: T | None = None
|
|
64
|
+
) -> T | Callable[[T], T]:
|
|
65
|
+
"""Register a handler that is removed before its first invocation."""
|
|
66
|
+
self._validate_event(event)
|
|
67
|
+
|
|
68
|
+
def register(callback: T) -> T:
|
|
69
|
+
self._validate_handler(callback)
|
|
70
|
+
|
|
71
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
72
|
+
self.off(event, wrapper)
|
|
73
|
+
return callback(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
# Keep the original callback available for off(event, callback).
|
|
76
|
+
setattr(wrapper, "__relaykit_original__", callback)
|
|
77
|
+
self._handlers.setdefault(event, []).append(wrapper)
|
|
78
|
+
return callback
|
|
79
|
+
|
|
80
|
+
if handler is None:
|
|
81
|
+
return register
|
|
82
|
+
|
|
83
|
+
return register(handler)
|
|
84
|
+
|
|
85
|
+
def off(self, event: str, handler: Handler | None = None) -> bool:
|
|
86
|
+
"""Remove a handler, or all handlers for an event.
|
|
87
|
+
|
|
88
|
+
Returns True if anything was removed, otherwise False.
|
|
89
|
+
Passing no handler removes every listener for the event.
|
|
90
|
+
"""
|
|
91
|
+
self._validate_event(event)
|
|
92
|
+
listeners = self._handlers.get(event)
|
|
93
|
+
|
|
94
|
+
if not listeners:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
if handler is None:
|
|
98
|
+
del self._handlers[event]
|
|
99
|
+
return True
|
|
100
|
+
|
|
101
|
+
remaining = [
|
|
102
|
+
registered
|
|
103
|
+
for registered in listeners
|
|
104
|
+
if registered is not handler
|
|
105
|
+
and getattr(registered, "__relaykit_original__", None)
|
|
106
|
+
is not handler
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
removed = len(remaining) != len(listeners)
|
|
110
|
+
|
|
111
|
+
if remaining:
|
|
112
|
+
self._handlers[event] = remaining
|
|
113
|
+
else:
|
|
114
|
+
self._handlers.pop(event, None)
|
|
115
|
+
|
|
116
|
+
return removed
|
|
117
|
+
|
|
118
|
+
def emit(self, event: str, *args: Any, **kwargs: Any) -> int:
|
|
119
|
+
"""Emit an event and return the number of handlers invoked.
|
|
120
|
+
|
|
121
|
+
Listeners are called in registration order. Wildcard listeners
|
|
122
|
+
registered for '*' receive (event, *args, **kwargs).
|
|
123
|
+
Listener lists are snapshotted before dispatch.
|
|
124
|
+
"""
|
|
125
|
+
self._validate_event(event)
|
|
126
|
+
|
|
127
|
+
listeners = list(self._handlers.get(event, ()))
|
|
128
|
+
if event != "*":
|
|
129
|
+
listeners.extend(self._handlers.get("*", ()))
|
|
130
|
+
|
|
131
|
+
for handler in listeners[:len(self._handlers.get(event, ())) if event != "*" else len(listeners)]:
|
|
132
|
+
handler(*args, **kwargs)
|
|
133
|
+
|
|
134
|
+
if event != "*":
|
|
135
|
+
for handler in list(self._handlers.get("*", ())):
|
|
136
|
+
handler(event, *args, **kwargs)
|
|
137
|
+
|
|
138
|
+
return len(listeners)
|
|
139
|
+
|
|
140
|
+
def listener_count(self, event: str | None = None) -> int:
|
|
141
|
+
"""Return the listener count for one event or all events."""
|
|
142
|
+
if event is None:
|
|
143
|
+
return sum(len(handlers) for handlers in self._handlers.values())
|
|
144
|
+
|
|
145
|
+
self._validate_event(event)
|
|
146
|
+
return len(self._handlers.get(event, ()))
|
|
147
|
+
|
|
148
|
+
def clear(self, event: str | None = None) -> None:
|
|
149
|
+
"""Remove listeners for one event, or all listeners."""
|
|
150
|
+
if event is None:
|
|
151
|
+
self._handlers.clear()
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
self._validate_event(event)
|
|
155
|
+
self._handlers.pop(event, None)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = ["EventBus", "__version__"]
|