wexample-event 0.0.75__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.
Files changed (25) hide show
  1. wexample_event-0.0.75/PKG-INFO +620 -0
  2. wexample_event-0.0.75/README.md +605 -0
  3. wexample_event-0.0.75/pyproject.toml +80 -0
  4. wexample_event-0.0.75/src/wexample_event/__init__.py +1 -0
  5. wexample_event-0.0.75/src/wexample_event/common/__init__.py +1 -0
  6. wexample_event-0.0.75/src/wexample_event/common/dispatcher.py +242 -0
  7. wexample_event-0.0.75/src/wexample_event/common/listener.py +100 -0
  8. wexample_event-0.0.75/src/wexample_event/common/listener_state.py +15 -0
  9. wexample_event-0.0.75/src/wexample_event/common/priority.py +14 -0
  10. wexample_event-0.0.75/src/wexample_event/dataclass/__init__.py +1 -0
  11. wexample_event-0.0.75/src/wexample_event/dataclass/event.py +28 -0
  12. wexample_event-0.0.75/src/wexample_event/dataclass/listener_record.py +16 -0
  13. wexample_event-0.0.75/src/wexample_event/dataclass/listener_spec.py +10 -0
  14. wexample_event-0.0.75/src/wexample_event/py.typed +0 -0
  15. wexample_event-0.0.75/tests/package/__init__.py +0 -0
  16. wexample_event-0.0.75/tests/package/common/__init__.py +0 -0
  17. wexample_event-0.0.75/tests/package/common/test_bubbling.py +212 -0
  18. wexample_event-0.0.75/tests/package/common/test_dispatcher.py +495 -0
  19. wexample_event-0.0.75/tests/package/common/test_listener.py +411 -0
  20. wexample_event-0.0.75/tests/package/common/test_listener_state.py +84 -0
  21. wexample_event-0.0.75/tests/package/common/test_priority.py +86 -0
  22. wexample_event-0.0.75/tests/package/dataclass/__init__.py +0 -0
  23. wexample_event-0.0.75/tests/package/dataclass/test_event.py +171 -0
  24. wexample_event-0.0.75/tests/package/dataclass/test_listener_record.py +74 -0
  25. wexample_event-0.0.75/tests/package/dataclass/test_listener_spec.py +68 -0
