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,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
+
busline/event/test.py ADDED
@@ -0,0 +1,47 @@
1
+ import unittest
2
+ from typing import Type
3
+
4
+ from busline.event.event import Event
5
+ from busline.event.registry import EventRegistry
6
+
7
+
8
+ class Event1(Event):
9
+ def my_value1(self) -> int:
10
+ return self.content
11
+
12
+ class Event2(Event):
13
+ def my_value2(self) -> int:
14
+ return self.content
15
+
16
+ class TestEventRegistry(unittest.TestCase):
17
+
18
+ def test(self):
19
+
20
+ event_registry = EventRegistry() # singleton
21
+
22
+ event_registry.register("event1", Event1)
23
+ event_registry.register("event2", Event2)
24
+
25
+ event_registry = EventRegistry() # singleton
26
+
27
+ generic_event1 = Event(content=1, event_type="event1")
28
+ generic_event2 = Event(content=2, event_type="event2")
29
+ generic_unknown_event = Event(content=2, event_type="unknown")
30
+
31
+ event1: Event1 = event_registry.convert(generic_event1)
32
+
33
+ self.assertEqual(event1.event_type, "event1")
34
+ self.assertEqual(event1.my_value1(), 1)
35
+
36
+ event2_class = event_registry.retrive_class(generic_event2)
37
+
38
+ self.assertIs(event2_class, Event2)
39
+
40
+ event2_class: Type[Event2] = event2_class
41
+
42
+ event2 = event2_class.from_event(generic_event2)
43
+
44
+ self.assertEqual(event2.event_type, "event2")
45
+ self.assertEqual(event2.my_value2(), 2)
46
+
47
+ self.assertRaises(KeyError, lambda: event_registry.retrive_class(generic_unknown_event))
busline/exceptions.py ADDED
@@ -0,0 +1,8 @@
1
+ class TopicNotFound(Exception):
2
+ pass
3
+
4
+ class EventBusClientNotConnected(Exception):
5
+ pass
6
+
7
+ class EventHandlerNotFound(Exception):
8
+ pass
@@ -0,0 +1,3 @@
1
+ from busline.local.eventbus.async_local_eventbus import AsyncLocalEventBus
2
+
3
+ DEFAULT_EVENT_BUS_INSTANCE = AsyncLocalEventBus()
File without changes
@@ -0,0 +1,26 @@
1
+ import logging
2
+ import asyncio
3
+ from dataclasses import dataclass
4
+ from busline.event.event import Event
5
+ from busline.local.eventbus.eventbus import EventBus
6
+
7
+
8
+ @dataclass
9
+ class AsyncLocalEventBus(EventBus):
10
+ """
11
+ Async local eventbus
12
+
13
+ Author: Nicola Ricciardi
14
+ """
15
+
16
+ async def put_event(self, topic: str, event: Event):
17
+
18
+ topic_subscriptions = self._get_topic_subscriptions(topic)
19
+
20
+ logging.debug(f"new event {event} on topic {topic}, notify subscribers: {topic_subscriptions}")
21
+
22
+ tasks = [subscriber.on_event(topic, event) for subscriber in topic_subscriptions]
23
+
24
+ await asyncio.gather(*tasks)
25
+
26
+
@@ -0,0 +1,86 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass, field
3
+ from typing import Dict, List, Optional
4
+ from busline.exceptions import TopicNotFound
5
+ from busline.client.subscriber.subscriber import Subscriber
6
+ from busline.event.event import Event
7
+
8
+
9
+ @dataclass
10
+ class EventBus(ABC):
11
+ """
12
+ Abstract class used as base for new eventbus implemented in local projects.
13
+
14
+ Author: Nicola Ricciardi
15
+ """
16
+
17
+ subscriptions: Dict[str, List[Subscriber]] = field(default_factory=dict)
18
+
19
+ def __post_init__(self):
20
+
21
+ self.reset_subscriptions()
22
+
23
+ def reset_subscriptions(self):
24
+ self.subscriptions = {}
25
+
26
+ @property
27
+ def topics(self) -> List[str]:
28
+ return list(self.subscriptions.keys())
29
+
30
+ def add_subscriber(self, topic: str, subscriber: Subscriber):
31
+ """
32
+ Add subscriber to topic
33
+
34
+ :param topic:
35
+ :param subscriber:
36
+ :return:
37
+ """
38
+
39
+ self.subscriptions.setdefault(topic, [])
40
+ self.subscriptions[topic].append(subscriber)
41
+
42
+ def remove_subscriber(self, subscriber: Subscriber, topic: Optional[str] = None, raise_if_topic_missed: bool = False):
43
+ """
44
+ Remove subscriber from topic selected or from all if topic is None
45
+
46
+ :param raise_if_topic_missed:
47
+ :param subscriber:
48
+ :param topic:
49
+ :return:
50
+ """
51
+
52
+ if raise_if_topic_missed and topic is not None and topic not in self.subscriptions.keys():
53
+ raise TopicNotFound(f"topic '{topic}' not found")
54
+
55
+ for name in self.subscriptions.keys():
56
+
57
+ if topic is None or self._topic_names_match(topic, name):
58
+ if subscriber in self.subscriptions[name]:
59
+ self.subscriptions[name].remove(subscriber)
60
+
61
+
62
+ def _topic_names_match(self, t1: str, t2: str):
63
+ return t1 == t2
64
+
65
+ def _get_topic_subscriptions(self, topic: str) -> List[Subscriber]:
66
+
67
+ topic_subscriptions: List[Subscriber] = []
68
+ for t, subs in self.subscriptions.items():
69
+ if self._topic_names_match(t, topic):
70
+ topic_subscriptions.extend(subs)
71
+
72
+ return topic_subscriptions
73
+
74
+ @abstractmethod
75
+ async def put_event(self, topic: str, event: Event):
76
+ """
77
+ Put a new event in the bus and notify subscribers of corresponding
78
+ event's topic
79
+
80
+ :param topic:
81
+ :param event:
82
+ :return:
83
+ """
84
+
85
+ raise NotImplemented()
86
+
@@ -0,0 +1,23 @@
1
+ from busline.event.event import Event
2
+ from busline.local import DEFAULT_EVENT_BUS_INSTANCE
3
+ from busline.local.eventbus.eventbus import EventBus
4
+
5
+
6
+ class LocalEventBus(EventBus):
7
+ """
8
+ Local *singleton* event bus instance
9
+
10
+ Author: Nicola Ricciardi
11
+ """
12
+
13
+ # === SINGLETON pattern ===
14
+ _instance = None
15
+
16
+ def __new__(cls, *args, **kwargs):
17
+ if cls._instance is None:
18
+ cls._instance = DEFAULT_EVENT_BUS_INSTANCE # super().__new__(cls)
19
+
20
+ return cls._instance
21
+
22
+ async def put_event(self, topic: str, event: Event):
23
+ return self._instance.put_event(topic, event)
@@ -0,0 +1,54 @@
1
+ from typing import Callable, Optional, Self, override
2
+ from dataclasses import dataclass, field
3
+ from busline.client.publisher.publisher import Publisher
4
+ from busline.client.subscriber.event_handler.closure_event_handler import ClosureEventHandler
5
+ from busline.event.event import Event
6
+ from busline.client.pubsub_client import PubSubClient, PubSubClientBuilder
7
+ from busline.local.eventbus.eventbus import EventBus
8
+ from busline.local.eventbus.local_eventbus import LocalEventBus
9
+ from busline.local.publisher.local_publisher import LocalEventBusPublisher
10
+ from busline.local.subscriber.local_subscriber import LocalEventBusSubscriber
11
+
12
+
13
+ @dataclass
14
+ class LocalPubSubClient(PubSubClient):
15
+ pass
16
+
17
+
18
+ @dataclass
19
+ class LocalPubSubClientBuilder(PubSubClientBuilder):
20
+ """
21
+ Builder for a local pub/sub client.
22
+
23
+ EventBus fed in init will be used to build publishers and subscribers
24
+
25
+ Author: Nicola Ricciardi
26
+ """
27
+
28
+ eventbus: EventBus = field(default_factory=LocalEventBus)
29
+
30
+ def with_default_publisher(self) -> Self:
31
+ """"
32
+ LocalEventBusPublisher coupled with eventbus is used
33
+ """
34
+
35
+ self.base_client.publishers.append(
36
+ LocalEventBusPublisher(eventbus=self.eventbus)
37
+ )
38
+
39
+ return self
40
+
41
+ def with_closure_subscriber(self, closure: Callable[[str, Event], None]) -> Self:
42
+ self.base_client.subscribers.append(
43
+ LocalEventBusSubscriber(
44
+ eventbus=self.eventbus,
45
+ fallback_event_handler=ClosureEventHandler(closure)
46
+ )
47
+ )
48
+
49
+ return self
50
+
51
+ @override
52
+ def build(self) -> LocalPubSubClient:
53
+ return LocalPubSubClient.from_pubsub_client(self.base_client)
54
+
File without changes
@@ -0,0 +1,38 @@
1
+ import logging
2
+ from typing import override
3
+
4
+ from busline.client.publisher.publisher import Publisher
5
+ from busline.event.event import Event
6
+ from busline.local.eventbus.eventbus import EventBus
7
+ from busline.exceptions import EventBusClientNotConnected
8
+ from dataclasses import dataclass, field
9
+
10
+
11
+ @dataclass
12
+ class LocalEventBusPublisher(Publisher):
13
+ """
14
+ Publisher which works with local eventbus, this class can be initialized and used stand-alone
15
+
16
+ Author: Nicola Ricciardi
17
+ """
18
+
19
+ eventbus: EventBus
20
+ connected: bool = field(default=False)
21
+
22
+ @override
23
+ async def connect(self):
24
+ logging.info(f"publisher {self.identifier} connecting...")
25
+ self.connected = True
26
+
27
+ @override
28
+ async def disconnect(self):
29
+ logging.info(f"publisher {self.identifier} disconnecting...")
30
+ self.connected = False
31
+
32
+ @override
33
+ async def _internal_publish(self, topic_name: str, event: Event, **kwargs):
34
+
35
+ if not self.connected:
36
+ raise EventBusClientNotConnected()
37
+
38
+ await self.eventbus.put_event(topic_name, event)
File without changes
@@ -0,0 +1,43 @@
1
+ import logging
2
+ from dataclasses import dataclass, field
3
+ from typing import Optional, override
4
+
5
+ from busline.client.subscriber.topic_subscriber import TopicSubscriber
6
+ from busline.local.eventbus.eventbus import EventBus
7
+ from busline.exceptions import EventBusClientNotConnected
8
+
9
+
10
+ @dataclass
11
+ class LocalEventBusSubscriber(TopicSubscriber):
12
+ """
13
+ Subscriber topic-based which works with local eventbus
14
+
15
+ Author: Nicola Ricciardi
16
+ """
17
+
18
+ eventbus: EventBus
19
+ connected: bool = field(default=False)
20
+
21
+ @override
22
+ async def connect(self):
23
+ logging.info(f"subscriber {self.identifier} connecting...")
24
+ self.connected = True
25
+
26
+ @override
27
+ async def disconnect(self):
28
+ logging.info(f"subscriber {self.identifier} disconnecting...")
29
+ self.connected = False
30
+
31
+ @override
32
+ async def _internal_subscribe(self, topic: str, **kwargs):
33
+ if not self.connected:
34
+ raise EventBusClientNotConnected()
35
+
36
+ self.eventbus.add_subscriber(topic, self)
37
+
38
+ @override
39
+ async def _internal_unsubscribe(self, topic: Optional[str] = None, **kwargs):
40
+ if not self.connected:
41
+ raise EventBusClientNotConnected()
42
+
43
+ self.eventbus.remove_subscriber(self, topic)
busline/local/test.py ADDED
@@ -0,0 +1,156 @@
1
+ import unittest
2
+
3
+ from busline.client.multiclient import EventBusMultiClient
4
+ from busline.client.subscriber.event_handler.closure_event_handler import ClosureEventHandler
5
+ from busline.local.eventbus.async_local_eventbus import AsyncLocalEventBus
6
+ from busline.local.eventbus.local_eventbus import LocalEventBus
7
+ from busline.local.local_pubsub_client import LocalPubSubClientBuilder
8
+ from busline.local.publisher.local_publisher import LocalEventBusPublisher
9
+ from busline.event.event import Event
10
+ from busline.local.subscriber.local_subscriber import LocalEventBusSubscriber
11
+
12
+
13
+ class TestLocalEventBus(unittest.IsolatedAsyncioTestCase):
14
+
15
+ async def test_async_eventbus(self):
16
+
17
+ local_eventbus_instance1 = LocalEventBus() # singleton
18
+ local_eventbus_instance2 = LocalEventBus() # singleton
19
+
20
+ self.assertIs(local_eventbus_instance1, local_eventbus_instance2) # check singleton
21
+
22
+ event = Event()
23
+ received_event = None
24
+
25
+ def callback(t: str, e: Event):
26
+ nonlocal received_event
27
+
28
+ received_event = e
29
+
30
+ subscriber = LocalEventBusSubscriber(
31
+ eventbus=local_eventbus_instance1,
32
+ fallback_event_handler=ClosureEventHandler(callback)
33
+ )
34
+ publisher = LocalEventBusPublisher(eventbus=local_eventbus_instance2)
35
+
36
+ await subscriber.connect()
37
+ await publisher.connect()
38
+
39
+ await subscriber.subscribe("test")
40
+
41
+ await publisher.publish("test", event)
42
+
43
+ self.assertIs(event, received_event)
44
+
45
+ await subscriber.unsubscribe()
46
+ received_event = None
47
+
48
+ await publisher.publish("test", event)
49
+
50
+ self.assertIs(received_event, None)
51
+
52
+
53
+ async def test_local_client(self):
54
+ received_event = None
55
+ event = Event()
56
+
57
+ def client_callback(topic_name: str, e: Event):
58
+ nonlocal received_event
59
+
60
+ received_event = e
61
+
62
+ client = LocalPubSubClientBuilder()\
63
+ .with_default_publisher()\
64
+ .with_closure_subscriber(client_callback)\
65
+ .build()
66
+
67
+ await client.connect()
68
+
69
+ await client.subscribe("test")
70
+
71
+ await client.publish("test", event)
72
+
73
+ self.assertIs(event, received_event)
74
+
75
+ await client.unsubscribe()
76
+ received_event = None
77
+
78
+ await client.publish("test", event)
79
+
80
+ self.assertIs(received_event, None)
81
+
82
+ async def test_mhs(self):
83
+
84
+ received_event = 0
85
+
86
+ def callback(t: str, e: Event):
87
+ nonlocal received_event
88
+
89
+ received_event += 1
90
+
91
+ subscriber = LocalEventBusSubscriber(
92
+ fallback_event_handler=ClosureEventHandler(callback),
93
+ eventbus=LocalEventBus()
94
+ )
95
+
96
+ await subscriber.connect()
97
+
98
+ await subscriber.subscribe("t1")
99
+ await subscriber.subscribe("t2", handler=ClosureEventHandler(callback))
100
+
101
+ await subscriber.notify("t1", Event())
102
+
103
+ self.assertEqual(received_event, 1)
104
+
105
+ await subscriber.notify("t2", Event())
106
+
107
+ self.assertEqual(received_event, 2)
108
+
109
+ await subscriber.unsubscribe()
110
+
111
+ await subscriber.notify("t1", Event())
112
+ await subscriber.notify("t2", Event())
113
+
114
+ self.assertEqual(received_event, 2)
115
+
116
+ async def test_multi_client(self):
117
+ local_eventbus_instance1 = AsyncLocalEventBus() # not singleton
118
+ local_eventbus_instance2 = AsyncLocalEventBus() # not singleton
119
+
120
+ n_events: int = 0
121
+
122
+ def on_event_callback(topic_name: str, e: Event):
123
+ nonlocal n_events
124
+
125
+ n_events += 1
126
+
127
+ client1 = LocalPubSubClientBuilder(local_eventbus_instance1)\
128
+ .with_default_publisher()\
129
+ .with_closure_subscriber(lambda t, e: ...)\
130
+ .build()
131
+
132
+ client2 = LocalPubSubClientBuilder(local_eventbus_instance2)\
133
+ .with_default_publisher()\
134
+ .with_closure_subscriber(lambda t, e: ...)\
135
+ .build()
136
+
137
+ multi_client = EventBusMultiClient([
138
+ client1,
139
+ client2
140
+ ])
141
+
142
+ await multi_client.connect()
143
+
144
+ await multi_client.subscribe("topic", handler=ClosureEventHandler(on_event_callback))
145
+
146
+ await multi_client.publish("topic", Event())
147
+
148
+ await multi_client.disconnect()
149
+
150
+ self.assertEqual(n_events, 2)
151
+
152
+
153
+
154
+
155
+ if __name__ == '__main__':
156
+ unittest.main()