busline 0.3.0__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- busline/client/client.py +27 -0
- {eventbus_client → busline/client}/eventbus_connector.py +5 -10
- busline/client/multiclient.py +41 -0
- busline/client/publisher/publisher.py +60 -0
- busline/client/pubsub_client.py +128 -0
- busline/client/subscriber/event_handler/closure_event_handler.py +18 -0
- busline/client/subscriber/event_handler/event_handler.py +15 -0
- busline/client/subscriber/event_handler/multi_handler.py +30 -0
- busline/client/subscriber/subscriber.py +109 -0
- busline/client/subscriber/topic_subscriber.py +79 -0
- busline/event/event.py +47 -0
- busline/event/registry.py +67 -0
- busline/event/test.py +47 -0
- busline/exceptions.py +8 -0
- busline/local/__init__.py +3 -0
- busline/local/eventbus/__init__.py +0 -0
- busline/local/eventbus/async_local_eventbus.py +26 -0
- busline/local/eventbus/eventbus.py +86 -0
- busline/local/eventbus/local_eventbus.py +23 -0
- busline/local/local_pubsub_client.py +54 -0
- busline/local/publisher/__init__.py +0 -0
- busline/local/publisher/local_publisher.py +38 -0
- busline/local/subscriber/__init__.py +0 -0
- busline/local/subscriber/local_subscriber.py +43 -0
- busline/local/test.py +156 -0
- busline-0.5.0.dist-info/METADATA +215 -0
- busline-0.5.0.dist-info/RECORD +36 -0
- {busline-0.3.0.dist-info → busline-0.5.0.dist-info}/WHEEL +1 -1
- busline-0.5.0.dist-info/top_level.txt +1 -0
- busline-0.3.0.dist-info/METADATA +0 -104
- busline-0.3.0.dist-info/RECORD +0 -30
- busline-0.3.0.dist-info/top_level.txt +0 -4
- event/event.py +0 -25
- event/event_content.py +0 -18
- event/event_metadata.py +0 -26
- eventbus/async_local_eventbus.py +0 -32
- eventbus/eventbus.py +0 -112
- eventbus/exceptions.py +0 -2
- eventbus/queued_local_eventbus.py +0 -50
- eventbus/topic.py +0 -35
- eventbus_client/eventbus_client.py +0 -99
- eventbus_client/exceptions.py +0 -4
- eventbus_client/local_eventbus_client.py +0 -25
- eventbus_client/publisher/local_eventbus_publisher.py +0 -35
- eventbus_client/publisher/publisher.py +0 -59
- eventbus_client/subscriber/closure_event_listener.py +0 -19
- eventbus_client/subscriber/event_listener.py +0 -15
- eventbus_client/subscriber/local_eventbus_closure_subscriber.py +0 -17
- eventbus_client/subscriber/local_eventbus_subscriber.py +0 -40
- eventbus_client/subscriber/subscriber.py +0 -93
- /__init__.py → /busline/__init__.py +0 -0
- {event → busline/client}/__init__.py +0 -0
- {eventbus → busline/client/publisher}/__init__.py +0 -0
- {eventbus_client → busline/client/subscriber}/__init__.py +0 -0
- {eventbus_client/publisher → busline/client/subscriber/event_handler}/__init__.py +0 -0
- {eventbus_client/subscriber → busline/event}/__init__.py +0 -0
- {busline-0.3.0.dist-info → busline-0.5.0.dist-info/licenses}/LICENSE +0 -0
busline/client/client.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from busline.event.event import Event
|
5
|
+
|
6
|
+
|
7
|
+
class EventBusClient(ABC):
|
8
|
+
|
9
|
+
@abstractmethod
|
10
|
+
async def connect(self):
|
11
|
+
pass
|
12
|
+
|
13
|
+
@abstractmethod
|
14
|
+
async def disconnect(self):
|
15
|
+
pass
|
16
|
+
|
17
|
+
@abstractmethod
|
18
|
+
async def publish(self, topic: str, event: Event, **kwargs):
|
19
|
+
pass
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
async def subscribe(self, topic: str, **kwargs):
|
23
|
+
pass
|
24
|
+
|
25
|
+
@abstractmethod
|
26
|
+
async def unsubscribe(self, topic: Optional[str] = None, **kwargs):
|
27
|
+
pass
|
@@ -1,24 +1,19 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
2
|
from uuid import uuid4
|
3
|
+
from typing import Optional
|
4
|
+
from dataclasses import dataclass, field
|
3
5
|
|
4
6
|
|
7
|
+
@dataclass(kw_only=True)
|
5
8
|
class EventBusConnector(ABC):
|
6
9
|
"""
|
7
|
-
Abstract class which
|
10
|
+
Abstract class which provides methods to interact with eventbus
|
8
11
|
|
9
12
|
Author: Nicola Ricciardi
|
10
13
|
"""
|
11
14
|
|
12
|
-
|
13
|
-
self._id = connector_id
|
15
|
+
identifier: str = field(default=str(uuid4()))
|
14
16
|
|
15
|
-
@property
|
16
|
-
def id(self) -> str:
|
17
|
-
return self._id
|
18
|
-
|
19
|
-
@id.setter
|
20
|
-
def id(self, value):
|
21
|
-
self._id = value
|
22
17
|
|
23
18
|
@abstractmethod
|
24
19
|
async def connect(self):
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import asyncio
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import List, Optional, override
|
4
|
+
|
5
|
+
from busline.client.client import EventBusClient
|
6
|
+
from busline.event.event import Event
|
7
|
+
|
8
|
+
|
9
|
+
@dataclass
|
10
|
+
class EventBusMultiClient(EventBusClient):
|
11
|
+
|
12
|
+
clients: List[EventBusClient]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
def from_client(cls, client: EventBusClient):
|
16
|
+
return cls([client])
|
17
|
+
|
18
|
+
@override
|
19
|
+
async def connect(self):
|
20
|
+
tasks = [client.connect() for client in self.clients]
|
21
|
+
await asyncio.gather(*tasks)
|
22
|
+
|
23
|
+
@override
|
24
|
+
async def disconnect(self):
|
25
|
+
tasks = [client.disconnect() for client in self.clients]
|
26
|
+
await asyncio.gather(*tasks)
|
27
|
+
|
28
|
+
@override
|
29
|
+
async def publish(self, topic: str, event: Event, **kwargs):
|
30
|
+
tasks = [client.publish(topic, event, **kwargs) for client in self.clients]
|
31
|
+
await asyncio.gather(*tasks)
|
32
|
+
|
33
|
+
@override
|
34
|
+
async def subscribe(self, topic: str, **kwargs):
|
35
|
+
tasks = [client.subscribe(topic, **kwargs) for client in self.clients]
|
36
|
+
await asyncio.gather(*tasks)
|
37
|
+
|
38
|
+
@override
|
39
|
+
async def unsubscribe(self, topic: Optional[str] = None, **kwargs):
|
40
|
+
tasks = [client.unsubscribe(topic, **kwargs) for client in self.clients]
|
41
|
+
await asyncio.gather(*tasks)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import logging
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from busline.event.event import Event
|
5
|
+
from busline.client.eventbus_connector import EventBusConnector
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class Publisher(EventBusConnector, ABC):
|
10
|
+
"""
|
11
|
+
Abstract class which can be implemented by your components which must be able to publish on eventbus
|
12
|
+
|
13
|
+
Author: Nicola Ricciardi
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __repr__(self) -> str:
|
17
|
+
return f"Publisher({self.identifier})"
|
18
|
+
|
19
|
+
@abstractmethod
|
20
|
+
async def _internal_publish(self, topic: str, event: Event, **kwargs):
|
21
|
+
"""
|
22
|
+
Actual publish on topic the event
|
23
|
+
|
24
|
+
:param topic:
|
25
|
+
:param event:
|
26
|
+
:return:
|
27
|
+
"""
|
28
|
+
|
29
|
+
async def publish(self, topic: str, event: Event, **kwargs):
|
30
|
+
"""
|
31
|
+
Publish on topic the event
|
32
|
+
|
33
|
+
:param topic:
|
34
|
+
:param event:
|
35
|
+
:return:
|
36
|
+
"""
|
37
|
+
|
38
|
+
logging.info(f"{self}: publish on {topic} -> {event}")
|
39
|
+
await self.on_publishing(topic, event, **kwargs)
|
40
|
+
await self._internal_publish(topic, event, **kwargs)
|
41
|
+
await self.on_published(topic, event, **kwargs)
|
42
|
+
|
43
|
+
|
44
|
+
async def on_publishing(self, topic: str, event: Event, **kwargs):
|
45
|
+
"""
|
46
|
+
Callback called on publishing start
|
47
|
+
|
48
|
+
:param topic:
|
49
|
+
:param event:
|
50
|
+
:return:
|
51
|
+
"""
|
52
|
+
|
53
|
+
async def on_published(self, topic: str, event: Event, **kwargs):
|
54
|
+
"""
|
55
|
+
Callback called on publishing end
|
56
|
+
|
57
|
+
:param topic:
|
58
|
+
:param event:
|
59
|
+
:return:
|
60
|
+
"""
|
@@ -0,0 +1,128 @@
|
|
1
|
+
import asyncio
|
2
|
+
from dataclasses import dataclass, field
|
3
|
+
from typing import Optional, override, List, Self
|
4
|
+
|
5
|
+
from busline.client.client import EventBusClient
|
6
|
+
from busline.client.publisher.publisher import Publisher
|
7
|
+
from busline.event.event import Event
|
8
|
+
from busline.client.subscriber.subscriber import Subscriber
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class PubSubClient(EventBusClient):
|
13
|
+
"""
|
14
|
+
Eventbus client which should used by components which wouldn't be a publisher/subscriber, but they need them
|
15
|
+
|
16
|
+
Author: Nicola Ricciardi
|
17
|
+
"""
|
18
|
+
|
19
|
+
publishers: List[Publisher]
|
20
|
+
subscribers: List[Subscriber]
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def from_pubsub(cls, publisher: Optional[Publisher] = None, subscriber: Optional[Subscriber] = None) -> Self:
|
24
|
+
|
25
|
+
publishers = []
|
26
|
+
if publisher is not None:
|
27
|
+
publishers = [publisher]
|
28
|
+
|
29
|
+
subscribers = []
|
30
|
+
if subscriber is not None:
|
31
|
+
subscribers = [subscriber]
|
32
|
+
|
33
|
+
return cls(publishers, subscribers)
|
34
|
+
|
35
|
+
@classmethod
|
36
|
+
def from_pubsub_client(cls, client: 'PubSubClient') -> Self:
|
37
|
+
return cls(client.publishers.copy(), client.subscribers.copy())
|
38
|
+
|
39
|
+
@override
|
40
|
+
async def connect(self):
|
41
|
+
"""
|
42
|
+
Connect all publishers and subscribers
|
43
|
+
"""
|
44
|
+
|
45
|
+
tasks = [publisher.connect() for publisher in self.publishers]
|
46
|
+
tasks += [subscriber.connect() for subscriber in self.subscribers]
|
47
|
+
|
48
|
+
await asyncio.gather(*tasks)
|
49
|
+
|
50
|
+
@override
|
51
|
+
async def disconnect(self):
|
52
|
+
"""
|
53
|
+
Disconnect all publishers and subscribers
|
54
|
+
"""
|
55
|
+
|
56
|
+
tasks = [publisher.disconnect() for publisher in self.publishers]
|
57
|
+
tasks += [subscriber.disconnect() for subscriber in self.subscribers]
|
58
|
+
|
59
|
+
await asyncio.gather(*tasks)
|
60
|
+
|
61
|
+
@override
|
62
|
+
async def publish(self, topic: str, event: Event, **kwargs):
|
63
|
+
"""
|
64
|
+
Publish event using all publishers
|
65
|
+
"""
|
66
|
+
|
67
|
+
await asyncio.gather(*[
|
68
|
+
publisher.publish(topic, event, **kwargs) for publisher in self.publishers
|
69
|
+
])
|
70
|
+
|
71
|
+
@override
|
72
|
+
async def subscribe(self, topic: str, **kwargs):
|
73
|
+
"""
|
74
|
+
Subscribe all subscribers on topic
|
75
|
+
"""
|
76
|
+
|
77
|
+
await asyncio.gather(*[
|
78
|
+
subscriber.subscribe(topic, **kwargs) for subscriber in self.subscribers
|
79
|
+
])
|
80
|
+
|
81
|
+
@override
|
82
|
+
async def unsubscribe(self, topic: Optional[str] = None, **kwargs):
|
83
|
+
"""
|
84
|
+
Alias of `client.subscriber.unsubscribe(...)`
|
85
|
+
"""
|
86
|
+
|
87
|
+
await asyncio.gather(*[
|
88
|
+
subscriber.unsubscribe(topic, **kwargs) for subscriber in self.subscribers
|
89
|
+
])
|
90
|
+
|
91
|
+
|
92
|
+
@dataclass
|
93
|
+
class PubSubClientBuilder:
|
94
|
+
"""
|
95
|
+
Builder for a pub/sub client.
|
96
|
+
|
97
|
+
Author: Nicola Ricciardi
|
98
|
+
"""
|
99
|
+
|
100
|
+
base_client: PubSubClient = field(
|
101
|
+
default_factory=lambda: PubSubClient([], []),
|
102
|
+
kw_only=True
|
103
|
+
)
|
104
|
+
|
105
|
+
|
106
|
+
def with_publisher(self, publisher: Publisher) -> Self:
|
107
|
+
self.base_client.publishers.append(publisher)
|
108
|
+
|
109
|
+
return self
|
110
|
+
|
111
|
+
def with_publishers(self, publishers: List[Publisher]) -> Self:
|
112
|
+
self.base_client.publishers.extend(publishers)
|
113
|
+
|
114
|
+
return self
|
115
|
+
|
116
|
+
def with_subscriber(self, subscriber: Subscriber) -> Self:
|
117
|
+
self.base_client.subscribers.append(subscriber)
|
118
|
+
|
119
|
+
return self
|
120
|
+
|
121
|
+
def with_subscribers(self, subscribers: List[Subscriber]) -> Self:
|
122
|
+
self.base_client.subscribers.extend(subscribers)
|
123
|
+
|
124
|
+
return self
|
125
|
+
|
126
|
+
def build(self) -> PubSubClient:
|
127
|
+
return self.base_client
|
128
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Callable
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from busline.event.event import Event
|
4
|
+
from busline.client.subscriber.event_handler.event_handler import EventHandler
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class ClosureEventHandler(EventHandler):
|
9
|
+
"""
|
10
|
+
Event handler which use a pre-defined callback as `on_event`
|
11
|
+
|
12
|
+
Author: Nicola Ricciardi
|
13
|
+
"""
|
14
|
+
|
15
|
+
on_event_callback: Callable[[str, Event], None]
|
16
|
+
|
17
|
+
async def handle(self, topic: str, event: Event):
|
18
|
+
self.on_event_callback(topic, event)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from busline.event.event import Event
|
3
|
+
|
4
|
+
|
5
|
+
class EventHandler(ABC):
|
6
|
+
|
7
|
+
@abstractmethod
|
8
|
+
async def handle(self, topic: str, event: Event):
|
9
|
+
"""
|
10
|
+
Manage an event of a topic
|
11
|
+
|
12
|
+
:param topic:
|
13
|
+
:param event:
|
14
|
+
:return:
|
15
|
+
"""
|
@@ -0,0 +1,30 @@
|
|
1
|
+
from typing import List
|
2
|
+
import asyncio
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from busline.event.event import Event
|
5
|
+
from busline.client.subscriber.event_handler.event_handler import EventHandler
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class MultiEventHandler(EventHandler):
|
10
|
+
"""
|
11
|
+
Call a batch of pre-defined handlers.
|
12
|
+
|
13
|
+
Another parameter can be specified. If strict order is False, async capabilities can be exploited
|
14
|
+
|
15
|
+
Author: Nicola Ricciardi
|
16
|
+
"""
|
17
|
+
|
18
|
+
handlers: List[EventHandler]
|
19
|
+
strict_order: bool = False
|
20
|
+
|
21
|
+
|
22
|
+
async def handle(self, topic: str, event: Event):
|
23
|
+
if self.strict_order:
|
24
|
+
for handler in self.handlers:
|
25
|
+
await handler.handle(topic, event)
|
26
|
+
else:
|
27
|
+
tasks = [handler.handle(topic, event) for handler in self.handlers]
|
28
|
+
|
29
|
+
await asyncio.gather(*tasks)
|
30
|
+
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import logging
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from busline.client.eventbus_connector import EventBusConnector
|
7
|
+
from busline.event.event import Event
|
8
|
+
|
9
|
+
|
10
|
+
@dataclass
|
11
|
+
class Subscriber(EventBusConnector, ABC):
|
12
|
+
"""
|
13
|
+
Abstract class which can be implemented by your components which must be able to subscribe on eventbus
|
14
|
+
|
15
|
+
Author: Nicola Ricciardi
|
16
|
+
"""
|
17
|
+
|
18
|
+
def __repr__(self) -> str:
|
19
|
+
return f"Subscriber({self.identifier})"
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
async def on_event(self, topic: str, event: Event):
|
23
|
+
"""
|
24
|
+
Callback called when an event arrives from a topic
|
25
|
+
"""
|
26
|
+
|
27
|
+
async def notify(self, topic: str, event: Event, **kwargs):
|
28
|
+
"""
|
29
|
+
Notify subscriber
|
30
|
+
"""
|
31
|
+
|
32
|
+
logging.info(f"{self}: incoming event on {topic} -> {event}")
|
33
|
+
await self.on_event(topic, event)
|
34
|
+
|
35
|
+
@abstractmethod
|
36
|
+
async def _internal_subscribe(self, topic: str, **kwargs):
|
37
|
+
"""
|
38
|
+
Actual subscribe to topic
|
39
|
+
|
40
|
+
:param topic:
|
41
|
+
:return:
|
42
|
+
"""
|
43
|
+
|
44
|
+
@abstractmethod
|
45
|
+
async def _internal_unsubscribe(self, topic: Optional[str] = None, **kwargs):
|
46
|
+
"""
|
47
|
+
Actual unsubscribe to topic
|
48
|
+
|
49
|
+
:param topic:
|
50
|
+
:return:
|
51
|
+
"""
|
52
|
+
|
53
|
+
async def subscribe(self, topic: str, **kwargs):
|
54
|
+
"""
|
55
|
+
Subscribe to topic
|
56
|
+
|
57
|
+
:param topic:
|
58
|
+
:return:
|
59
|
+
"""
|
60
|
+
|
61
|
+
logging.info(f"{self}: subscribe on topic {topic}")
|
62
|
+
await self._on_subscribing(topic, **kwargs)
|
63
|
+
await self._internal_subscribe(topic, **kwargs)
|
64
|
+
await self._on_subscribed(topic, **kwargs)
|
65
|
+
|
66
|
+
async def unsubscribe(self, topic: Optional[str] = None, **kwargs):
|
67
|
+
"""
|
68
|
+
Unsubscribe to topic
|
69
|
+
|
70
|
+
:param topic:
|
71
|
+
:return:
|
72
|
+
"""
|
73
|
+
|
74
|
+
logging.info(f"{self}: unsubscribe from topic {topic}")
|
75
|
+
await self._on_unsubscribing(topic, **kwargs)
|
76
|
+
await self._internal_unsubscribe(topic, **kwargs)
|
77
|
+
await self._on_unsubscribed(topic, **kwargs)
|
78
|
+
|
79
|
+
async def _on_subscribing(self, topic: str, **kwargs):
|
80
|
+
"""
|
81
|
+
Callback called on subscribing
|
82
|
+
|
83
|
+
:param topic:
|
84
|
+
:return:
|
85
|
+
"""
|
86
|
+
|
87
|
+
async def _on_subscribed(self, topic: str, **kwargs):
|
88
|
+
"""
|
89
|
+
Callback called on subscribed
|
90
|
+
|
91
|
+
:param topic:
|
92
|
+
:return:
|
93
|
+
"""
|
94
|
+
|
95
|
+
async def _on_unsubscribing(self, topic: Optional[str], **kwargs):
|
96
|
+
"""
|
97
|
+
Callback called on unsubscribing
|
98
|
+
|
99
|
+
:param topic:
|
100
|
+
:return:
|
101
|
+
"""
|
102
|
+
|
103
|
+
async def _on_unsubscribed(self, topic: Optional[str], **kwargs):
|
104
|
+
"""
|
105
|
+
Callback called on unsubscribed
|
106
|
+
|
107
|
+
:param topic:
|
108
|
+
:return:
|
109
|
+
"""
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
from abc import ABC
|
4
|
+
from typing import Dict, List, Callable, Optional, override
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from busline.client.subscriber.event_handler.event_handler import EventHandler
|
7
|
+
from busline.client.subscriber.subscriber import Subscriber
|
8
|
+
from busline.event.event import Event
|
9
|
+
from busline.exceptions import EventHandlerNotFound
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass(kw_only=True)
|
13
|
+
class TopicSubscriber(Subscriber, ABC):
|
14
|
+
"""
|
15
|
+
Handles different topic events using ad hoc handlers defined by user,
|
16
|
+
else it uses fallback handler if provided (otherwise throws an exception)
|
17
|
+
|
18
|
+
Attributes:
|
19
|
+
fallback_event_handler: event handler used for a topic if no event handler is specified for that topic
|
20
|
+
handlers: event handler for each topic (i.e. key); notice that key can also be a string with wildcards
|
21
|
+
topic_names_matcher: function used to check match between two topic name (with wildcards); default "t1 == t2"
|
22
|
+
event_handler_always_required: raise an exception if no handlers are found for a topic
|
23
|
+
|
24
|
+
Author: Nicola Ricciardi
|
25
|
+
"""
|
26
|
+
|
27
|
+
fallback_event_handler: Optional[EventHandler] = field(default=None)
|
28
|
+
handlers: Dict[str, EventHandler] = field(default_factory=dict)
|
29
|
+
topic_names_matcher: Callable[[str, str], bool] = field(repr=False, default=lambda t1, t2: t1 == t2)
|
30
|
+
event_handler_always_required: bool = field(default=False)
|
31
|
+
|
32
|
+
@override
|
33
|
+
async def _on_subscribing(self, topic: str, handler: Optional[EventHandler] = None, **kwargs):
|
34
|
+
|
35
|
+
if self.fallback_event_handler is None:
|
36
|
+
if self.event_handler_always_required:
|
37
|
+
raise EventHandlerNotFound()
|
38
|
+
else:
|
39
|
+
logging.warning(f"{self}: event handler for topic '{topic}' not found")
|
40
|
+
|
41
|
+
@override
|
42
|
+
async def _on_subscribed(self, topic: str, handler: Optional[EventHandler] = None, **kwargs):
|
43
|
+
|
44
|
+
self.handlers[topic] = handler
|
45
|
+
|
46
|
+
@override
|
47
|
+
async def _on_unsubscribed(self, topic: str | None, **kwargs):
|
48
|
+
|
49
|
+
if topic is None:
|
50
|
+
self.handlers = {}
|
51
|
+
else:
|
52
|
+
del self.handlers[topic]
|
53
|
+
|
54
|
+
def __get_handlers_of_topic(self, topic: str) -> List[EventHandler]:
|
55
|
+
|
56
|
+
handlers = []
|
57
|
+
for t, h in self.handlers.items():
|
58
|
+
if self.topic_names_matcher(topic, t):
|
59
|
+
if h is not None:
|
60
|
+
handlers.append(h)
|
61
|
+
else:
|
62
|
+
if self.fallback_event_handler is not None:
|
63
|
+
handlers.append(self.fallback_event_handler)
|
64
|
+
else:
|
65
|
+
if self.event_handler_always_required:
|
66
|
+
raise EventHandlerNotFound()
|
67
|
+
else:
|
68
|
+
logging.warning(f"{self}: event handler for topic '{topic}' not found")
|
69
|
+
|
70
|
+
return handlers
|
71
|
+
|
72
|
+
@override
|
73
|
+
async def on_event(self, topic: str, event: Event):
|
74
|
+
|
75
|
+
handlers_of_topic: List[EventHandler] = self.__get_handlers_of_topic(topic)
|
76
|
+
|
77
|
+
if len(handlers_of_topic) > 0:
|
78
|
+
tasks = [handler.handle(topic, event) for handler in handlers_of_topic]
|
79
|
+
await asyncio.gather(*tasks)
|
busline/event/event.py
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
import json
|
2
|
+
import uuid
|
3
|
+
from typing import Any, Self
|
4
|
+
import datetime
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
|
9
|
+
@dataclass(frozen=True)
|
10
|
+
class Event:
|
11
|
+
"""
|
12
|
+
Event publishable in an eventbus
|
13
|
+
|
14
|
+
Author: Nicola Ricciardi
|
15
|
+
"""
|
16
|
+
|
17
|
+
identifier: str = field(default=str(uuid.uuid4()))
|
18
|
+
content: Any = field(default=None)
|
19
|
+
content_type: Optional[str] = field(default=None)
|
20
|
+
event_type: Optional[str] = field(default=None)
|
21
|
+
timestamp: float = field(default_factory=lambda: datetime.datetime.now(datetime.timezone.utc).timestamp())
|
22
|
+
metadata: dict = field(default_factory=dict)
|
23
|
+
|
24
|
+
@classmethod
|
25
|
+
def from_json(cls, json_str: str) -> Self:
|
26
|
+
"""
|
27
|
+
Build event object from JSON string
|
28
|
+
"""
|
29
|
+
|
30
|
+
return cls(**json.loads(json_str))
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def from_event(cls, event: 'Event') -> Self:
|
34
|
+
"""
|
35
|
+
Build event object based on Event (base) class.
|
36
|
+
|
37
|
+
This method can be useful when Event class is inherited and an event registry is used.
|
38
|
+
"""
|
39
|
+
|
40
|
+
return cls(
|
41
|
+
identifier=event.identifier,
|
42
|
+
content=event.content,
|
43
|
+
content_type=event.content_type,
|
44
|
+
event_type=event.event_type,
|
45
|
+
timestamp=event.timestamp,
|
46
|
+
metadata=event.metadata
|
47
|
+
)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Dict, Type
|
3
|
+
|
4
|
+
from busline.event.event import Event
|
5
|
+
|
6
|
+
class _Singleton(type):
|
7
|
+
_instances = {}
|
8
|
+
|
9
|
+
def __call__(cls, *args, **kwargs):
|
10
|
+
if cls not in cls._instances:
|
11
|
+
cls._instances[cls] = super().__call__(*args, **kwargs)
|
12
|
+
return cls._instances[cls]
|
13
|
+
|
14
|
+
|
15
|
+
@dataclass
|
16
|
+
class EventRegistry(metaclass=_Singleton):
|
17
|
+
"""
|
18
|
+
Registry to manage different event types
|
19
|
+
|
20
|
+
Author: Nicola Ricciardi
|
21
|
+
"""
|
22
|
+
|
23
|
+
associations: Dict[str, Type[Event]] = field(default_factory=dict)
|
24
|
+
|
25
|
+
def unregister(self, event_type: str):
|
26
|
+
"""
|
27
|
+
Remove an event type association
|
28
|
+
"""
|
29
|
+
|
30
|
+
self.associations.pop(event_type)
|
31
|
+
|
32
|
+
def register(self, event_type: str, event_class: Type[Event]):
|
33
|
+
"""
|
34
|
+
Add a new association between an event type and an event class
|
35
|
+
"""
|
36
|
+
|
37
|
+
self.associations[event_type] = event_class
|
38
|
+
|
39
|
+
def retrive_class(self, event) -> Type[Event]:
|
40
|
+
"""
|
41
|
+
Retrive event class of event input based on saved associations and given event type
|
42
|
+
|
43
|
+
KeyError is raised if no association is found
|
44
|
+
"""
|
45
|
+
|
46
|
+
return self.associations[event.event_type]
|
47
|
+
|
48
|
+
def convert(self, event: Event, raise_on_miss: bool = True) -> Event:
|
49
|
+
"""
|
50
|
+
Convert a generic event, auto-building the right event class based on event type.
|
51
|
+
|
52
|
+
If raise_on_miss=True, a KeyError exception is raised. Otherwise, input is returned in output.
|
53
|
+
"""
|
54
|
+
|
55
|
+
if event.event_type not in self.associations and not raise_on_miss:
|
56
|
+
return event
|
57
|
+
|
58
|
+
event_class: Type[Event] = self.retrive_class(event)
|
59
|
+
|
60
|
+
return event_class.from_event(event)
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
|
67
|
+
|