@@ -0,0 +1,620 @@
1
+ Metadata-Version: 2.1
2
+ Name: wexample-event
3
+ Version: 0.0.75
4
+ Author-Email: weeger <contact@wexample.com>
5
+ License: MIT
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: wexample-helpers==0.0.78
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == "dev"
13
+ Requires-Dist: pytest-cov; extra == "dev"
14
+ Description-Content-Type: text/markdown
15
+
16
+ # wexample-event
17
+
18
+ Version: 0.0.75
19
+
20
+
21
+
22
+ # Quickstart
23
+
24
+ ## Basic Event Dispatching
25
+
26
+ ```python
27
+ from wexample_event.dataclass.event import Event
28
+ from wexample_event.common.dispatcher import EventDispatcherMixin
29
+
30
+ # Create a dispatcher
31
+ class MyApp(EventDispatcherMixin):
32
+ pass
33
+
34
+ app = MyApp()
35
+
36
+ # Add a listener
37
+ def on_user_login(event: Event) -> None:
38
+ print(f"User logged in: {event.payload['username']}")
39
+
40
+ app.add_event_listener("user.login", on_user_login)
41
+
42
+ # Dispatch an event
43
+ app.dispatch("user.login", payload={"username": "john"})
44
+ ```
45
+
46
+ ## Using the Listener Mixin
47
+
48
+ ```python
49
+ from wexample_event.dataclass.event import Event
50
+ from wexample_event.common.listener import EventListenerMixin
51
+ from wexample_event.common.dispatcher import EventDispatcherMixin
52
+
53
+ class MyDispatcher(EventDispatcherMixin):
54
+ pass
55
+
56
+ class MyListener(EventListenerMixin):
57
+ @EventListenerMixin.on("user.login")
58
+ def handle_login(self, event: Event) -> None:
59
+ print(f"Login handled: {event.payload['username']}")
60
+
61
+ @EventListenerMixin.on("user.logout")
62
+ def handle_logout(self, event: Event) -> None:
63
+ print("User logged out")
64
+
65
+ # Setup
66
+ dispatcher = MyDispatcher()
67
+ listener = MyListener()
68
+ listener.bind_to_dispatcher(dispatcher)
69
+
70
+ # Dispatch events
71
+ dispatcher.dispatch("user.login", payload={"username": "jane"})
72
+ dispatcher.dispatch("user.logout")
73
+ ```
74
+
75
+ ## Async Events
76
+
77
+ ```python
78
+ import asyncio
79
+ from wexample_event.dataclass.event import Event
80
+ from wexample_event.common.dispatcher import EventDispatcherMixin
81
+
82
+ class AsyncApp(EventDispatcherMixin):
83
+ pass
84
+
85
+ app = AsyncApp()
86
+
87
+ async def async_handler(event: Event) -> None:
88
+ await asyncio.sleep(0.1)
89
+ print(f"Async handler: {event.name}")
90
+
91
+ app.add_event_listener("async.event", async_handler)
92
+
93
+ # Use dispatch_async for async handlers
94
+ await app.dispatch_async("async.event")
95
+ ```
96
+
97
+ ## Priority and Once
98
+
99
+ ```python
100
+ from wexample_event.dataclass.event import Event
101
+ from wexample_event.common.priority import EventPriority
102
+ from wexample_event.common.dispatcher import EventDispatcherMixin
103
+
104
+ app = EventDispatcherMixin()
105
+
106
+ # High priority listener (runs first)
107
+ def high_priority(event: Event) -> None:
108
+ print("High priority")
109
+
110
+ # Low priority listener (runs last)
111
+ def low_priority(event: Event) -> None:
112
+ print("Low priority")
113
+
114
+ # One-time listener
115
+ def once_handler(event: Event) -> None:
116
+ print("This runs only once")
117
+
118
+ app.add_event_listener("test", high_priority, priority=EventPriority.HIGH)
119
+ app.add_event_listener("test", low_priority, priority=EventPriority.LOW)
120
+ app.add_event_listener("test", once_handler, once=True)
121
+
122
+ app.dispatch("test") # All three run
123
+ app.dispatch("test") # Only high and low priority run
124
+ ```
125
+
126
+ # API Reference
127
+
128
+ ## Core Classes
129
+
130
+ ### Event
131
+
132
+ Immutable dataclass representing an event.
133
+
134
+ ```python
135
+ @dataclass(frozen=True, slots=True)
136
+ class Event:
137
+ name: str
138
+ payload: Mapping[str, Any] | None = None
139
+ metadata: Mapping[str, Any] | None = None
140
+ source: Any | None = None
141
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
142
+ ```
143
+
144
+ **Methods:**
145
+
146
+ - `with_update(**changes)` - Returns a copy with updated fields
147
+ - `derive(name=None, **changes)` - Creates a derived event, optionally with a new name
148
+
149
+ ### EventDispatcherMixin
150
+
151
+ Mixin providing event dispatching capabilities.
152
+
153
+ **Methods:**
154
+
155
+ - `add_event_listener(name, callback, *, once=False, priority=DEFAULT_PRIORITY)` - Register a listener
156
+ - `remove_event_listener(name, callback)` - Remove a listener (returns bool)
157
+ - `clear_event_listeners(name=None)` - Clear listeners (all or for specific event)
158
+ - `has_event_listeners(name)` - Check if event has listeners
159
+ - `dispatch(event, *, payload=None, metadata=None, source=_UNSET)` - Dispatch synchronously
160
+ - `dispatch_async(event, *, payload=None, metadata=None, source=_UNSET)` - Dispatch asynchronously
161
+ - `dispatch_event(...)` - Alias for `dispatch`
162
+ - `dispatch_event_async(...)` - Alias for `dispatch_async`
163
+
164
+ ### EventListenerMixin
165
+
166
+ Mixin for declarative event listeners using decorators.
167
+
168
+ **Class Method:**
169
+
170
+ - `@on(event_name, *, priority=DEFAULT_PRIORITY, once=False)` - Decorator to mark methods as listeners
171
+
172
+ **Methods:**
173
+
174
+ - `bind_to_dispatcher(dispatcher)` - Bind all decorated methods to a dispatcher
175
+ - `unbind_from_dispatcher()` - Unbind from current dispatcher
176
+ - `get_bound_dispatcher()` - Get the currently bound dispatcher
177
+
178
+ ### EventPriority
179
+
180
+ Enum for common priority values.
181
+
182
+ ```python
183
+ class EventPriority(IntEnum):
184
+ LOW = -100
185
+ NORMAL = 0
186
+ HIGH = 100
187
+ ```
188
+
189
+ **Constant:**
190
+
191
+ - `DEFAULT_PRIORITY = EventPriority.NORMAL`
192
+
193
+ ## Type Aliases
194
+
195
+ ```python
196
+ EventCallback = Callable[[Event], Awaitable[None] | None]
197
+ ```
198
+
199
+ ## Dataclasses
200
+
201
+ ### ListenerRecord
202
+
203
+ Internal dataclass storing listener information.
204
+
205
+ ```python
206
+ @dataclass(slots=True)
207
+ class ListenerRecord:
208
+ callback: EventCallback
209
+ once: bool
210
+ priority: int
211
+ order: int
212
+ ```
213
+
214
+ ### ListenerSpec
215
+
216
+ Internal dataclass for decorator metadata.
217
+
218
+ ```python
219
+ @dataclass(frozen=True, slots=True)
220
+ class ListenerSpec:
221
+ name: str
222
+ priority: int
223
+ once: bool
224
+ ```
225
+
226
+ ### ListenerState
227
+
228
+ Internal class managing listener binding state.
229
+
230
+ ```python
231
+ class ListenerState:
232
+ dispatcher: EventDispatcherMixin | None
233
+ bindings: List[tuple[str, EventCallback]]
234
+ ```
235
+
236
+ ## Usage Patterns
237
+
238
+ ### Synchronous Listener
239
+
240
+ ```python
241
+ def my_listener(event: Event) -> None:
242
+ print(event.name)
243
+ ```
244
+
245
+ ### Asynchronous Listener
246
+
247
+ ```python
248
+ async def my_async_listener(event: Event) -> None:
249
+ await some_async_operation()
250
+ ```
251
+
252
+ ### Decorator Pattern
253
+
254
+ ```python
255
+ class MyListener(EventListenerMixin):
256
+ @EventListenerMixin.on("event.name", priority=EventPriority.HIGH)
257
+ def handle_event(self, event: Event) -> None:
258
+ pass
259
+ ```
260
+
261
+ ### Custom Priority
262
+
263
+ ```python
264
+ dispatcher.add_event_listener("event", callback, priority=50)
265
+ ```
266
+
267
+ ### Once Listener
268
+
269
+ ```python
270
+ dispatcher.add_event_listener("event", callback, once=True)
271
+ ```
272
+
273
+ # Examples
274
+
275
+ ## Plugin System
276
+
277
+ ```python
278
+ from wexample_event import Event, EventDispatcherMixin, EventListenerMixin
279
+
280
+ class Application(EventDispatcherMixin):
281
+ def start(self) -> None:
282
+ self.dispatch("app.startup")
283
+ print("Application started")
284
+
285
+ def stop(self) -> None:
286
+ self.dispatch("app.shutdown")
287
+ print("Application stopped")
288
+
289
+ class LoggingPlugin(EventListenerMixin):
290
+ @EventListenerMixin.on("app.startup")
291
+ def on_startup(self, event: Event) -> None:
292
+ print("[LOG] Application is starting...")
293
+
294
+ @EventListenerMixin.on("app.shutdown")
295
+ def on_shutdown(self, event: Event) -> None:
296
+ print("[LOG] Application is shutting down...")
297
+
298
+ class MetricsPlugin(EventListenerMixin):
299
+ @EventListenerMixin.on("app.startup")
300
+ def on_startup(self, event: Event) -> None:
301
+ print("[METRICS] Recording startup time")
302
+
303
+ # Setup
304
+ app = Application()
305
+ logging_plugin = LoggingPlugin()
306
+ metrics_plugin = MetricsPlugin()
307
+
308
+ logging_plugin.bind_to_dispatcher(app)
309
+ metrics_plugin.bind_to_dispatcher(app)
310
+
311
+ # Run
312
+ app.start()
313
+ app.stop()
314
+ ```
315
+
316
+ ## State Change Notifications
317
+
318
+ ```python
319
+ from wexample_event import Event, EventDispatcherMixin
320
+
321
+ class DataStore(EventDispatcherMixin):
322
+ def __init__(self) -> None:
323
+ self._data: dict = {}
324
+
325
+ def set(self, key: str, value: any) -> None:
326
+ old_value = self._data.get(key)
327
+ self._data[key] = value
328
+
329
+ self.dispatch(
330
+ "data.changed",
331
+ payload={"key": key, "old": old_value, "new": value}
332
+ )
333
+
334
+ def get(self, key: str) -> any:
335
+ return self._data.get(key)
336
+
337
+ # Create store and add listeners
338
+ store = DataStore()
339
+
340
+ def on_data_changed(event: Event) -> None:
341
+ payload = event.payload
342
+ print(f"Data changed: {payload['key']} = {payload['new']}")
343
+
344
+ store.add_event_listener("data.changed", on_data_changed)
345
+
346
+ # Use the store
347
+ store.set("username", "alice")
348
+ store.set("username", "bob")
349
+ ```
350
+
351
+ ## Request/Response Pipeline
352
+
353
+ ```python
354
+ from wexample_event import Event, EventDispatcherMixin, EventPriority
355
+
356
+ class RequestPipeline(EventDispatcherMixin):
357
+ def process(self, request: dict) -> dict:
358
+ # Create event with mutable response
359
+ response = {"status": "pending", "data": request}
360
+
361
+ self.dispatch(
362
+ "request.process",
363
+ payload={"request": request, "response": response}
364
+ )
365
+
366
+ return response
367
+
368
+ pipeline = RequestPipeline()
369
+
370
+ # Authentication middleware (high priority)
371
+ def authenticate(event: Event) -> None:
372
+ response = event.payload["response"]
373
+ request = event.payload["request"]
374
+
375
+ if not request.get("auth_token"):
376
+ response["status"] = "error"
377
+ response["message"] = "Authentication required"
378
+ else:
379
+ response["authenticated"] = True
380
+
381
+ # Validation middleware (normal priority)
382
+ def validate(event: Event) -> None:
383
+ response = event.payload["response"]
384
+ if response["status"] == "error":
385
+ return # Skip if already errored
386
+
387
+ request = event.payload["request"]
388
+ if not request.get("data"):
389
+ response["status"] = "error"
390
+ response["message"] = "Data required"
391
+
392
+ # Processing (low priority)
393
+ def process_data(event: Event) -> None:
394
+ response = event.payload["response"]
395
+ if response["status"] == "error":
396
+ return
397
+
398
+ response["status"] = "success"
399
+ response["processed"] = True
400
+
401
+ pipeline.add_event_listener("request.process", authenticate, priority=EventPriority.HIGH)
402
+ pipeline.add_event_listener("request.process", validate, priority=EventPriority.NORMAL)
403
+ pipeline.add_event_listener("request.process", process_data, priority=EventPriority.LOW)
404
+
405
+ # Process requests
406
+ result1 = pipeline.process({"auth_token": "abc", "data": {"value": 123}})
407
+ print(result1) # Success
408
+
409
+ result2 = pipeline.process({"data": {"value": 456}})
410
+ print(result2) # Error: Authentication required
411
+ ```
412
+
413
+ ## Event Inheritance
414
+
415
+ ```python
416
+ from wexample_event import Event, EventDispatcherMixin, EventListenerMixin
417
+
418
+ class BaseListener(EventListenerMixin):
419
+ @EventListenerMixin.on("base.event")
420
+ def handle_base(self, event: Event) -> None:
421
+ print("Base handler")
422
+
423
+ class ExtendedListener(BaseListener):
424
+ @EventListenerMixin.on("extended.event")
425
+ def handle_extended(self, event: Event) -> None:
426
+ print("Extended handler")
427
+
428
+ dispatcher = EventDispatcherMixin()
429
+ listener = ExtendedListener()
430
+ listener.bind_to_dispatcher(dispatcher)
431
+
432
+ # Both base and extended events work
433
+ dispatcher.dispatch("base.event") # Prints: Base handler
434
+ dispatcher.dispatch("extended.event") # Prints: Extended handler
435
+ ```
436
+
437
+ ## Dynamic Event Names
438
+
439
+ ```python
440
+ from wexample_event import Event, EventDispatcherMixin
441
+
442
+ class EventBus(EventDispatcherMixin):
443
+ def emit(self, event_type: str, **data) -> None:
444
+ self.dispatch(f"event.{event_type}", payload=data)
445
+
446
+ bus = EventBus()
447
+
448
+ # Add wildcard-style handlers
449
+ def handle_user_events(event: Event) -> None:
450
+ print(f"User event: {event.name}")
451
+
452
+ bus.add_event_listener("event.user.login", handle_user_events)
453
+ bus.add_event_listener("event.user.logout", handle_user_events)
454
+
455
+ # Emit events
456
+ bus.emit("user.login", username="alice")
457
+ bus.emit("user.logout", username="alice")
458
+ ```
459
+
460
+ ## Tests
461
+
462
+ This project uses `pytest` for testing and `pytest-cov` for code coverage analysis.
463
+
464
+ ### Installation
465
+
466
+ First, install the required testing dependencies:
467
+ ```bash
468
+ .venv/bin/python -m pip install pytest pytest-cov
469
+ ```
470
+
471
+ ### Basic Usage
472
+
473
+ Run all tests with coverage:
474
+ ```bash
475
+ .venv/bin/python -m pytest --cov --cov-report=html
476
+ ```
477
+
478
+ ### Common Commands
479
+ ```bash
480
+ # Run tests with coverage for a specific module
481
+ .venv/bin/python -m pytest --cov=your_module
482
+
483
+ # Show which lines are not covered
484
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing
485
+
486
+ # Generate an HTML coverage report
487
+ .venv/bin/python -m pytest --cov=your_module --cov-report=html
488
+
489
+ # Combine terminal and HTML reports
490
+ .venv/bin/python -m pytest --cov=your_module --cov-report=term-missing --cov-report=html
491
+
492
+ # Run specific test file with coverage
493
+ .venv/bin/python -m pytest tests/test_file.py --cov=your_module --cov-report=term-missing
494
+ ```
495
+
496
+ ### Viewing HTML Reports
497
+
498
+ After generating an HTML report, open `htmlcov/index.html` in your browser to view detailed line-by-line coverage information.
499
+
500
+ ### Coverage Threshold
501
+
502
+ To enforce a minimum coverage percentage:
503
+ ```bash
504
+ .venv/bin/python -m pytest --cov=your_module --cov-fail-under=80
505
+ ```
506
+
507
+ This will cause the test suite to fail if coverage drops below 80%.
508
+
509
+ ## Code Quality & Typing
510
+
511
+ All the suite packages follow strict quality standards:
512
+
513
+ - **Type hints**: Full type coverage with mypy validation
514
+ - **Code formatting**: Enforced with black and isort
515
+ - **Linting**: Comprehensive checks with custom scripts and tools
516
+ - **Testing**: High test coverage requirements
517
+
518
+ These standards ensure reliability and maintainability across the suite.
519
+
520
+ ## Versioning & Compatibility Policy
521
+
522
+ Wexample packages follow **Semantic Versioning** (SemVer):
523
+
524
+ - **MAJOR**: Breaking changes
525
+ - **MINOR**: New features, backward compatible
526
+ - **PATCH**: Bug fixes, backward compatible
527
+
528
+ We maintain backward compatibility within major versions and provide clear migration guides for breaking changes.
529
+
530
+ ## Changelog
531
+
532
+ See [CHANGELOG.md](CHANGELOG.md) for detailed version history and release notes.
533
+
534
+ Major changes are documented with migration guides when applicable.
535
+
536
+ ## Migration Notes
537
+
538
+ When upgrading between major versions, refer to the migration guides in the documentation.
539
+
540
+ Breaking changes are clearly documented with upgrade paths and examples.
541
+
542
+ ## Known Limitations & Roadmap
543
+
544
+ Current limitations and planned features are tracked in the GitHub issues.
545
+
546
+ See the [project roadmap](/issues) for upcoming features and improvements.
547
+
548
+ ## Security Policy
549
+
550
+ ### Reporting Vulnerabilities
551
+
552
+ If you discover a security vulnerability, please email security@wexample.com.
553
+
554
+ **Do not** open public issues for security vulnerabilities.
555
+
556
+ We take security seriously and will respond promptly to verified reports.
557
+
558
+ ## Privacy & Telemetry
559
+
560
+ This package does **not** collect any telemetry or usage data.
561
+
562
+ Your privacy is respected — no data is transmitted to external services.
563
+
564
+ ## Support Channels
565
+
566
+ - **GitHub Issues**: Bug reports and feature requests
567
+ - **GitHub Discussions**: Questions and community support
568
+ - **Documentation**: Comprehensive guides and API reference
569
+ - **Email**: contact@wexample.com for general inquiries
570
+
571
+ Community support is available through GitHub Discussions.
572
+
573
+ ## Contribution Guidelines
574
+
575
+ We welcome contributions to the Wexample suite!
576
+
577
+ ### How to Contribute
578
+
579
+ 1. **Fork** the repository
580
+ 2. **Create** a feature branch
581
+ 3. **Make** your changes
582
+ 4. **Test** thoroughly
583
+ 5. **Submit** a pull request
584
+
585
+ ## Maintainers & Authors
586
+
587
+ Maintained by the Wexample team and community contributors.
588
+
589
+ See [CONTRIBUTORS.md](CONTRIBUTORS.md) for the full list of contributors.
590
+
591
+ ## License
592
+
593
+ MIT
594
+
595
+ ## Useful Links
596
+
597
+ - **Homepage**:
598
+ - **Documentation**: [docs.wexample.com](https://docs.wexample.com)
599
+ - **Issue Tracker**: /issues
600
+ - **Discussions**: /discussions
601
+ - **PyPI**: [pypi.org/project/wexample-event](https://pypi.org/project/wexample-event/)
602
+
603
+ ## Integration in the Suite
604
+
605
+ This package is part of the **Wexample Suite** — a collection of high-quality Python packages designed to work seamlessly together.
606
+
607
+ ### Related Packages
608
+
609
+ The suite includes packages for configuration management, file handling, prompts, and more. Each package can be used independently or as part of the integrated suite.
610
+
611
+ Visit the [Wexample Suite documentation](https://docs.wexample.com) for the complete package ecosystem.
612
+
613
+ # About us
614
+
615
+ Wexample stands as a cornerstone of the digital ecosystem — a collective of seasoned engineers, researchers, and creators driven by a relentless pursuit of technological excellence. More than a media platform, it has grown into a vibrant community where innovation meets craftsmanship, and where every line of code reflects a commitment to clarity, durability, and shared intelligence.
616
+
617
+ This packages suite embodies this spirit. Trusted by professionals and enthusiasts alike, it delivers a consistent, high-quality foundation for modern development — open, elegant, and battle-tested. Its reputation is built on years of collaboration, refinement, and rigorous attention to detail, making it a natural choice for those who demand both robustness and beauty in their tools.
618
+
619
+ Wexample cultivates a culture of mastery. Each package, each contribution carries the mark of a community that values precision, ethics, and innovation — a community proud to shape the future of digital craftsmanship.
620
+