vaitty-eventbus 0.0.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,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaitty_eventbus
3
+ Version: 0.0.1
4
+ Summary: A minimal, internal Python library for decoupled event communication using channels and wildcard-capable handlers.
5
+ Home-page: https://bitbucket.org/rapihogar/event-bus
6
+ Author: Tech Data AI
7
+ Author-email: tech-data-ai@rapihogar.com
8
+ Requires-Python: >=3.7
9
+ Requires-Dist: requests>=2.0.0
10
+ Dynamic: author
11
+ Dynamic: author-email
12
+ Dynamic: home-page
13
+ Dynamic: requires-dist
14
+ Dynamic: requires-python
15
+ Dynamic: summary
@@ -0,0 +1,90 @@
1
+ # event-bus
2
+
3
+ A minimal, internal Python library for decoupled event communication using channels and wildcard-capable handlers.
4
+
5
+ ---
6
+
7
+ ## ๐Ÿš€ Features
8
+
9
+ - ๐Ÿ”Œ Pluggable event channels (console, file, or custom)
10
+ - ๐ŸŽฏ Wildcard-matching event handlers (`user.*`, `*`)
11
+ - ๐Ÿ” Emits and triggers events independently
12
+ - ๐Ÿงผ No third-party dependencies
13
+ - ๐Ÿงฉ Fully configurable via Python dict or environment variables
14
+
15
+ ---
16
+
17
+ ## ๐Ÿ“ฆ Installation
18
+
19
+ To manually install the package, download the `vaitty_eventbus-X.X.X-py3-none-any.whl` file from the `dist` directory and run the following command:
20
+
21
+ ```bash
22
+ pip install vaitty_eventbus-X.X.X-py3-none-any.whl
23
+ ```
24
+
25
+ ## ๐Ÿ›  Configuration
26
+
27
+ You can configure event-bus through a dictionary or environment variables:
28
+
29
+ - **EVENTBUS_CHANNEL**: Determines the channel type to use: `console`, `file`, `http`, or a registered custom type. Default: `console`
30
+ - **EVENTBUS_FILE**: For the `file` channel, the path to the log file. Default: `events.log`
31
+ - **EVENTBUS_HTTP_ENDPOINT**: For the `http` channel, the target HTTP endpoint. Default: `http://example.com/events`
32
+
33
+ ### Example using environment variables:
34
+ ```bash
35
+ export EVENTBUS_CHANNEL=file
36
+ export EVENTBUS_FILE=/tmp/my-events.log
37
+ ```
38
+
39
+ ### Example using a configuration dictionary:
40
+ ```python
41
+ import event-bus as events
42
+
43
+ config = {
44
+ "EVENTBUS_CHANNEL": "http",
45
+ "EVENTBUS_HTTP_ENDPOINT": "http://api.my-events.com/ingest"
46
+ }
47
+ handler = events.EventHandlerFactory.from_config(config=config)
48
+ ```
49
+
50
+ ## ๐ŸŒฟ Usage
51
+
52
+ Here's a simple example demonstrating basic usage of `event-bus`:
53
+
54
+ ```python
55
+ import event-bus as events
56
+
57
+ def handle_all(payload: Dict[str, Any]):
58
+ print(f"[*] Received: {payload}")
59
+
60
+ def handle_user(payload: Dict[str, Any]):
61
+ print(f"[user.*] User event: {payload}")
62
+
63
+ def handle_login(payload: Dict[str, Any]):
64
+ print(f"[user.login] Specific login: {payload['user_id']}")
65
+
66
+ handler = events.EventHandlerFactory.from_config()
67
+
68
+ # Register wildcard and specific listeners
69
+ handler.on("*", handle_all)
70
+ handler.on("user.*", handle_user)
71
+ handler.on("user.login", handle_login)
72
+
73
+ # Emit an event
74
+ handler.emit("user.login", {"user_id": 42})
75
+
76
+ # Trigger an event
77
+ event = Event("user.login", {"user_id": 42})
78
+ handler.trigger(event)
79
+
80
+ ```
81
+
82
+ ## ๐Ÿ”ฎ Extensibility
83
+
84
+ The library is designed to be easily extended:
85
+
86
+ 1. Add new `EventChannel` subclasses.
87
+ 2. Register them with `EventHandlerFactory.register_channel`.
88
+ 3. Configure their behavior through the dictionary or environment variables.
89
+
90
+ This makes it simple to integrate `event-bus` into diverse workflows and systems!
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ from setuptools import setup
2
+
3
+ setup(
4
+ name="vaitty_eventbus",
5
+ version="0.0.1",
6
+ description="A minimal, internal Python library for decoupled event communication using channels and wildcard-capable handlers.",
7
+ author="Tech Data AI",
8
+ author_email="tech-data-ai@rapihogar.com",
9
+ url="https://bitbucket.org/rapihogar/event-bus",
10
+ packages=["vaitty_eventbus"],
11
+ package_dir={"": "src"},
12
+ install_requires=[
13
+ 'requests>=2.0.0',
14
+ ],
15
+ python_requires='>=3.7',
16
+ )
@@ -0,0 +1,14 @@
1
+ from .channels import ConsoleEventChannel, FileEventChannel, HttpEventChannel
2
+ from .core import Event, EventHandler, EventChannel, EventBusError
3
+ from .factory import EventHandlerFactory
4
+
5
+ __all__ = [
6
+ "Event",
7
+ "EventBusError",
8
+ "EventHandler",
9
+ "EventChannel",
10
+ "EventHandlerFactory",
11
+ "ConsoleEventChannel",
12
+ "FileEventChannel",
13
+ "HttpEventChannel"
14
+ ]
@@ -0,0 +1,98 @@
1
+ from typing import Dict
2
+
3
+ import requests
4
+
5
+ from .core import EventChannel, Event, EventBusError
6
+
7
+
8
+ class ConsoleEventChannel(EventChannel):
9
+ """
10
+ Provides a channel for sending events to the console.
11
+
12
+ This class is a specific implementation of the EventChannel base class
13
+ and is designed to handle events by outputting them to the standard
14
+ console. It is useful for debugging or simple event logging where
15
+ persistent storage or advanced handling of events is not required.
16
+ """
17
+
18
+ @classmethod
19
+ def from_config(cls, config: Dict[str, str]):
20
+ return cls()
21
+
22
+ def send(self, event: Event) -> None:
23
+ print(f"[ConsoleChannel] Event sent: {event}")
24
+
25
+
26
+ class FileEventChannel(EventChannel):
27
+ """
28
+ Handles event logging to a specified file.
29
+
30
+ This class inherits from EventChannel and is used to log events to a
31
+ file. It appends the event data to the specified file in text format.
32
+ Useful for persistent logging or debugging purposes.
33
+
34
+ :ivar filename: Path to the file where events are logged.
35
+ :type filename: str
36
+ """
37
+
38
+ def __init__(self, filename: str = "events.log"):
39
+ self.filename = filename
40
+
41
+ @classmethod
42
+ def from_config(cls, config: Dict[str, str]):
43
+ filename = config.get("EVENTBUS_FILE", "events.log")
44
+ return cls(filename)
45
+
46
+ def send(self, event: Event) -> None:
47
+ with open(self.filename, 'a') as f:
48
+ f.write(f"{event}\n")
49
+
50
+
51
+ class HttpEventChannel(EventChannel):
52
+ """
53
+ Implements an HTTP-based event channel.
54
+
55
+ Sends events as HTTP POST requests to a configured endpoint. This class
56
+ is useful when working with systems that accept event ingestion via HTTP
57
+ APIs.
58
+
59
+ :ivar endpoint: The HTTP endpoint URI to send events to.
60
+ :type endpoint: str
61
+ :ivar headers: Optional HTTP headers to send with the requests.
62
+ :type headers: dict
63
+ """
64
+
65
+ def __init__(self, endpoint: str, headers: dict = None):
66
+ """
67
+ Initializes the HTTP event channel.
68
+
69
+ :param endpoint: The target URL endpoint for event ingestion.
70
+ :type endpoint: str
71
+ :param headers: Additional HTTP headers to include in the request.
72
+ :type headers: dict, optional
73
+ """
74
+ self.endpoint = endpoint
75
+ self.headers = headers or {}
76
+
77
+ @classmethod
78
+ def from_config(cls, config: Dict[str, str]):
79
+ endpoint = config.get("EVENTBUS_HTTP_ENDPOINT", "http://example.com/events")
80
+ return cls(endpoint=endpoint, headers={})
81
+
82
+ def send(self, event: Event) -> None:
83
+ """
84
+ Sends an event as an HTTP POST request.
85
+
86
+ :param event: The event to send.
87
+ :type event: Event
88
+ :raises EventBusError: If the request fails.
89
+ """
90
+ try:
91
+ response = requests.post(
92
+ self.endpoint,
93
+ json=event.to_dict(),
94
+ headers=self.headers
95
+ )
96
+ response.raise_for_status()
97
+ except Exception as error:
98
+ raise EventBusError(f"Failed to send event: {error}")
@@ -0,0 +1,163 @@
1
+ import fnmatch
2
+ from abc import ABC, abstractmethod
3
+ from typing import Dict, Any, Callable, List
4
+
5
+
6
+ class EventBusError(Exception):
7
+ pass
8
+
9
+
10
+ class Event:
11
+ """
12
+ Represents an event with a topic and associated payload.
13
+
14
+ This class is designed to encapsulate an event, typically used in event-driven
15
+ systems or messaging patterns. The class ensures that the topic is a string and
16
+ the payload is a dictionary. It provides a representation method for easy
17
+ inspection of event instances.
18
+
19
+ :ivar topic: The topic of the event.
20
+ :type topic: str
21
+ :ivar payload: The payload of the event, containing related data.
22
+ :type payload: Dict[str, Any]
23
+ """
24
+
25
+ def __init__(self, topic: str, payload: Dict[str, Any]):
26
+ if not isinstance(topic, str):
27
+ raise TypeError("Event topic must be a string.")
28
+ if not isinstance(payload, dict):
29
+ raise TypeError("Event payload must be a dictionary.")
30
+ self.topic = topic
31
+ self.payload = payload
32
+
33
+ def __repr__(self):
34
+ return f"Event(topic={self.topic!r}, payload={self.payload!r})"
35
+
36
+ def __str__(self):
37
+ return f"Event(topic={self.topic!r}, payload={self.payload!r})"
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ """
41
+ Converts the event instance to a dictionary representation.
42
+
43
+ :return: A dictionary containing the topic and payload of the event.
44
+ """
45
+ return {
46
+ 'topic': self.topic,
47
+ 'payload': self.payload,
48
+ }
49
+
50
+
51
+ class EventChannel(ABC):
52
+ """
53
+ Defines an abstract base class for an event channel.
54
+
55
+ The purpose of this class is to serve as a blueprint for creating
56
+ concrete event channels that can send events. It ensures that
57
+ all subclasses implement the `send` method to handle the process
58
+ of dispatching events.
59
+ """
60
+
61
+ @abstractmethod
62
+ def send(self, event: Event) -> None:
63
+ pass
64
+
65
+ @classmethod
66
+ @abstractmethod
67
+ def from_config(cls, config: Dict[str, str]) -> 'EventChannel':
68
+ """
69
+ Creates an instance of the class based on the provided configuration.
70
+
71
+ :param config: A dictionary containing configuration properties.
72
+ :return: An initialized instance of the class.
73
+ """
74
+ pass
75
+
76
+ @classmethod
77
+ def validate_config(cls, config: Dict[str, str], required_keys: List[str]) -> None:
78
+ """
79
+ Validates that all required keys are present in the provided configuration.
80
+
81
+ :param config: A dictionary containing configuration properties.
82
+ :param required_keys: A list of keys that must be present.
83
+ :raises ValueError: If any of the required keys are missing.
84
+ """
85
+ missing = [key for key in required_keys if key not in config]
86
+ if missing:
87
+ raise ValueError(f"Missing required configuration keys: {missing}")
88
+
89
+
90
+ class EventHandler:
91
+ """
92
+ Handles event-based communication within a system.
93
+
94
+ This class is responsible for managing subscription to event topics, emitting events to a channel,
95
+ and triggering callbacks registered for specific topics. Its primary purpose is to provide a
96
+ centralized mechanism for event-driven programming, where subscribers can listen and respond
97
+ to specific topics without direct coupling between components.
98
+
99
+ :ivar channel: The communication channel through which events are sent.
100
+ :type channel: EventChannel
101
+ :ivar _handlers: A mapping of topic patterns to lists of callback functions subscribed to the
102
+ respective patterns.
103
+ :type _handlers: Dict[str, List[Callable[[Dict[str, Any]], None]]]
104
+ """
105
+
106
+ def __init__(self, channel: EventChannel):
107
+ self.channel = channel
108
+ self._handlers: Dict[str, List[Callable[[Dict[str, Any]], None]]] = {}
109
+
110
+ def emit(self, topic: str, payload: Dict[str, Any]) -> None:
111
+ """
112
+ Emit an event using the specified topic and payload values.
113
+
114
+ Creates an event with the given topic and payload, and sends it
115
+ through the associated channel instance.
116
+
117
+ :param topic: The topic to associate with the event.
118
+ :param payload: A dictionary containing the event payload data.
119
+ :return: None
120
+ """
121
+ event = Event(topic, payload)
122
+ self.channel.send(event)
123
+
124
+ def on(self, topic: str, callback: Callable[[Dict[str, Any]], None]) -> None:
125
+ """
126
+ Registers a callback function for a specific topic. If the topic does not already
127
+ exist in the handlers, it initializes a new list for the topic and appends the
128
+ callback to it.
129
+
130
+ The callback will be triggered when the associated topic is processed.
131
+
132
+ :param topic: The name of the topic to register the callback for.
133
+ :type topic: str
134
+ :param callback: A callable function that will handle messages with the given
135
+ topic. The callable must accept a dictionary containing message data.
136
+ :type callback: Callable[[Dict[str, Any]], None]
137
+ :return: None
138
+ """
139
+ if topic not in self._handlers:
140
+ self._handlers[topic] = []
141
+ self._handlers[topic].append(callback)
142
+
143
+ def trigger(self, event: Event) -> None:
144
+ """
145
+ Triggers the execution of all callbacks that are associated with patterns
146
+ matching the topic of the given event. It iterates through all registered
147
+ patterns and checks if the event's topic matches any pattern. If a match
148
+ is found, all associated callbacks for the matching pattern are executed
149
+ with the event payload as an argument. This allows for dynamic and flexible
150
+ event handling based on topic patterns.
151
+
152
+ :param event: The event object containing a topic and payload.
153
+ :type event: Event
154
+ :return: None
155
+ """
156
+ matched = set()
157
+
158
+ for pattern, callbacks in self._handlers.items():
159
+ if fnmatch.fnmatch(event.topic, pattern):
160
+ matched.update(callbacks)
161
+
162
+ for callback in matched:
163
+ callback(event.payload)
@@ -0,0 +1,44 @@
1
+ from typing import Dict, Type, Optional
2
+
3
+ from .channels import ConsoleEventChannel, FileEventChannel, HttpEventChannel, EventChannel
4
+ from .core import EventHandler
5
+
6
+
7
+ class EventHandlerFactory:
8
+ _registered_channels: Dict[str, Type[EventChannel]] = {
9
+ "console": ConsoleEventChannel,
10
+ "file": FileEventChannel,
11
+ "http": HttpEventChannel,
12
+ }
13
+
14
+ @classmethod
15
+ def register_channel(cls, channel_type: str, channel_class: Type[EventChannel]) -> None:
16
+ """
17
+ Registers a custom channel type to the channel registry.
18
+
19
+ :param channel_type: The unique string identifier for the custom channel.
20
+ :param channel_class: The class implementing the EventChannel for this type.
21
+ """
22
+ if not issubclass(channel_class, EventChannel):
23
+ raise TypeError("channel_class must be a subclass of EventChannel")
24
+ cls._registered_channels[channel_type.lower()] = channel_class
25
+
26
+ @staticmethod
27
+ def from_config(config: Optional[Dict[str, str]] = None) -> EventHandler:
28
+ """
29
+ Creates an EventHandler instance based on the provided configuration.
30
+
31
+ :param config: A dictionary containing configuration properties.
32
+ :return: An EventHandler instance configured with the appropriate channel.
33
+ """
34
+ if config is None:
35
+ config = {}
36
+
37
+ channel_type = config.get("EVENTBUS_CHANNEL", "console").lower()
38
+ channel_class = EventHandlerFactory._registered_channels.get(channel_type)
39
+
40
+ if channel_class is None:
41
+ raise ValueError(f"Unsupported channel type: {channel_type}")
42
+
43
+ channel = channel_class.from_config(config)
44
+ return EventHandler(channel)
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaitty_eventbus
3
+ Version: 0.0.1
4
+ Summary: A minimal, internal Python library for decoupled event communication using channels and wildcard-capable handlers.
5
+ Home-page: https://bitbucket.org/rapihogar/event-bus
6
+ Author: Tech Data AI
7
+ Author-email: tech-data-ai@rapihogar.com
8
+ Requires-Python: >=3.7
9
+ Requires-Dist: requests>=2.0.0
10
+ Dynamic: author
11
+ Dynamic: author-email
12
+ Dynamic: home-page
13
+ Dynamic: requires-dist
14
+ Dynamic: requires-python
15
+ Dynamic: summary
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ src/vaitty_eventbus/__init__.py
5
+ src/vaitty_eventbus/channels.py
6
+ src/vaitty_eventbus/core.py
7
+ src/vaitty_eventbus/factory.py
8
+ src/vaitty_eventbus.egg-info/PKG-INFO
9
+ src/vaitty_eventbus.egg-info/SOURCES.txt
10
+ src/vaitty_eventbus.egg-info/dependency_links.txt
11
+ src/vaitty_eventbus.egg-info/requires.txt
12
+ src/vaitty_eventbus.egg-info/top_level.txt
13
+ tests/test_channels.py
14
+ tests/test_core.py
15
+ tests/test_factory.py
@@ -0,0 +1 @@
1
+ requests>=2.0.0
@@ -0,0 +1 @@
1
+ vaitty_eventbus
@@ -0,0 +1,55 @@
1
+ import unittest
2
+ from unittest.mock import patch, mock_open, MagicMock
3
+
4
+ from src.vaitty_eventbus import ConsoleEventChannel, Event, FileEventChannel, HttpEventChannel, EventBusError
5
+
6
+
7
+ class TestChannels(unittest.TestCase):
8
+ def test_console_event_channel(self):
9
+ channel = ConsoleEventChannel()
10
+ with patch('builtins.print') as mocked_print:
11
+ event = Event("test.topic", {"key": "value"})
12
+ channel.send(event)
13
+ mocked_print.assert_called_once_with(
14
+ "[ConsoleChannel] Event sent: Event(topic='test.topic', payload={'key': 'value'})")
15
+
16
+ def test_file_event_channel(self):
17
+ channel = FileEventChannel("test.log")
18
+ event = Event("test.topic", {"key": "value"})
19
+ with patch("builtins.open", mock_open()) as mocked_file:
20
+ channel.send(event)
21
+ mocked_file.assert_called_once_with("test.log", "a")
22
+ mocked_file().write.assert_called_once_with("Event(topic='test.topic', payload={'key': 'value'})\n")
23
+
24
+ def test_http_event_channel(self):
25
+ channel = HttpEventChannel("http://example.com/events")
26
+ event = Event("test.topic", {"key": "value"})
27
+
28
+ with patch('requests.post') as mocked_post:
29
+ # Simulate a successful response
30
+ mocked_response = MagicMock()
31
+ mocked_response.raise_for_status.return_value = None
32
+ mocked_post.return_value = mocked_response
33
+
34
+ channel.send(event)
35
+
36
+ mocked_post.assert_called_once_with(
37
+ "http://example.com/events",
38
+ json={"topic": "test.topic", "payload": {"key": "value"}},
39
+ headers={}
40
+ )
41
+
42
+ mocked_response.raise_for_status.assert_called_once()
43
+
44
+ def test_http_event_channel_failure(self):
45
+ channel = HttpEventChannel("http://example.com/events")
46
+ event = Event("test.topic", {"key": "value"})
47
+
48
+ with patch('requests.post') as mocked_post:
49
+ # Simulate a failed response
50
+ mocked_post.side_effect = Exception("Failed to send request")
51
+
52
+ try:
53
+ channel.send(event)
54
+ except EventBusError as e:
55
+ self.assertEqual(str(e), "Failed to send event: Failed to send request")
@@ -0,0 +1,73 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock
3
+
4
+ from src.vaitty_eventbus import Event, EventChannel, EventHandler
5
+
6
+
7
+ class TestCore(unittest.TestCase):
8
+ def test_event_initialization(self):
9
+ event = Event("test.topic", {"key": "value"})
10
+ self.assertEqual(event.topic, "test.topic")
11
+ self.assertEqual(event.payload, {"key": "value"})
12
+
13
+ def test_event_repr(self):
14
+ event = Event("test.topic", {"key": "value"})
15
+ self.assertEqual(repr(event), "Event(topic='test.topic', payload={'key': 'value'})")
16
+
17
+ def test_event_invalid_topic_type(self):
18
+ with self.assertRaises(TypeError) as context:
19
+ Event(123, {"key": "value"})
20
+ self.assertEqual(str(context.exception), "Event topic must be a string.")
21
+
22
+ def test_event_invalid_payload_type(self):
23
+ with self.assertRaises(TypeError) as context:
24
+ Event("test.topic", ["invalid_payload"])
25
+ self.assertEqual(str(context.exception), "Event payload must be a dictionary.")
26
+
27
+ def test_event_handler_emit(self):
28
+ channel = MagicMock(spec=EventChannel)
29
+ handler = EventHandler(channel)
30
+ event_topic = "test.topic"
31
+ event_payload = {"key": "value"}
32
+
33
+ handler.emit(event_topic, event_payload)
34
+ channel.send.assert_called_once()
35
+ sent_event = channel.send.call_args[0][0]
36
+ self.assertEqual(sent_event.topic, event_topic)
37
+ self.assertEqual(sent_event.payload, event_payload)
38
+
39
+ def test_event_handler_on_and_trigger(self):
40
+ channel = MagicMock(spec=EventChannel)
41
+ handler = EventHandler(channel)
42
+
43
+ mock_callback = MagicMock()
44
+ handler.on("test.topic", mock_callback)
45
+
46
+ event = Event("test.topic", {"key": "value"})
47
+ handler.trigger(event)
48
+
49
+ mock_callback.assert_called_once_with({"key": "value"})
50
+
51
+ def test_event_handler_trigger_with_wildcard(self):
52
+ channel = MagicMock(spec=EventChannel)
53
+ handler = EventHandler(channel)
54
+
55
+ mock_callback = MagicMock()
56
+ handler.on("test.*", mock_callback)
57
+
58
+ event = Event("test.topic", {"key": "value"})
59
+ handler.trigger(event)
60
+
61
+ mock_callback.assert_called_once_with({"key": "value"})
62
+
63
+ def test_event_handler_trigger_no_match(self):
64
+ channel = MagicMock(spec=EventChannel)
65
+ handler = EventHandler(channel)
66
+
67
+ mock_callback = MagicMock()
68
+ handler.on("other.topic", mock_callback)
69
+
70
+ event = Event("test.topic", {"key": "value"})
71
+ handler.trigger(event)
72
+
73
+ mock_callback.assert_not_called()
@@ -0,0 +1,43 @@
1
+ import unittest
2
+ from typing import Dict
3
+
4
+ from src.vaitty_eventbus import EventHandlerFactory, EventChannel, Event, ConsoleEventChannel, FileEventChannel, \
5
+ HttpEventChannel
6
+
7
+
8
+ class TestFactory(unittest.TestCase):
9
+ def test_factory_from_config_console(self):
10
+ handler = EventHandlerFactory.from_config()
11
+ self.assertIsInstance(handler.channel, ConsoleEventChannel)
12
+
13
+ def test_factory_from_config_file(self):
14
+ config = {"EVENTBUS_CHANNEL": "file", "EVENTBUS_FILE": "test.log"}
15
+ handler = EventHandlerFactory.from_config(config=config)
16
+ self.assertIsInstance(handler.channel, FileEventChannel)
17
+ self.assertEqual(handler.channel.filename, "test.log")
18
+
19
+ def test_factory_from_config_http(self):
20
+ config = {"EVENTBUS_CHANNEL": "http", "EVENTBUS_HTTP_ENDPOINT": "http://example.com/events"}
21
+ handler = EventHandlerFactory.from_config(config=config)
22
+ self.assertIsInstance(handler.channel, HttpEventChannel)
23
+ self.assertEqual(handler.channel.endpoint, "http://example.com/events")
24
+
25
+ def test_factory_invalid_channel(self):
26
+ invalid_config = {"EVENTBUS_CHANNEL": "unsupported"}
27
+ with self.assertRaises(ValueError) as context:
28
+ EventHandlerFactory.from_config(invalid_config)
29
+ self.assertEqual(str(context.exception), "Unsupported channel type: unsupported")
30
+
31
+ def test_factory_custom_channel(self):
32
+ class CustomEventChannel(EventChannel):
33
+ @classmethod
34
+ def from_config(cls, config: Dict[str, str]):
35
+ return cls()
36
+
37
+ def send(self, event: Event) -> None:
38
+ pass
39
+
40
+ EventHandlerFactory.register_channel("custom", CustomEventChannel)
41
+ config = {"EVENTBUS_CHANNEL": "custom"}
42
+ handler = EventHandlerFactory.from_config(config=config)
43
+ self.assertIsInstance(handler.channel, CustomEventChannel)