busline 0.3.1__py3-none-any.whl → 0.5.1__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.
Files changed (53) hide show
  1. busline/client/client.py +27 -0
  2. busline/{eventbus_client → client}/eventbus_connector.py +5 -10
  3. busline/client/multiclient.py +41 -0
  4. busline/client/publisher/publisher.py +60 -0
  5. busline/client/pubsub_client.py +151 -0
  6. busline/client/subscriber/event_handler/closure_event_handler.py +18 -0
  7. busline/client/subscriber/event_handler/event_handler.py +15 -0
  8. busline/client/subscriber/event_handler/multi_handler.py +30 -0
  9. busline/client/subscriber/subscriber.py +109 -0
  10. busline/client/subscriber/topic_subscriber.py +79 -0
  11. busline/event/event.py +43 -21
  12. busline/event/registry.py +67 -0
  13. busline/event/test.py +47 -0
  14. busline/exceptions.py +8 -0
  15. busline/local/__init__.py +3 -0
  16. busline/local/eventbus/__init__.py +0 -0
  17. busline/local/eventbus/async_local_eventbus.py +26 -0
  18. busline/local/eventbus/eventbus.py +86 -0
  19. busline/local/eventbus/local_eventbus.py +23 -0
  20. busline/local/local_pubsub_client.py +54 -0
  21. busline/local/publisher/__init__.py +0 -0
  22. busline/local/publisher/local_publisher.py +38 -0
  23. busline/local/subscriber/__init__.py +0 -0
  24. busline/local/subscriber/local_subscriber.py +43 -0
  25. busline/local/test.py +156 -0
  26. busline-0.5.1.dist-info/METADATA +215 -0
  27. busline-0.5.1.dist-info/RECORD +36 -0
  28. {busline-0.3.1.dist-info → busline-0.5.1.dist-info}/WHEEL +1 -1
  29. busline/event/event_content.py +0 -18
  30. busline/event/event_metadata.py +0 -26
  31. busline/eventbus/async_local_eventbus.py +0 -32
  32. busline/eventbus/eventbus.py +0 -112
  33. busline/eventbus/exceptions.py +0 -2
  34. busline/eventbus/queued_local_eventbus.py +0 -50
  35. busline/eventbus/topic.py +0 -35
  36. busline/eventbus_client/eventbus_client.py +0 -99
  37. busline/eventbus_client/exceptions.py +0 -4
  38. busline/eventbus_client/local_eventbus_client.py +0 -25
  39. busline/eventbus_client/publisher/local_eventbus_publisher.py +0 -35
  40. busline/eventbus_client/publisher/publisher.py +0 -59
  41. busline/eventbus_client/subscriber/closure_event_listener.py +0 -19
  42. busline/eventbus_client/subscriber/event_listener.py +0 -15
  43. busline/eventbus_client/subscriber/local_eventbus_closure_subscriber.py +0 -17
  44. busline/eventbus_client/subscriber/local_eventbus_subscriber.py +0 -40
  45. busline/eventbus_client/subscriber/subscriber.py +0 -93
  46. busline-0.3.1.dist-info/METADATA +0 -111
  47. busline-0.3.1.dist-info/RECORD +0 -30
  48. /busline/{eventbus → client}/__init__.py +0 -0
  49. /busline/{eventbus_client → client/publisher}/__init__.py +0 -0
  50. /busline/{eventbus_client/publisher → client/subscriber}/__init__.py +0 -0
  51. /busline/{eventbus_client/subscriber → client/subscriber/event_handler}/__init__.py +0 -0
  52. {busline-0.3.1.dist-info → busline-0.5.1.dist-info/licenses}/LICENSE +0 -0
  53. {busline-0.3.1.dist-info → busline-0.5.1.dist-info}/top_level.txt +0 -0
@@ -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 is used as base class to create a component which interacts with eventbus
10
+ Abstract class which provides methods to interact with eventbus
8
11
 
9
12
  Author: Nicola Ricciardi
