relaykit-tools 0.1.1__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.
@@ -0,0 +1,8 @@
1
+
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
@@ -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.1
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 Tools
23
+
24
+ **A tiny, dependency-free event bus for Python.**
25
+
26
+ RelayKit Tools 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-tools
43
+ ```
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ from relaykit_tools 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_tools 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 Tools
3
+
4
+ **A tiny, dependency-free event bus for Python.**
5
+
6
+ RelayKit Tools 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-tools
23
+ ```
24
+
25
+ ## Quick start
26
+
27
+ ```python
28
+ from relaykit_tools 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_tools 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.1"
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_tools"]
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 Tools: 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.1"
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__"]