strongbus 0.1.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,14 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ .tox
12
+
13
+ # IDE
14
+ .vscode
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,54 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ### Testing
8
+ ```bash
9
+ python -m unittest src/strongbus/tests.py
10
+ ```
11
+
12
+ ### Build and Package
13
+ ```bash
14
+ python -m build
15
+ ```
16
+
17
+ ### Install in Development Mode
18
+ ```bash
19
+ pip install -e .
20
+ ```
21
+
22
+ ## Architecture
23
+
24
+ StrongBus is a type-safe event bus library for Python with these core components:
25
+
26
+ ### Core Classes (`src/strongbus/core.py`)
27
+ - **Event**: Base class for all events. All event types must inherit from this class.
28
+ - **EventBus**: Central event dispatcher that manages subscriptions and publishing
29
+ - Uses weak references for method callbacks to prevent memory leaks
30
+ - Strong references for function callbacks
31
+ - Type-safe subscription with generic type constraints
32
+ - **Enrollment**: Abstract base class that provides subscription management with automatic cleanup
33
+ - Tracks all subscriptions for easy bulk unsubscription via `clear()`
34
+ - Delegates to EventBus for actual event handling
35
+
36
+ ### Key Design Patterns
37
+ - **Type Safety**: Uses generics (`SpecificEvent = TypeVar("SpecificEvent", bound=Event)`) to ensure callbacks receive the correct event type
38
+ - **Memory Management**: Automatic cleanup of dead method references using `weakref.WeakMethod`
39
+ - **Inheritance Isolation**: Events don't propagate to parent/child types - each event type is handled independently
40
+ - **Subscription Tracking**: Enrollment pattern allows components to manage their own subscriptions lifecycle
41
+
42
+ ### Event Flow
43
+ 1. Create event classes by inheriting from `Event` (typically as frozen dataclasses)
44
+ 2. Components inherit from `Enrollment` and subscribe to specific event types
45
+ 3. Events are published through EventBus and delivered only to exact type matches
46
+ 4. Cleanup handled automatically via weak references or explicit `clear()` calls
47
+
48
+ ### Testing
49
+ Comprehensive test suite in `src/strongbus/tests.py` covers:
50
+ - Basic subscription/publishing
51
+ - Multiple subscribers
52
+ - Type isolation
53
+ - Memory management (weak references)
54
+ - Subscription cleanup
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.4
2
+ Name: strongbus
3
+ Version: 0.1.0
4
+ Summary: A type-safe event bus library for Python that provides reliable publish-subscribe messaging with automatic memory management and full type safety.
5
+ Author-email: Marius Räsener <marius@raesener.de>
6
+ Keywords: event-bus,events,pubsub,type-safe,typed
7
+ Requires-Python: >=3.10
8
+ Provides-Extra: dev
9
+ Requires-Dist: build; extra == 'dev'
10
+ Requires-Dist: tox; extra == 'dev'
11
+ Description-Content-Type: text/markdown
12
+
13
+
14
+ # StrongBus
15
+
16
+ A type-safe event bus library for Python that provides reliable publish-subscribe messaging with automatic memory management and full type safety.
17
+
18
+ ## Features
19
+
20
+ - **Type Safety**: Full type checking with generics ensures callbacks receive the correct event types
21
+ - **Memory Management**: Automatic cleanup of dead references using weak references for methods
22
+ - **Subscription Management**: Easy subscription tracking and bulk cleanup via the Enrollment pattern
23
+ - **Event Isolation**: Events don't propagate to parent/child types - each event type is handled independently
24
+ - **Zero Dependencies**: Pure Python implementation with no external dependencies
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install strongbus
30
+ ```
31
+
32
+ For development:
33
+ ```bash
34
+ pip install -e .
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from dataclasses import dataclass
41
+ from strongbus import Event, EventBus, Enrollment
42
+
43
+ # Define your events
44
+ @dataclass(frozen=True)
45
+ class UserLoginEvent(Event):
46
+ username: str
47
+
48
+ # Create subscribers using Enrollment
49
+ class NotificationService(Enrollment):
50
+ def __init__(self, event_bus: EventBus):
51
+ super().__init__(event_bus)
52
+ self.subscribe(UserLoginEvent, self.on_user_login)
53
+
54
+ def on_user_login(self, event: UserLoginEvent) -> None:
55
+ print(f"Welcome {event.username}!")
56
+
57
+ # Usage
58
+ event_bus = EventBus()
59
+ service = NotificationService(event_bus)
60
+ event_bus.publish(UserLoginEvent(username="Alice"))
61
+ # Output: Welcome Alice!
62
+
63
+ # Cleanup
64
+ service.clear() # Automatically unsubscribes from all events
65
+ ```
66
+
67
+ ## Core Concepts
68
+
69
+ ### Events
70
+
71
+ Events are simple data classes that inherit from the `Event` base class:
72
+
73
+ ```python
74
+ @dataclass(frozen=True)
75
+ class OrderCreatedEvent(Event):
76
+ order_id: str
77
+ customer_id: str
78
+ total: float
79
+ ```
80
+
81
+ ### EventBus
82
+
83
+ The central hub for publishing and subscribing to events:
84
+
85
+ ```python
86
+ event_bus = EventBus()
87
+
88
+ # Subscribe to events
89
+ event_bus.subscribe(OrderCreatedEvent, handle_order)
90
+
91
+ # Publish events
92
+ event_bus.publish(OrderCreatedEvent(
93
+ order_id="12345",
94
+ customer_id="user123",
95
+ total=99.99
96
+ ))
97
+ ```
98
+
99
+ ### Enrollment
100
+
101
+ A base class that simplifies subscription management:
102
+
103
+ ```python
104
+ class OrderProcessor(Enrollment):
105
+ def __init__(self, event_bus: EventBus):
106
+ super().__init__(event_bus)
107
+ self.subscribe(OrderCreatedEvent, self.process_order)
108
+ self.subscribe(PaymentReceivedEvent, self.confirm_payment)
109
+
110
+ def process_order(self, event: OrderCreatedEvent) -> None:
111
+ # Handle order processing
112
+ pass
113
+
114
+ def confirm_payment(self, event: PaymentReceivedEvent) -> None:
115
+ # Handle payment confirmation
116
+ pass
117
+ ```
118
+
119
+ ## Memory Management
120
+
121
+ StrongBus automatically manages memory to prevent leaks:
122
+
123
+ - **Method callbacks** use weak references and are automatically cleaned up when the object is garbage collected
124
+ - **Function callbacks** use strong references and persist until explicitly unsubscribed
125
+ - **Enrollment pattern** provides easy bulk cleanup with `clear()`
126
+
127
+ ## Testing
128
+
129
+ ### Using tox (recommended)
130
+
131
+ Install tox with uv support:
132
+ ```bash
133
+ uv tool install tox --with tox-uv
134
+ ```
135
+
136
+ Run all tests across multiple Python versions:
137
+ ```bash
138
+ tox
139
+ ```
140
+
141
+ ### Manual testing
142
+
143
+ Run the test suite directly:
144
+ ```bash
145
+ python -m unittest src/strongbus/tests.py
146
+ ```
147
+
148
+ ## Slightly larger example
149
+ ```python
150
+ from dataclasses import dataclass
151
+
152
+ from strongbus import Event, EventBus, Enrollment
153
+
154
+
155
+ @dataclass(frozen=True)
156
+ class UserLoginEvent(Event):
157
+ username: str
158
+
159
+
160
+ @dataclass(frozen=True)
161
+ class UserLogoutEvent(Event):
162
+ username: str
163
+
164
+
165
+ @dataclass(frozen=True)
166
+ class DataUpdatedEvent(Event):
167
+ data_id: str
168
+ new_value: str
169
+
170
+
171
+ @dataclass(frozen=True)
172
+ class TestEvent(Event):
173
+ message: str
174
+
175
+
176
+ class PackageManager(Enrollment):
177
+ def __init__(self, event_bus: EventBus):
178
+ super().__init__(event_bus)
179
+ # Type-safe subscription - callback must accept UserLoginEvent
180
+ self.subscribe(UserLoginEvent, self.on_user_login)
181
+ self.subscribe(DataUpdatedEvent, self.on_data_updated)
182
+
183
+ def on_user_login(self, event: UserLoginEvent) -> None:
184
+ # Can access event.username with full type safety
185
+ print(f"PackageManager: User {event.username} logged in")
186
+
187
+ def on_data_updated(self, event: DataUpdatedEvent) -> None:
188
+ print(f"PackageManager: Data {event.data_id} updated to {event.new_value}")
189
+
190
+
191
+ class ContainerManager(Enrollment):
192
+ def __init__(self, event_bus: EventBus):
193
+ super().__init__(event_bus)
194
+ self.subscribe(UserLoginEvent, self.on_user_login)
195
+ self.subscribe(UserLogoutEvent, self.on_user_logout)
196
+
197
+ def on_user_login(self, event: UserLoginEvent) -> None:
198
+ print(f"ContainerManager: User {event.username} logged in")
199
+
200
+ def on_user_logout(self, event: UserLogoutEvent) -> None:
201
+ print(f"ContainerManager: User {event.username} logged out")
202
+
203
+
204
+ if __name__ == "__main__":
205
+ # Usage
206
+ event_bus = EventBus()
207
+ manager0 = PackageManager(event_bus)
208
+ manager1 = ContainerManager(event_bus)
209
+
210
+ # Publish events - type-safe with proper event objects
211
+ event_bus.publish(UserLoginEvent(username="Alice"))
212
+ # Output:
213
+ # PackageManager: User Alice logged in
214
+ # ContainerManager: User Alice logged in
215
+
216
+ event_bus.publish(UserLogoutEvent(username="Alice"))
217
+ # Output:
218
+ # ContainerManager: User Alice logged out
219
+
220
+ event_bus.publish(DataUpdatedEvent(data_id="123", new_value="new data"))
221
+ # Output:
222
+ # PackageManager: Data 123 updated to new data
223
+
224
+ # List all available event types
225
+ print("\nAvailable event types:")
226
+ for event_class in Event.__subclasses__():
227
+ print(f" - {event_class.__name__}")
228
+
229
+ # Cleanup
230
+ manager0.clear()
231
+ manager1.clear()
232
+
233
+ ```
@@ -0,0 +1,221 @@
1
+
2
+ # StrongBus
3
+
4
+ A type-safe event bus library for Python that provides reliable publish-subscribe messaging with automatic memory management and full type safety.
5
+
6
+ ## Features
7
+
8
+ - **Type Safety**: Full type checking with generics ensures callbacks receive the correct event types
9
+ - **Memory Management**: Automatic cleanup of dead references using weak references for methods
10
+ - **Subscription Management**: Easy subscription tracking and bulk cleanup via the Enrollment pattern
11
+ - **Event Isolation**: Events don't propagate to parent/child types - each event type is handled independently
12
+ - **Zero Dependencies**: Pure Python implementation with no external dependencies
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ pip install strongbus
18
+ ```
19
+
20
+ For development:
21
+ ```bash
22
+ pip install -e .
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```python
28
+ from dataclasses import dataclass
29
+ from strongbus import Event, EventBus, Enrollment
30
+
31
+ # Define your events
32
+ @dataclass(frozen=True)
33
+ class UserLoginEvent(Event):
34
+ username: str
35
+
36
+ # Create subscribers using Enrollment
37
+ class NotificationService(Enrollment):
38
+ def __init__(self, event_bus: EventBus):
39
+ super().__init__(event_bus)
40
+ self.subscribe(UserLoginEvent, self.on_user_login)
41
+
42
+ def on_user_login(self, event: UserLoginEvent) -> None:
43
+ print(f"Welcome {event.username}!")
44
+
45
+ # Usage
46
+ event_bus = EventBus()
47
+ service = NotificationService(event_bus)
48
+ event_bus.publish(UserLoginEvent(username="Alice"))
49
+ # Output: Welcome Alice!
50
+
51
+ # Cleanup
52
+ service.clear() # Automatically unsubscribes from all events
53
+ ```
54
+
55
+ ## Core Concepts
56
+
57
+ ### Events
58
+
59
+ Events are simple data classes that inherit from the `Event` base class:
60
+
61
+ ```python
62
+ @dataclass(frozen=True)
63
+ class OrderCreatedEvent(Event):
64
+ order_id: str
65
+ customer_id: str
66
+ total: float
67
+ ```
68
+
69
+ ### EventBus
70
+
71
+ The central hub for publishing and subscribing to events:
72
+
73
+ ```python
74
+ event_bus = EventBus()
75
+
76
+ # Subscribe to events
77
+ event_bus.subscribe(OrderCreatedEvent, handle_order)
78
+
79
+ # Publish events
80
+ event_bus.publish(OrderCreatedEvent(
81
+ order_id="12345",
82
+ customer_id="user123",
83
+ total=99.99
84
+ ))
85
+ ```
86
+
87
+ ### Enrollment
88
+
89
+ A base class that simplifies subscription management:
90
+
91
+ ```python
92
+ class OrderProcessor(Enrollment):
93
+ def __init__(self, event_bus: EventBus):
94
+ super().__init__(event_bus)
95
+ self.subscribe(OrderCreatedEvent, self.process_order)
96
+ self.subscribe(PaymentReceivedEvent, self.confirm_payment)
97
+
98
+ def process_order(self, event: OrderCreatedEvent) -> None:
99
+ # Handle order processing
100
+ pass
101
+
102
+ def confirm_payment(self, event: PaymentReceivedEvent) -> None:
103
+ # Handle payment confirmation
104
+ pass
105
+ ```
106
+
107
+ ## Memory Management
108
+
109
+ StrongBus automatically manages memory to prevent leaks:
110
+
111
+ - **Method callbacks** use weak references and are automatically cleaned up when the object is garbage collected
112
+ - **Function callbacks** use strong references and persist until explicitly unsubscribed
113
+ - **Enrollment pattern** provides easy bulk cleanup with `clear()`
114
+
115
+ ## Testing
116
+
117
+ ### Using tox (recommended)
118
+
119
+ Install tox with uv support:
120
+ ```bash
121
+ uv tool install tox --with tox-uv
122
+ ```
123
+
124
+ Run all tests across multiple Python versions:
125
+ ```bash
126
+ tox
127
+ ```
128
+
129
+ ### Manual testing
130
+
131
+ Run the test suite directly:
132
+ ```bash
133
+ python -m unittest src/strongbus/tests.py
134
+ ```
135
+
136
+ ## Slightly larger example
137
+ ```python
138
+ from dataclasses import dataclass
139
+
140
+ from strongbus import Event, EventBus, Enrollment
141
+
142
+
143
+ @dataclass(frozen=True)
144
+ class UserLoginEvent(Event):
145
+ username: str
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class UserLogoutEvent(Event):
150
+ username: str
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class DataUpdatedEvent(Event):
155
+ data_id: str
156
+ new_value: str
157
+
158
+
159
+ @dataclass(frozen=True)
160
+ class TestEvent(Event):
161
+ message: str
162
+
163
+
164
+ class PackageManager(Enrollment):
165
+ def __init__(self, event_bus: EventBus):
166
+ super().__init__(event_bus)
167
+ # Type-safe subscription - callback must accept UserLoginEvent
168
+ self.subscribe(UserLoginEvent, self.on_user_login)
169
+ self.subscribe(DataUpdatedEvent, self.on_data_updated)
170
+
171
+ def on_user_login(self, event: UserLoginEvent) -> None:
172
+ # Can access event.username with full type safety
173
+ print(f"PackageManager: User {event.username} logged in")
174
+
175
+ def on_data_updated(self, event: DataUpdatedEvent) -> None:
176
+ print(f"PackageManager: Data {event.data_id} updated to {event.new_value}")
177
+
178
+
179
+ class ContainerManager(Enrollment):
180
+ def __init__(self, event_bus: EventBus):
181
+ super().__init__(event_bus)
182
+ self.subscribe(UserLoginEvent, self.on_user_login)
183
+ self.subscribe(UserLogoutEvent, self.on_user_logout)
184
+
185
+ def on_user_login(self, event: UserLoginEvent) -> None:
186
+ print(f"ContainerManager: User {event.username} logged in")
187
+
188
+ def on_user_logout(self, event: UserLogoutEvent) -> None:
189
+ print(f"ContainerManager: User {event.username} logged out")
190
+
191
+
192
+ if __name__ == "__main__":
193
+ # Usage
194
+ event_bus = EventBus()
195
+ manager0 = PackageManager(event_bus)
196
+ manager1 = ContainerManager(event_bus)
197
+
198
+ # Publish events - type-safe with proper event objects
199
+ event_bus.publish(UserLoginEvent(username="Alice"))
200
+ # Output:
201
+ # PackageManager: User Alice logged in
202
+ # ContainerManager: User Alice logged in
203
+
204
+ event_bus.publish(UserLogoutEvent(username="Alice"))
205
+ # Output:
206
+ # ContainerManager: User Alice logged out
207
+
208
+ event_bus.publish(DataUpdatedEvent(data_id="123", new_value="new data"))
209
+ # Output:
210
+ # PackageManager: Data 123 updated to new data
211
+
212
+ # List all available event types
213
+ print("\nAvailable event types:")
214
+ for event_class in Event.__subclasses__():
215
+ print(f" - {event_class.__name__}")
216
+
217
+ # Cleanup
218
+ manager0.clear()
219
+ manager1.clear()
220
+
221
+ ```
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "strongbus"
3
+ version = "0.1.0"
4
+ description = "A type-safe event bus library for Python that provides reliable publish-subscribe messaging with automatic memory management and full type safety."
5
+ readme = "README.md"
6
+ authors = [{ name = "Marius Räsener", email = "marius@raesener.de" }]
7
+ requires-python = ">=3.10"
8
+ dependencies = []
9
+ keywords = ["events", "event-bus", "typed", "type-safe", "pubsub"]
10
+
11
+ [project.optional-dependencies]
12
+ dev = ["tox", "build"]
13
+
14
+ [project.scripts]
15
+ strongbus = "strongbus:main"
16
+
17
+ [build-system]
18
+ requires = ["hatchling"]
19
+ build-backend = "hatchling.build"
@@ -0,0 +1,3 @@
1
+ from .core import Enrollment, Event, EventBus
2
+
3
+ __all__ = ["EventBus", "Event", "Enrollment"]
@@ -0,0 +1,107 @@
1
+ import inspect
2
+ import weakref
3
+ from abc import ABC
4
+ from typing import Callable, Dict, List, Type, TypeVar, Union
5
+
6
+
7
+ class Event:
8
+ """Base class for all events. Subclass for specific types."""
9
+
10
+ pass
11
+
12
+
13
+ SpecificEvent = TypeVar("SpecificEvent", bound=Event)
14
+
15
+ EventHandler = Callable[[Event], None]
16
+ SubscriberType = Union[EventHandler, weakref.WeakMethod[EventHandler]]
17
+
18
+
19
+ class EventBus:
20
+ def __init__(self):
21
+ self._subscribers: Dict[Type[Event], List[SubscriberType]] = {}
22
+
23
+ def subscribe(
24
+ self, event_type: Type[SpecificEvent], callback: Callable[[SpecificEvent], None]
25
+ ) -> None:
26
+ """Subscribe to a specific event type with a type-safe callback."""
27
+ if event_type not in self._subscribers:
28
+ self._subscribers[event_type] = []
29
+
30
+ if inspect.ismethod(callback):
31
+ weak_callback: SubscriberType = weakref.WeakMethod(callback) # type: ignore[arg-type]
32
+ else:
33
+ weak_callback: SubscriberType = callback # type: ignore[assignment]
34
+
35
+ self._subscribers[event_type].append(weak_callback)
36
+
37
+ def unsubscribe(
38
+ self, event_type: Type[SpecificEvent], callback: Callable[[SpecificEvent], None]
39
+ ) -> None:
40
+ """Unsubscribe a callback from a specific event type."""
41
+ if event_type in self._subscribers:
42
+ to_remove: List[SubscriberType] = []
43
+ for weak_cb in self._subscribers[event_type]:
44
+ if isinstance(weak_cb, weakref.WeakMethod):
45
+ cb: Callable[[Event], None] | None = weak_cb()
46
+ if cb is not None and cb == callback:
47
+ to_remove.append(weak_cb)
48
+ else:
49
+ if weak_cb == callback:
50
+ to_remove.append(weak_cb)
51
+ for r in to_remove:
52
+ self._subscribers[event_type].remove(r)
53
+
54
+ def publish(self, event: Event) -> None:
55
+ """Publish an event to all subscribers of its type."""
56
+ event_type = type(event)
57
+ if event_type in self._subscribers:
58
+ subscribers = self._subscribers[event_type][
59
+ :
60
+ ] # Copy to avoid modification during iteration
61
+ for weak_cb in subscribers:
62
+ if isinstance(weak_cb, weakref.WeakMethod):
63
+ cb: Callable[[Event], None] | None = weak_cb()
64
+ if cb is not None:
65
+ cb(event)
66
+ else:
67
+ self._subscribers[event_type].remove(weak_cb)
68
+ else:
69
+ weak_cb(event)
70
+
71
+
72
+ class Enrollment(ABC):
73
+ def __init__(self, event_bus: EventBus):
74
+ self._event_bus = event_bus
75
+ self._subscriptions: Dict[Type[Event], List[Callable[[Event], None]]] = {}
76
+
77
+ def subscribe(
78
+ self, event_type: Type[SpecificEvent], callback: Callable[[SpecificEvent], None]
79
+ ) -> None:
80
+ """Subscribe to an event type with automatic tracking."""
81
+ if event_type not in self._subscriptions:
82
+ self._subscriptions[event_type] = []
83
+ self._subscriptions[event_type].append(callback) # type: ignore
84
+ self._event_bus.subscribe(event_type, callback)
85
+
86
+ def unsubscribe(
87
+ self, event_type: Type[SpecificEvent], callback: Callable[[SpecificEvent], None]
88
+ ) -> None:
89
+ """Unsubscribe from an event type."""
90
+ if event_type in self._subscriptions:
91
+ self._subscriptions[event_type] = [
92
+ cb for cb in self._subscriptions[event_type] if cb != callback
93
+ ]
94
+ self._event_bus.unsubscribe(event_type, callback)
95
+ if not self._subscriptions[event_type]:
96
+ del self._subscriptions[event_type]
97
+
98
+ def publish(self, event: Event) -> None:
99
+ """Publish an event through the event bus."""
100
+ self._event_bus.publish(event)
101
+
102
+ def clear(self) -> None:
103
+ """Unsubscribe from all events."""
104
+ for event_type, callbacks in list(self._subscriptions.items()):
105
+ for callback in callbacks:
106
+ self._event_bus.unsubscribe(event_type, callback)
107
+ self._subscriptions.clear()
File without changes
@@ -0,0 +1,143 @@
1
+ import gc
2
+ import unittest
3
+ from dataclasses import dataclass
4
+ from unittest.mock import Mock
5
+
6
+ from strongbus import Event, EventBus
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class MyEvent(Event):
11
+ value: str
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class AnotherEvent(Event):
16
+ number: int
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class SubEvent(MyEvent):
21
+ pass
22
+
23
+
24
+ class TestEventBus(unittest.TestCase):
25
+ def setUp(self):
26
+ self.bus = EventBus()
27
+
28
+ def test_subscribe_and_publish(self):
29
+ callback = Mock()
30
+ self.bus.subscribe(MyEvent, callback)
31
+ event = MyEvent("test")
32
+ self.bus.publish(event)
33
+ callback.assert_called_once_with(event)
34
+
35
+ def test_no_subscribers(self):
36
+ event = MyEvent("test")
37
+ # Should not raise any error
38
+ self.bus.publish(event)
39
+
40
+ def test_multiple_subscribers(self):
41
+ callback1 = Mock()
42
+ callback2 = Mock()
43
+ self.bus.subscribe(MyEvent, callback1)
44
+ self.bus.subscribe(MyEvent, callback2)
45
+ event = MyEvent("test")
46
+ self.bus.publish(event)
47
+ callback1.assert_called_once_with(event)
48
+ callback2.assert_called_once_with(event)
49
+
50
+ def test_different_event_types(self):
51
+ callback_my = Mock()
52
+ callback_another = Mock()
53
+ self.bus.subscribe(MyEvent, callback_my)
54
+ self.bus.subscribe(AnotherEvent, callback_another)
55
+ event_my = MyEvent("test")
56
+ event_another = AnotherEvent(42)
57
+ self.bus.publish(event_my)
58
+ self.bus.publish(event_another)
59
+ callback_my.assert_called_once_with(event_my)
60
+ callback_another.assert_called_once_with(event_another)
61
+
62
+ def test_unsubscribe(self):
63
+ callback = Mock()
64
+ self.bus.subscribe(MyEvent, callback)
65
+ event = MyEvent("test")
66
+ self.bus.publish(event)
67
+ callback.assert_called_once_with(event)
68
+ self.bus.unsubscribe(MyEvent, callback)
69
+ callback.reset_mock()
70
+ self.bus.publish(event)
71
+ callback.assert_not_called()
72
+
73
+ def test_unsubscribe_non_existent(self):
74
+ callback = Mock()
75
+ # Should not raise error
76
+ self.bus.unsubscribe(MyEvent, callback)
77
+ event = MyEvent("test")
78
+ self.bus.publish(event)
79
+ callback.assert_not_called()
80
+
81
+ def test_inheritance_not_propagated(self):
82
+ callback_base = Mock()
83
+ callback_sub = Mock()
84
+ self.bus.subscribe(MyEvent, callback_base)
85
+ self.bus.subscribe(SubEvent, callback_sub)
86
+ event_base = MyEvent("base")
87
+ event_sub = SubEvent("sub")
88
+ self.bus.publish(event_base)
89
+ self.bus.publish(event_sub)
90
+ callback_base.assert_called_once_with(event_base)
91
+ callback_sub.assert_called_once_with(event_sub)
92
+ # Ensure base callback not called for sub event
93
+ self.assertEqual(callback_base.call_count, 1)
94
+
95
+ def test_weak_reference_for_methods(self):
96
+ class Subscriber:
97
+ def __init__(self):
98
+ self.called = 0
99
+
100
+ def on_event(self, event: MyEvent):
101
+ self.called += 1
102
+
103
+ sub = Subscriber()
104
+ self.bus.subscribe(MyEvent, sub.on_event)
105
+ self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1) # pyright: ignore[reportPrivateUsage]
106
+
107
+ event = MyEvent("test")
108
+ self.bus.publish(event)
109
+ self.assertEqual(sub.called, 1)
110
+
111
+ del sub
112
+ gc.collect()
113
+
114
+ self.bus.publish(event) # Should clean up dead weak ref
115
+ self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 0) # pyright: ignore[reportPrivateUsage]
116
+
117
+ def test_strong_reference_for_functions(self):
118
+ def on_event(event: MyEvent):
119
+ pass
120
+
121
+ self.bus.subscribe(MyEvent, on_event)
122
+ self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1) # pyright: ignore[reportPrivateUsage]
123
+
124
+ # Simulate GC, but since strong ref, should remain
125
+ gc.collect()
126
+ self.bus.publish(MyEvent("test"))
127
+ self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1) # pyright: ignore[reportPrivateUsage]
128
+
129
+ def test_unsubscribe_weak_method(self):
130
+ class Subscriber:
131
+ def on_event(self, event: MyEvent):
132
+ pass
133
+
134
+ sub = Subscriber()
135
+ self.bus.subscribe(MyEvent, sub.on_event)
136
+ self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 1) # pyright: ignore[reportPrivateUsage]
137
+
138
+ self.bus.unsubscribe(MyEvent, sub.on_event)
139
+ self.assertEqual(len(self.bus._subscribers.get(MyEvent, [])), 0) # pyright: ignore[reportPrivateUsage]
140
+
141
+
142
+ if __name__ == "__main__":
143
+ unittest.main()
@@ -0,0 +1,21 @@
1
+ [tox]
2
+ envlist = py310, py311, py312, py313
3
+ isolated_build = true
4
+
5
+ [testenv]
6
+ deps =
7
+ build
8
+ commands =
9
+ python -m unittest src/strongbus/tests.py
10
+
11
+ [testenv:build]
12
+ deps =
13
+ build
14
+ commands =
15
+ python -m build
16
+
17
+ [testenv:dev]
18
+ deps =
19
+ build
20
+ commands =
21
+ pip install -e .
@@ -0,0 +1,236 @@
1
+ version = 1
2
+ revision = 2
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "build"
7
+ version = "1.2.2.post1"
8
+ source = { registry = "https://pypi.org/simple" }
9
+ dependencies = [
10
+ { name = "colorama", marker = "os_name == 'nt'" },
11
+ { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" },
12
+ { name = "packaging" },
13
+ { name = "pyproject-hooks" },
14
+ { name = "tomli", marker = "python_full_version < '3.11'" },
15
+ ]
16
+ sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" }
17
+ wheels = [
18
+ { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" },
19
+ ]
20
+
21
+ [[package]]
22
+ name = "cachetools"
23
+ version = "6.1.0"
24
+ source = { registry = "https://pypi.org/simple" }
25
+ sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" }
26
+ wheels = [
27
+ { url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" },
28
+ ]
29
+
30
+ [[package]]
31
+ name = "chardet"
32
+ version = "5.2.0"
33
+ source = { registry = "https://pypi.org/simple" }
34
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" }
35
+ wheels = [
36
+ { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" },
37
+ ]
38
+
39
+ [[package]]
40
+ name = "colorama"
41
+ version = "0.4.6"
42
+ source = { registry = "https://pypi.org/simple" }
43
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
44
+ wheels = [
45
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
46
+ ]
47
+
48
+ [[package]]
49
+ name = "distlib"
50
+ version = "0.3.9"
51
+ source = { registry = "https://pypi.org/simple" }
52
+ sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" }
53
+ wheels = [
54
+ { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" },
55
+ ]
56
+
57
+ [[package]]
58
+ name = "filelock"
59
+ version = "3.18.0"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" }
62
+ wheels = [
63
+ { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
64
+ ]
65
+
66
+ [[package]]
67
+ name = "importlib-metadata"
68
+ version = "8.7.0"
69
+ source = { registry = "https://pypi.org/simple" }
70
+ dependencies = [
71
+ { name = "zipp" },
72
+ ]
73
+ sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
74
+ wheels = [
75
+ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
76
+ ]
77
+
78
+ [[package]]
79
+ name = "packaging"
80
+ version = "25.0"
81
+ source = { registry = "https://pypi.org/simple" }
82
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
83
+ wheels = [
84
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
85
+ ]
86
+
87
+ [[package]]
88
+ name = "platformdirs"
89
+ version = "4.3.8"
90
+ source = { registry = "https://pypi.org/simple" }
91
+ sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
92
+ wheels = [
93
+ { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
94
+ ]
95
+
96
+ [[package]]
97
+ name = "pluggy"
98
+ version = "1.6.0"
99
+ source = { registry = "https://pypi.org/simple" }
100
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
101
+ wheels = [
102
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
103
+ ]
104
+
105
+ [[package]]
106
+ name = "pyproject-api"
107
+ version = "1.9.1"
108
+ source = { registry = "https://pypi.org/simple" }
109
+ dependencies = [
110
+ { name = "packaging" },
111
+ { name = "tomli", marker = "python_full_version < '3.11'" },
112
+ ]
113
+ sdist = { url = "https://files.pythonhosted.org/packages/19/fd/437901c891f58a7b9096511750247535e891d2d5a5a6eefbc9386a2b41d5/pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335", size = 22710, upload-time = "2025-05-12T14:41:58.025Z" }
114
+ wheels = [
115
+ { url = "https://files.pythonhosted.org/packages/ef/e6/c293c06695d4a3ab0260ef124a74ebadba5f4c511ce3a4259e976902c00b/pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948", size = 13158, upload-time = "2025-05-12T14:41:56.217Z" },
116
+ ]
117
+
118
+ [[package]]
119
+ name = "pyproject-hooks"
120
+ version = "1.2.0"
121
+ source = { registry = "https://pypi.org/simple" }
122
+ sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" }
123
+ wheels = [
124
+ { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
125
+ ]
126
+
127
+ [[package]]
128
+ name = "strongbus"
129
+ version = "0.1.0"
130
+ source = { editable = "." }
131
+
132
+ [package.optional-dependencies]
133
+ dev = [
134
+ { name = "build" },
135
+ { name = "tox" },
136
+ ]
137
+
138
+ [package.metadata]
139
+ requires-dist = [
140
+ { name = "build", marker = "extra == 'dev'" },
141
+ { name = "tox", marker = "extra == 'dev'" },
142
+ ]
143
+ provides-extras = ["dev"]
144
+
145
+ [[package]]
146
+ name = "tomli"
147
+ version = "2.2.1"
148
+ source = { registry = "https://pypi.org/simple" }
149
+ sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
150
+ wheels = [
151
+ { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
152
+ { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
153
+ { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
154
+ { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
155
+ { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
156
+ { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
157
+ { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
158
+ { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
159
+ { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
160
+ { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
161
+ { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
162
+ { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
163
+ { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
164
+ { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
165
+ { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
166
+ { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
167
+ { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
168
+ { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
169
+ { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
170
+ { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
171
+ { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
172
+ { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
173
+ { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
174
+ { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
175
+ { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
176
+ { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
177
+ { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
178
+ { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
179
+ { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
180
+ { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
181
+ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
182
+ ]
183
+
184
+ [[package]]
185
+ name = "tox"
186
+ version = "4.27.0"
187
+ source = { registry = "https://pypi.org/simple" }
188
+ dependencies = [
189
+ { name = "cachetools" },
190
+ { name = "chardet" },
191
+ { name = "colorama" },
192
+ { name = "filelock" },
193
+ { name = "packaging" },
194
+ { name = "platformdirs" },
195
+ { name = "pluggy" },
196
+ { name = "pyproject-api" },
197
+ { name = "tomli", marker = "python_full_version < '3.11'" },
198
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
199
+ { name = "virtualenv" },
200
+ ]
201
+ sdist = { url = "https://files.pythonhosted.org/packages/a5/b7/19c01717747076f63c54d871ada081cd711a7c9a7572f2225675c3858b94/tox-4.27.0.tar.gz", hash = "sha256:b97d5ecc0c0d5755bcc5348387fef793e1bfa68eb33746412f4c60881d7f5f57", size = 198351, upload-time = "2025-06-17T15:17:50.585Z" }
202
+ wheels = [
203
+ { url = "https://files.pythonhosted.org/packages/c1/3a/30889167f41ecaffb957ec4409e1cbc1d5d558a5bbbdfb734a5b9911930f/tox-4.27.0-py3-none-any.whl", hash = "sha256:2b8a7fb986b82aa2c830c0615082a490d134e0626dbc9189986da46a313c4f20", size = 173441, upload-time = "2025-06-17T15:17:48.689Z" },
204
+ ]
205
+
206
+ [[package]]
207
+ name = "typing-extensions"
208
+ version = "4.14.1"
209
+ source = { registry = "https://pypi.org/simple" }
210
+ sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
211
+ wheels = [
212
+ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
213
+ ]
214
+
215
+ [[package]]
216
+ name = "virtualenv"
217
+ version = "20.31.2"
218
+ source = { registry = "https://pypi.org/simple" }
219
+ dependencies = [
220
+ { name = "distlib" },
221
+ { name = "filelock" },
222
+ { name = "platformdirs" },
223
+ ]
224
+ sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" }
225
+ wheels = [
226
+ { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" },
227
+ ]
228
+
229
+ [[package]]
230
+ name = "zipp"
231
+ version = "3.23.0"
232
+ source = { registry = "https://pypi.org/simple" }
233
+ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
234
+ wheels = [
235
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
236
+ ]