10
13
  """
11
14
 
12
- def __init__(self, connector_id: str = str(uuid4())):
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,151 @@
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.client.subscriber.event_handler.event_handler import EventHandler
8
+ from busline.client.subscriber.topic_subscriber import TopicSubscriber
9
+ from busline.event.event import Event
10
+ from busline.client.subscriber.subscriber import Subscriber
11
+
12
+
13
+ @dataclass
14
+ class PubSubClient(EventBusClient):
15
+ """
16
+ Eventbus client which should used by components which wouldn't be a publisher/subscriber, but they need them
17
+
18
+ Author: Nicola Ricciardi
19
+ """
20
+
21
+ publishers: List[Publisher]
22
+ subscribers: List[Subscriber]
23
+
24
+ @classmethod
25
+ def from_pubsub(cls, publisher: Optional[Publisher] = None, subscriber: Optional[Subscriber] = None) -> Self:
26
+
27
+ publishers = []
28
+ if publisher is not None:
29
+ publishers = [publisher]
30
+
31
+ subscribers = []
32
+ if subscriber is not None:
33
+ subscribers = [subscriber]
34
+
35
+ return cls(publishers, subscribers)
36
+
37
+ @classmethod
38
+ def from_pubsub_client(cls, client: Self) -> Self:
39
+ return cls(client.publishers.copy(), client.subscribers.copy())
40
+
41
+ @override
42
+ async def connect(self):
43
+ """
44
+ Connect all publishers and subscribers
45
+ """
46
+
47
+ tasks = [publisher.connect() for publisher in self.publishers]
48
+ tasks += [subscriber.connect() for subscriber in self.subscribers]
49
+
50
+ await asyncio.gather(*tasks)
51
+
52
+ @override
53
+ async def disconnect(self):
54
+ """
55
+ Disconnect all publishers and subscribers
56
+ """
57
+
58
+ tasks = [publisher.disconnect() for publisher in self.publishers]
59
+ tasks += [subscriber.disconnect() for subscriber in self.subscribers]
60
+
61
+ await asyncio.gather(*tasks)
62
+
63
+ @override
64
+ async def publish(self, topic: str, event: Event, **kwargs):
65
+ """
66
+ Publish event using all publishers
67
+ """
68
+
69
+ await asyncio.gather(*[
70
+ publisher.publish(topic, event, **kwargs) for publisher in self.publishers
71
+ ])
72
+
73
+ @override
74
+ async def subscribe(self, topic: str, **kwargs):
75
+ """
76
+ Subscribe all subscribers on topic
77
+ """
78
+
79
+ await asyncio.gather(*[
80
+ subscriber.subscribe(topic, **kwargs) for subscriber in self.subscribers
81
+ ])
82
+
83
+ @override
84
+ async def unsubscribe(self, topic: Optional[str] = None, **kwargs):
85
+ """
86
+ Alias of `client.subscriber.unsubscribe(...)`
87
+ """
88
+
89
+ await asyncio.gather(*[
90
+ subscriber.unsubscribe(topic, **kwargs) for subscriber in self.subscribers
91
+ ])
92
+
93
+
94
+ @dataclass
95
+ class PubSubTopicClient(PubSubClient):
96
+ """
97
+ Eventbus client which should used by components which wouldn't be a publisher/subscriber, but they need them
98
+
99
+ Author: Nicola Ricciardi
100
+ """
101
+
102
+ subscribers: List[TopicSubscriber]
103
+
104
+ @override
105
+ async def subscribe(self, topic: str, handler: Optional[EventHandler] = None, **kwargs):
106
+ """
107
+ Subscribe all subscribers on topic
108
+ """
109
+
110
+ await asyncio.gather(*[
111
+ subscriber.subscribe(topic, handler=handler, **kwargs) for subscriber in self.subscribers
112
+ ])
113
+
114
+
115
+ @dataclass
116
+ class PubSubClientBuilder:
117
+ """
118
+ Builder for a pub/sub client.
119
+
120
+ Author: Nicola Ricciardi
121
+ """
122
+
123
+ base_client: PubSubClient = field(
124
+ default_factory=lambda: PubSubClient([], []),
125
+ kw_only=True
126
+ )
127
+
128
+
129
+ def with_publisher(self, publisher: Publisher) -> Self:
130
+ self.base_client.publishers.append(publisher)
131
+
132
+ return self
133
+
134
+ def with_publishers(self, publishers: List[Publisher]) -> Self:
135
+ self.base_client.publishers.extend(publishers)
136
+
137
+ return self
138
+
139
+ def with_subscriber(self, subscriber: Subscriber) -> Self:
140
+ self.base_client.subscribers.append(subscriber)
141
+
142
+ return self
143
+
144
+ def with_subscribers(self, subscribers: List[Subscriber]) -> Self:
145
+ self.base_client.subscribers.extend(subscribers)
146
+
147
+ return self
148
+
149
+ def build(self) -> PubSubClient:
150
+ return self.base_client
151
+
@@ -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 CHANGED
@@ -1,25 +1,47 @@
1
+ import json
1
2
  import uuid
2
- from busline.event.event_content import EventContent
3
- from busline.event.event_metadata import EventMetadata
3
+ from typing import Any, Self
4
+ import datetime
5
+ from dataclasses import dataclass, field
6
+ from typing import Optional
4
7
 
5
8
 
9
+ @dataclass(frozen=True)
6
10
  class Event:
7
-
8
- def __init__(self, content: EventContent = None, metadata: EventMetadata = EventMetadata()):
9
-
10
- self._identifier = str(uuid.uuid4())
11
- self._content = content
12
- self._metadata = metadata
13
-
14
-
15
- @property
16
- def identifier(self) -> str:
17
- return self._identifier
18
-
19
- @property
20
- def content(self) -> EventContent:
21
- return self._content
22
-
23
- @property
24
- def metadata(self) -> EventMetadata:
25
- return self._metadata
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
+ )