delegate-events 0.0.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Dagrofa BI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: delegate-events
3
+ Version: 0.0.0
4
+ Summary: Python implementation of the Event Pattern
5
+ Author-email: Anders Madsen <anders.madsen@alphavue.com>
6
+ License: MIT
7
+ Project-URL: repository, https://github.com/apmadsen/delegate-events
8
+ Keywords: windows,linux,delegates,events
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Development Status :: 6 - Mature
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Natural Language :: English
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: delegate-pattern==0.0.0
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=8.3; extra == "test"
31
+ Requires-Dist: pytest-cov>=6.1; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ [![Test](https://github.com/apmadsen/delegate-events/actions/workflows/python-test.yml/badge.svg)](https://github.com/apmadsen/delegate-events/actions/workflows/python-test.yml)
35
+ [![Coverage](https://github.com/apmadsen/delegate-events/actions/workflows/python-test-coverage.yml/badge.svg)](https://github.com/apmadsen/delegate-events/actions/workflows/python-test-coverage.yml)
36
+ [![Stable Version](https://img.shields.io/pypi/v/delegate-events?label=stable&sort=semver&color=blue)](https://github.com/apmadsen/delegate-events/releases)
37
+ ![Pre-release Version](https://img.shields.io/github/v/release/apmadsen/delegate-events?label=pre-release&include_prereleases&sort=semver&color=blue)
38
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/delegate-events)
39
+ [![PyPI Downloads](https://static.pepy.tech/badge/delegate-events/week)](https://pepy.tech/projects/delegate-events)
40
+
41
+ # delegate-events: Python implementation of the Event Pattern.
42
+
43
+ delegate-events provides a basic implementation of the well-known Event or Pub/Sub Pattern, and is built on top of the `delegate-pattern` package which handles the delegation part.
44
+
45
+ ## Example
46
+
47
+ ```python
48
+ from delegate.pattern import delegate
49
+ from delegate.events import Channel, Event
50
+
51
+ class OnSomethingEvent(Event):
52
+ def __init__(self, what: str, why: str):
53
+ self.what = what
54
+ self.why = why
55
+
56
+ def __str__(self):
57
+ return f"{self.what} happened because of {self.why}"
58
+
59
+ class Class1:
60
+ on_something = delegate(Channel[OnSomethingEvent])
61
+
62
+ inst = Class1()
63
+ msg_queue: list[Event] = []
64
+
65
+ def fn(publisher, event: OnSomethingEvent):
66
+ msg_queue.append(event)
67
+
68
+ inst.on_something.subscribe(fn)
69
+ inst.on_something.fire(OnSomethingEvent("Something", "some other thing"))
70
+
71
+ str(msg_queue[0]) # => "Something happened because of some other thing"
72
+ ```
73
+
74
+ # API
75
+
76
+ ## Event class
77
+ The `Event` class is the base class of all events. Events are the classes holding the event information, when an event is fired, thus, when subclassed, should contain properties/fields of the event information.
78
+
79
+ #### Example
80
+ ```python
81
+ from delegate.events import Event
82
+
83
+ class OnSomething(Event):
84
+ def __init__(self, what: str, why: str):
85
+ self.what = what
86
+ self.why = why
87
+
88
+ def __str__(self):
89
+ return f"{self.what} happened because of {self.why}"
90
+
91
+ ```
92
+
93
+ ## Channel class
94
+ The `Channel` class is the event channel returned by the delegate, and its jobs are handling subscriptions and firing of events.
95
+
96
+ ### Channel.subscribe(handler: _Callable[[object, T\<Event>], None]_)
97
+
98
+ The `subscribe` function subscribes a handler function to the channel. The function takes 1 argument:
99
+ - handler: `Callable[[object, Event], None]`
100
+
101
+ #### Example
102
+ ```python
103
+ from delegate.events import Channel
104
+
105
+ channel = Channel[OnSomethingEvent]()
106
+
107
+ def handler(sender: object, args: OnSomethingEvent) -> None:
108
+ pass
109
+
110
+ channel.subscribe(handler)
111
+ ```
112
+
113
+
114
+ ### Channel.unsubscribe(handler: _Callable[[object, T\<Event>], None]_)
115
+ The `unsubscribe` function unsubscribes a handler function from the channel. The function takes 1 argument:
116
+ - handler: `Callable[[object, Event], None]`
117
+
118
+ ```python
119
+ from delegate.events import Channel
120
+
121
+ channel = Channel[OnSomethingEvent]()
122
+
123
+ def handler(sender: object, args: OnSomethingEvent) -> None:
124
+ pass
125
+
126
+ channel.subscribe(handler)
127
+ channel.unsubscribe(handler)
128
+ ```
129
+
130
+ ### Channel.fire(event: _T\<Event>_, *, ttl: _float | None = 0_) -> _bool_ function
131
+ The `fire` function fires the event, invoking all subscribed handlers. The function returns True if event was successfully sent and takes 1 argument:
132
+ - event: `Event`
133
+
134
+ and 1 optional keyword argument:
135
+ - ttl: `float | None` the time-to-live (seconds) of the event.
136
+
137
+ ```python
138
+ from delegate.events import Channel
139
+
140
+ channel = Channel[OnSomethingEvent]()
141
+
142
+ msg_queue: list[Event] = []
143
+
144
+ def handler(sender: object, event: OnSomethingEvent) -> None:
145
+ msg_queue.append(event)
146
+
147
+ channel.fire(OnSomethingEvent("Something", "some other thing")) # => False
148
+ channel.fire(OnSomethingEvent("Something", "some other thing"), ttl=10) # => False - event stays in queue and will be sent if subcrribed to within 10 seconds though
149
+
150
+ channel.subscribe(handler)
151
+
152
+ channel.fire(OnSomethingEvent("Something", "some other thing")) # => True
153
+
154
+ len(msg_queue) # => 2
155
+ ```
@@ -0,0 +1,122 @@
1
+ [![Test](https://github.com/apmadsen/delegate-events/actions/workflows/python-test.yml/badge.svg)](https://github.com/apmadsen/delegate-events/actions/workflows/python-test.yml)
2
+ [![Coverage](https://github.com/apmadsen/delegate-events/actions/workflows/python-test-coverage.yml/badge.svg)](https://github.com/apmadsen/delegate-events/actions/workflows/python-test-coverage.yml)
3
+ [![Stable Version](https://img.shields.io/pypi/v/delegate-events?label=stable&sort=semver&color=blue)](https://github.com/apmadsen/delegate-events/releases)
4
+ ![Pre-release Version](https://img.shields.io/github/v/release/apmadsen/delegate-events?label=pre-release&include_prereleases&sort=semver&color=blue)
5
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/delegate-events)
6
+ [![PyPI Downloads](https://static.pepy.tech/badge/delegate-events/week)](https://pepy.tech/projects/delegate-events)
7
+
8
+ # delegate-events: Python implementation of the Event Pattern.
9
+
10
+ delegate-events provides a basic implementation of the well-known Event or Pub/Sub Pattern, and is built on top of the `delegate-pattern` package which handles the delegation part.
11
+
12
+ ## Example
13
+
14
+ ```python
15
+ from delegate.pattern import delegate
16
+ from delegate.events import Channel, Event
17
+
18
+ class OnSomethingEvent(Event):
19
+ def __init__(self, what: str, why: str):
20
+ self.what = what
21
+ self.why = why
22
+
23
+ def __str__(self):
24
+ return f"{self.what} happened because of {self.why}"
25
+
26
+ class Class1:
27
+ on_something = delegate(Channel[OnSomethingEvent])
28
+
29
+ inst = Class1()
30
+ msg_queue: list[Event] = []
31
+
32
+ def fn(publisher, event: OnSomethingEvent):
33
+ msg_queue.append(event)
34
+
35
+ inst.on_something.subscribe(fn)
36
+ inst.on_something.fire(OnSomethingEvent("Something", "some other thing"))
37
+
38
+ str(msg_queue[0]) # => "Something happened because of some other thing"
39
+ ```
40
+
41
+ # API
42
+
43
+ ## Event class
44
+ The `Event` class is the base class of all events. Events are the classes holding the event information, when an event is fired, thus, when subclassed, should contain properties/fields of the event information.
45
+
46
+ #### Example
47
+ ```python
48
+ from delegate.events import Event
49
+
50
+ class OnSomething(Event):
51
+ def __init__(self, what: str, why: str):
52
+ self.what = what
53
+ self.why = why
54
+
55
+ def __str__(self):
56
+ return f"{self.what} happened because of {self.why}"
57
+
58
+ ```
59
+
60
+ ## Channel class
61
+ The `Channel` class is the event channel returned by the delegate, and its jobs are handling subscriptions and firing of events.
62
+
63
+ ### Channel.subscribe(handler: _Callable[[object, T\<Event>], None]_)
64
+
65
+ The `subscribe` function subscribes a handler function to the channel. The function takes 1 argument:
66
+ - handler: `Callable[[object, Event], None]`
67
+
68
+ #### Example
69
+ ```python
70
+ from delegate.events import Channel
71
+
72
+ channel = Channel[OnSomethingEvent]()
73
+
74
+ def handler(sender: object, args: OnSomethingEvent) -> None:
75
+ pass
76
+
77
+ channel.subscribe(handler)
78
+ ```
79
+
80
+
81
+ ### Channel.unsubscribe(handler: _Callable[[object, T\<Event>], None]_)
82
+ The `unsubscribe` function unsubscribes a handler function from the channel. The function takes 1 argument:
83
+ - handler: `Callable[[object, Event], None]`
84
+
85
+ ```python
86
+ from delegate.events import Channel
87
+
88
+ channel = Channel[OnSomethingEvent]()
89
+
90
+ def handler(sender: object, args: OnSomethingEvent) -> None:
91
+ pass
92
+
93
+ channel.subscribe(handler)
94
+ channel.unsubscribe(handler)
95
+ ```
96
+
97
+ ### Channel.fire(event: _T\<Event>_, *, ttl: _float | None = 0_) -> _bool_ function
98
+ The `fire` function fires the event, invoking all subscribed handlers. The function returns True if event was successfully sent and takes 1 argument:
99
+ - event: `Event`
100
+
101
+ and 1 optional keyword argument:
102
+ - ttl: `float | None` the time-to-live (seconds) of the event.
103
+
104
+ ```python
105
+ from delegate.events import Channel
106
+
107
+ channel = Channel[OnSomethingEvent]()
108
+
109
+ msg_queue: list[Event] = []
110
+
111
+ def handler(sender: object, event: OnSomethingEvent) -> None:
112
+ msg_queue.append(event)
113
+
114
+ channel.fire(OnSomethingEvent("Something", "some other thing")) # => False
115
+ channel.fire(OnSomethingEvent("Something", "some other thing"), ttl=10) # => False - event stays in queue and will be sent if subcrribed to within 10 seconds though
116
+
117
+ channel.subscribe(handler)
118
+
119
+ channel.fire(OnSomethingEvent("Something", "some other thing")) # => True
120
+
121
+ len(msg_queue) # => 2
122
+ ```
@@ -0,0 +1,48 @@
1
+ [project]
2
+ name = "delegate-events"
3
+ version = "0.0.0"
4
+ description = "Python implementation of the Event Pattern"
5
+ keywords = ["windows", "linux", "delegates", "events"]
6
+ readme = "README.md"
7
+ authors = [
8
+ { name = "Anders Madsen", email = "anders.madsen@alphavue.com" }
9
+ ]
10
+ license = { text = "MIT" }
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Development Status :: 6 - Mature",
14
+ "Operating System :: Microsoft :: Windows",
15
+ "Operating System :: POSIX :: Linux",
16
+ "Intended Audience :: Developers",
17
+ "Natural Language :: English",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: Software Development :: Libraries",
27
+ "Typing :: Typed"
28
+ ]
29
+ dependencies = [
30
+ "delegate-pattern==0.0.0"
31
+ ]
32
+ requires-python = ">=3.10"
33
+
34
+ [project.urls]
35
+ repository = "https://github.com/apmadsen/delegate-events"
36
+
37
+ [project.optional-dependencies]
38
+ test = [
39
+ "pytest>=8.3",
40
+ "pytest-cov>=6.1",
41
+ ]
42
+
43
+ [tool.setuptools-git-versioning]
44
+ enabled = true
45
+
46
+ [build-system]
47
+ requires = ["setuptools >= 77.0.3", "setuptools-git-versioning >= 2.1.0"]
48
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,13 @@
1
+ from delegate.events.core.channel_closed_error import ChannelClosedError
2
+ from delegate.events.core.channel import Channel
3
+ from delegate.events.core.event import Event
4
+ from delegate.events.core.handler_already_subscribed_error import HandlerAlreadySubscribedError
5
+ from delegate.events.core.handler_not_subscribed_error import HandlerNotSubscribedError
6
+
7
+ __all__ = (
8
+ 'Channel',
9
+ 'Event',
10
+ 'ChannelClosedError',
11
+ 'HandlerAlreadySubscribedError',
12
+ 'HandlerNotSubscribedError',
13
+ )
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+ from typing import Generic, TypeVar, MutableSequence, Callable, cast, overload
3
+ from datetime import datetime, timedelta
4
+ from threading import RLock
5
+ from weakref import ref
6
+
7
+ from delegate.events.core.channel_closed_error import ChannelClosedError
8
+ from delegate.events.core.event import Event
9
+ from delegate.events.core.queued_event import QueuedEvent
10
+ from delegate.events.core.handler_already_subscribed_error import HandlerAlreadySubscribedError
11
+ from delegate.events.core.handler_not_subscribed_error import HandlerNotSubscribedError
12
+
13
+ T = TypeVar("T", bound=Event, contravariant=True)
14
+ class Channel(Generic[T]):
15
+ __slots__ = [ "__publisher", "__subscribers", "__queue", "__queue_lock", "__weakref__", "__closed" ]
16
+
17
+ @overload
18
+ def __init__(self) -> None:
19
+ """
20
+ Creates a channel without a publisher. This is meant for testing purposes.
21
+ """
22
+ ...
23
+ @overload
24
+ def __init__(self, delegator: object) -> None:
25
+ """
26
+ Creates a delegate channel for specific publisher (delegator).
27
+ """
28
+ ...
29
+ def __init__(self, delegator: object | None = None):
30
+ self.__publisher = ref(delegator) if delegator is not None else None
31
+ self.__subscribers: MutableSequence[Callable[[object, T], None]] = []
32
+ self.__queue: MutableSequence[QueuedEvent] = []
33
+ self.__queue_lock = RLock()
34
+ self.__closed = False
35
+
36
+ def subscribe(self, handler: Callable[[object, T], None]) -> None:
37
+ """
38
+ Subscribes to the event channel.
39
+
40
+ Args:
41
+ handler (Callable[[object, T], None]): The event handler
42
+ """
43
+ if self.__closed:
44
+ raise ChannelClosedError
45
+
46
+ if handler in self.__subscribers:
47
+ raise HandlerAlreadySubscribedError
48
+
49
+ self.__subscribers.append(handler)
50
+
51
+ with self.__queue_lock:
52
+ for event in self.__queue:
53
+ if not event.expired:
54
+ self.fire(cast(T, event.event))
55
+ self.__queue.clear()
56
+
57
+
58
+ def unsubscribe(self, handler: Callable[[object, T], None]) -> None:
59
+ """
60
+ Unsubscribes handler from event channel.
61
+
62
+ Args:
63
+ handler (Callable[[object, T], None]): The event handler
64
+ """
65
+ if self.__closed:
66
+ raise ChannelClosedError
67
+
68
+ if handler not in self.__subscribers:
69
+ raise HandlerNotSubscribedError
70
+
71
+ self.__subscribers.remove(handler)
72
+
73
+ def fire(self, event: T, *, ttl: float | None = 0) -> bool:
74
+ """
75
+ Fires the event, invoking all subscribed handlers
76
+
77
+ Args:
78
+ event (Event): The event
79
+ ttl (float, optional): The time (seconds) the event may live in the queue while awaiting subcriptions
80
+ Returns:
81
+ bool: Returns True when event was successfully sent to one or more subscribers.
82
+ """
83
+ if self.__closed:
84
+ raise ChannelClosedError
85
+
86
+ if not self.__subscribers:
87
+ if ttl:
88
+ with self.__queue_lock:
89
+ self.__queue.append(QueuedEvent(event, datetime.now() + timedelta(seconds = ttl)))
90
+
91
+ return False
92
+
93
+ for subscriber in self.__subscribers:
94
+ subscriber(self.__publisher() if self.__publisher else None, event)
95
+
96
+ return True
97
+
98
+ def close(self) -> None:
99
+ """
100
+ Closes the channel.
101
+
102
+ Raises:
103
+ ChannelClosedError: _description_
104
+ """
105
+ if self.__closed:
106
+ raise ChannelClosedError
107
+
108
+ self.__closed = True
109
+ self.__subscribers.clear()
110
+ self.__queue.clear()
111
+
112
+ def __iadd__(self, handler: Callable[[object, T], None]) -> Channel[T]:
113
+ self.subscribe(handler)
114
+ return self
115
+
116
+ def __isub__(self, handler: Callable[[object, T], None]) -> Channel[T]:
117
+ self.unsubscribe(handler)
118
+ return self
@@ -0,0 +1,5 @@
1
+ class ChannelClosedError(Exception):
2
+ """
3
+ The ChannelClosedError exception is raised when accessing functions etc. on a closed channel.
4
+ """
5
+ pass
@@ -0,0 +1,7 @@
1
+
2
+ class Event:
3
+ __slots__ = [ ]
4
+
5
+
6
+
7
+
@@ -0,0 +1,6 @@
1
+
2
+ class HandlerAlreadySubscribedError(Exception):
3
+ """
4
+ The HandlerAlreadySubscribedError exception is raised when an event handler is already subscribed to the channel.
5
+ """
6
+ pass
@@ -0,0 +1,6 @@
1
+
2
+ class HandlerNotSubscribedError(Exception):
3
+ """
4
+ The HandlerNotSubscribedError exception is raised when an event handler is not subscribed to the channel.
5
+ """
6
+ pass
@@ -0,0 +1,18 @@
1
+ from datetime import datetime
2
+
3
+ from delegate.events.core.event import Event
4
+
5
+ class QueuedEvent:
6
+ __slots__ = [ "__event", "__expires" ]
7
+
8
+ def __init__(self, event: Event, expires: datetime) -> None:
9
+ self.__event = event
10
+ self.__expires = expires
11
+
12
+ @property
13
+ def event(self) -> Event:
14
+ return self.__event
15
+
16
+ @property
17
+ def expired(self) -> bool:
18
+ return self.__expires < datetime.now()
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: delegate-events
3
+ Version: 0.0.0
4
+ Summary: Python implementation of the Event Pattern
5
+ Author-email: Anders Madsen <anders.madsen@alphavue.com>
6
+ License: MIT
7
+ Project-URL: repository, https://github.com/apmadsen/delegate-events
8
+ Keywords: windows,linux,delegates,events
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Development Status :: 6 - Mature
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Natural Language :: English
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: delegate-pattern==0.0.0
29
+ Provides-Extra: test
30
+ Requires-Dist: pytest>=8.3; extra == "test"
31
+ Requires-Dist: pytest-cov>=6.1; extra == "test"
32
+ Dynamic: license-file
33
+
34
+ [![Test](https://github.com/apmadsen/delegate-events/actions/workflows/python-test.yml/badge.svg)](https://github.com/apmadsen/delegate-events/actions/workflows/python-test.yml)
35
+ [![Coverage](https://github.com/apmadsen/delegate-events/actions/workflows/python-test-coverage.yml/badge.svg)](https://github.com/apmadsen/delegate-events/actions/workflows/python-test-coverage.yml)
36
+ [![Stable Version](https://img.shields.io/pypi/v/delegate-events?label=stable&sort=semver&color=blue)](https://github.com/apmadsen/delegate-events/releases)
37
+ ![Pre-release Version](https://img.shields.io/github/v/release/apmadsen/delegate-events?label=pre-release&include_prereleases&sort=semver&color=blue)
38
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/delegate-events)
39
+ [![PyPI Downloads](https://static.pepy.tech/badge/delegate-events/week)](https://pepy.tech/projects/delegate-events)
40
+
41
+ # delegate-events: Python implementation of the Event Pattern.
42
+
43
+ delegate-events provides a basic implementation of the well-known Event or Pub/Sub Pattern, and is built on top of the `delegate-pattern` package which handles the delegation part.
44
+
45
+ ## Example
46
+
47
+ ```python
48
+ from delegate.pattern import delegate
49
+ from delegate.events import Channel, Event
50
+
51
+ class OnSomethingEvent(Event):
52
+ def __init__(self, what: str, why: str):
53
+ self.what = what
54
+ self.why = why
55
+
56
+ def __str__(self):
57
+ return f"{self.what} happened because of {self.why}"
58
+
59
+ class Class1:
60
+ on_something = delegate(Channel[OnSomethingEvent])
61
+
62
+ inst = Class1()
63
+ msg_queue: list[Event] = []
64
+
65
+ def fn(publisher, event: OnSomethingEvent):
66
+ msg_queue.append(event)
67
+
68
+ inst.on_something.subscribe(fn)
69
+ inst.on_something.fire(OnSomethingEvent("Something", "some other thing"))
70
+
71
+ str(msg_queue[0]) # => "Something happened because of some other thing"
72
+ ```
73
+
74
+ # API
75
+
76
+ ## Event class
77
+ The `Event` class is the base class of all events. Events are the classes holding the event information, when an event is fired, thus, when subclassed, should contain properties/fields of the event information.
78
+
79
+ #### Example
80
+ ```python
81
+ from delegate.events import Event
82
+
83
+ class OnSomething(Event):
84
+ def __init__(self, what: str, why: str):
85
+ self.what = what
86
+ self.why = why
87
+
88
+ def __str__(self):
89
+ return f"{self.what} happened because of {self.why}"
90
+
91
+ ```
92
+
93
+ ## Channel class
94
+ The `Channel` class is the event channel returned by the delegate, and its jobs are handling subscriptions and firing of events.
95
+
96
+ ### Channel.subscribe(handler: _Callable[[object, T\<Event>], None]_)
97
+
98
+ The `subscribe` function subscribes a handler function to the channel. The function takes 1 argument:
99
+ - handler: `Callable[[object, Event], None]`
100
+
101
+ #### Example
102
+ ```python
103
+ from delegate.events import Channel
104
+
105
+ channel = Channel[OnSomethingEvent]()
106
+
107
+ def handler(sender: object, args: OnSomethingEvent) -> None:
108
+ pass
109
+
110
+ channel.subscribe(handler)
111
+ ```
112
+
113
+
114
+ ### Channel.unsubscribe(handler: _Callable[[object, T\<Event>], None]_)
115
+ The `unsubscribe` function unsubscribes a handler function from the channel. The function takes 1 argument:
116
+ - handler: `Callable[[object, Event], None]`
117
+
118
+ ```python
119
+ from delegate.events import Channel
120
+
121
+ channel = Channel[OnSomethingEvent]()
122
+
123
+ def handler(sender: object, args: OnSomethingEvent) -> None:
124
+ pass
125
+
126
+ channel.subscribe(handler)
127
+ channel.unsubscribe(handler)
128
+ ```
129
+
130
+ ### Channel.fire(event: _T\<Event>_, *, ttl: _float | None = 0_) -> _bool_ function
131
+ The `fire` function fires the event, invoking all subscribed handlers. The function returns True if event was successfully sent and takes 1 argument:
132
+ - event: `Event`
133
+
134
+ and 1 optional keyword argument:
135
+ - ttl: `float | None` the time-to-live (seconds) of the event.
136
+
137
+ ```python
138
+ from delegate.events import Channel
139
+
140
+ channel = Channel[OnSomethingEvent]()
141
+
142
+ msg_queue: list[Event] = []
143
+
144
+ def handler(sender: object, event: OnSomethingEvent) -> None:
145
+ msg_queue.append(event)
146
+
147
+ channel.fire(OnSomethingEvent("Something", "some other thing")) # => False
148
+ channel.fire(OnSomethingEvent("Something", "some other thing"), ttl=10) # => False - event stays in queue and will be sent if subcrribed to within 10 seconds though
149
+
150
+ channel.subscribe(handler)
151
+
152
+ channel.fire(OnSomethingEvent("Something", "some other thing")) # => True
153
+
154
+ len(msg_queue) # => 2
155
+ ```
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/delegate/events/__init__.py
5
+ src/delegate/events/core/channel.py
6
+ src/delegate/events/core/channel_closed_error.py
7
+ src/delegate/events/core/event.py
8
+ src/delegate/events/core/handler_already_subscribed_error.py
9
+ src/delegate/events/core/handler_not_subscribed_error.py
10
+ src/delegate/events/core/queued_event.py
11
+ src/delegate_events.egg-info/PKG-INFO
12
+ src/delegate_events.egg-info/SOURCES.txt
13
+ src/delegate_events.egg-info/dependency_links.txt
14
+ src/delegate_events.egg-info/requires.txt
15
+ src/delegate_events.egg-info/top_level.txt
16
+ tests/test_channel.py
@@ -0,0 +1,5 @@
1
+ delegate-pattern==0.0.0
2
+
3
+ [test]
4
+ pytest>=8.3
5
+ pytest-cov>=6.1
@@ -0,0 +1,304 @@
1
+ # pyright: basic
2
+ from typing import Any
3
+ from time import sleep
4
+ import gc
5
+ from weakref import ref
6
+ from pytest import raises as assert_raises, fixture
7
+
8
+ from delegate.pattern import delegate
9
+ from delegate.events import Event, Channel, HandlerNotSubscribedError, HandlerAlreadySubscribedError, ChannelClosedError
10
+
11
+ class Event1(Event):
12
+ def __init__(self, arg1: str, arg2: int):
13
+ self.arg1 = arg1
14
+ self.arg2 = arg2
15
+
16
+ class Event2(Event):
17
+ def __init__(self, arg1: str, arg2: int):
18
+ self.arg1 = arg1
19
+ self.arg2 = arg2
20
+
21
+ class OnSomethingEvent(Event):
22
+ def __init__(self, what: str, why: str):
23
+ self.what = what
24
+ self.why = why
25
+
26
+ def __str__(self):
27
+ return f"{self.what} happened because of {self.why}"
28
+
29
+
30
+ def test_channel_basics():
31
+ class Class1:
32
+ event1 = delegate(Channel[Event1])
33
+
34
+ inst = Class1()
35
+ msg_queue: list[tuple[object, Event]] = []
36
+
37
+ def fn(publisher, event: Event1):
38
+ msg_queue.append((publisher, event))
39
+
40
+ inst.event1.subscribe(fn)
41
+ inst.event1.fire(Event1("msg",1))
42
+
43
+ assert len(msg_queue) == 1
44
+ assert msg_queue[0][0] is inst
45
+
46
+
47
+ def test_channel_closing():
48
+ class Class1:
49
+ event1 = delegate(Channel[Event1])
50
+
51
+ inst = Class1()
52
+
53
+ def fn(publisher, event: Event1):
54
+ pass
55
+
56
+ inst.event1.close()
57
+
58
+ with assert_raises(ChannelClosedError):
59
+ inst.event1.close()
60
+
61
+ with assert_raises(ChannelClosedError):
62
+ inst.event1.subscribe(fn)
63
+
64
+ with assert_raises(ChannelClosedError):
65
+ inst.event1.unsubscribe(fn)
66
+
67
+ with assert_raises(ChannelClosedError):
68
+ inst.event1.fire(Event1("msg",2))
69
+
70
+
71
+ def test_garbage_collection():
72
+ class Event3(Event):
73
+ def __init__(self, arg1: str, arg2: int):
74
+ self.arg1 = arg1
75
+ self.arg2 = arg2
76
+
77
+ class Class1:
78
+ event3 = delegate(Channel[Event3])
79
+
80
+
81
+
82
+ inst = Class1()
83
+ channel = ref(inst.event3)
84
+ refs_before_gc = len(gc.get_referrers(channel()))
85
+
86
+ inst = None
87
+ del Class1
88
+ del Event3
89
+
90
+ gc.collect()
91
+ gc.collect()
92
+ gc.collect()
93
+
94
+ assert refs_before_gc > 0
95
+ assert channel() is None
96
+
97
+
98
+ def test_multiple_channels():
99
+
100
+ class Class1:
101
+ event1 = delegate(Channel[Event1])
102
+ event2 = delegate(Channel[Event2])
103
+
104
+ inst = Class1()
105
+ msg_queue: list[tuple[object, Event]] = []
106
+
107
+ def fn(publisher, event: Event):
108
+ msg_queue.append((publisher, event))
109
+
110
+ inst.event1.subscribe(fn)
111
+ inst.event2.subscribe(fn)
112
+
113
+ inst.event1.fire(Event1("msg",1))
114
+ inst.event2.fire(Event2("msg",2))
115
+
116
+ assert len(msg_queue) == 2
117
+ assert msg_queue[0][0] is inst
118
+ assert isinstance(msg_queue[0][1], Event1)
119
+ assert msg_queue[1][0] is inst
120
+ assert isinstance(msg_queue[1][1], Event2)
121
+ x=0
122
+
123
+
124
+
125
+ def test_subscriptions():
126
+
127
+ class Class1:
128
+ event1 = delegate(Channel[Event1])
129
+
130
+ inst = Class1()
131
+ msg_queue: list[tuple[object, Event1]] = []
132
+
133
+ def fn1(publisher, event: Event1):
134
+ msg_queue.append((publisher, event))
135
+
136
+ def fn2(publisher, event: Event1):
137
+ msg_queue.append((publisher, event))
138
+
139
+ inst.event1.subscribe(fn1)
140
+
141
+ with assert_raises(HandlerAlreadySubscribedError):
142
+ inst.event1.subscribe(fn1)
143
+
144
+ inst.event1.unsubscribe(fn1)
145
+
146
+ with assert_raises(HandlerNotSubscribedError):
147
+ inst.event1.unsubscribe(fn1)
148
+
149
+ inst.event1 += fn1
150
+ inst.event1 += fn2
151
+
152
+ inst.event1.fire(Event1("msg",1))
153
+
154
+ assert len(msg_queue) == 2
155
+ assert msg_queue[0][0] is inst
156
+ assert msg_queue[1][0] is inst
157
+ assert msg_queue[0][1] is msg_queue[1][1] # events should be same instance
158
+
159
+ inst.event1 -= fn2
160
+ inst.event1.fire(Event1("msg",2))
161
+
162
+ assert len(msg_queue) == 3
163
+ assert msg_queue[2][1].arg2 ==2
164
+
165
+
166
+ def test_event_queue():
167
+
168
+ class Class1:
169
+ event1 = delegate(Channel[Event1])
170
+
171
+ inst = Class1()
172
+ msg_queue: list[tuple[object, Event1]] = []
173
+
174
+ def fn(publisher, event: Event1):
175
+ msg_queue.append((publisher, event))
176
+
177
+ inst.event1.fire(Event1("msg",1)) # this msg will not be received because it has no ttl
178
+ inst.event1.fire(Event1("msg",2), ttl=60)
179
+ inst.event1.subscribe(fn)
180
+
181
+ assert len(msg_queue) == 1
182
+ assert msg_queue[0][0] is inst
183
+
184
+ inst = Class1()
185
+ msg_queue.clear()
186
+
187
+ inst.event1.fire(Event1("msg",1), ttl=0.001) # this msg will not be received because it has no ttl
188
+ sleep(0.01)
189
+ inst.event1.subscribe(fn)
190
+
191
+ assert len(msg_queue) == 0
192
+
193
+ def test_multiple_instances_of_same_class():
194
+
195
+ class Class1:
196
+ event1 = delegate(Channel[Event1])
197
+
198
+ inst1 = Class1()
199
+ inst2 = Class1()
200
+
201
+ msg_queue: list[tuple[object, Event1]] = []
202
+
203
+ def fn(publisher, event: Event1):
204
+ msg_queue.append((publisher, event))
205
+
206
+ inst1.event1.subscribe(fn)
207
+ inst2.event1.subscribe(fn)
208
+
209
+ inst1.event1.fire(Event1("msg",1))
210
+ inst2.event1.fire(Event1("msg",2))
211
+
212
+ assert len(msg_queue) == 2
213
+ assert msg_queue[0][0] is inst1
214
+ assert msg_queue[0][1].arg2 == 1
215
+ assert msg_queue[1][0] is inst2
216
+ assert msg_queue[1][1].arg2 == 2
217
+
218
+
219
+
220
+ def test_instances_of_different_classes():
221
+
222
+ class Class1:
223
+ event1 = delegate(Channel[Event1])
224
+
225
+ class Class2:
226
+ event1 = delegate(Channel[Event1])
227
+
228
+ inst1 = Class1()
229
+ inst2 = Class2()
230
+
231
+ msg_queue: list[tuple[object, Event1]] = []
232
+
233
+ def fn(publisher, event: Event1):
234
+ msg_queue.append((publisher, event))
235
+
236
+ inst1.event1.subscribe(fn)
237
+ inst2.event1.subscribe(fn)
238
+
239
+ inst1.event1.fire(Event1("msg",1))
240
+ inst2.event1.fire(Event1("msg",2))
241
+
242
+ assert len(msg_queue) == 2
243
+ assert msg_queue[0][0] is inst1
244
+ assert msg_queue[0][1].arg2 == 1
245
+ assert msg_queue[1][0] is inst2
246
+ assert msg_queue[1][1].arg2 == 2
247
+
248
+ def test_readme_example1():
249
+ from delegate.pattern import delegate
250
+ from delegate.events import Channel, Event
251
+
252
+ class OnSomethingEvent(Event):
253
+ def __init__(self, what: str, why: str):
254
+ self.what = what
255
+ self.why = why
256
+
257
+ def __str__(self):
258
+ return f"{self.what} happened because of {self.why}"
259
+
260
+ class Class1:
261
+ on_something = delegate(Channel[OnSomethingEvent])
262
+
263
+ inst = Class1()
264
+ msg_queue: list[Event] = []
265
+
266
+ def fn(publisher, event: OnSomethingEvent):
267
+ msg_queue.append(event)
268
+
269
+ inst.on_something.subscribe(fn)
270
+ inst.on_something.fire(OnSomethingEvent("Something", "some other thing"))
271
+
272
+ event_msg = str(msg_queue[0]) # => "Something happened because of some other thing"
273
+ assert str(event_msg) == "Something happened because of some other thing"
274
+
275
+
276
+ def test_readme_example2():
277
+ from delegate.events import Channel
278
+
279
+ channel = Channel[OnSomethingEvent]()
280
+
281
+ def handler(sender: object, event: OnSomethingEvent) -> None:
282
+ pass
283
+
284
+ channel.subscribe(handler)
285
+ assert True == channel.fire(OnSomethingEvent("Something", "some other thing"))
286
+
287
+
288
+ def test_readme_example3():
289
+ from delegate.events import Channel
290
+
291
+ channel = Channel[OnSomethingEvent]()
292
+
293
+ msg_queue: list[Event] = []
294
+
295
+ def handler(sender: object, event: OnSomethingEvent) -> None:
296
+ msg_queue.append(event)
297
+
298
+ assert False == channel.fire(OnSomethingEvent("Something", "some other thing")) # -> False
299
+ assert False == channel.fire(OnSomethingEvent("Something", "some other thing"), ttl=10) # -> False - event stays in queue for 10 seconds though
300
+
301
+ channel.subscribe(handler)
302
+
303
+ assert True == channel.fire(OnSomethingEvent("Something", "some other thing")) # -> True
304
+ assert len(msg_queue) == 2