├── tests ├── __init__.py ├── pyventus │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ ├── utils │ │ │ ├── __init__.py │ │ │ └── test_repr_utils.py │ │ ├── collections │ │ │ └── __init__.py │ │ ├── exceptions │ │ │ ├── __init__.py │ │ │ ├── test_pyventus_exception.py │ │ │ └── test_pyventus_import_exception.py │ │ ├── loggers │ │ │ ├── __init__.py │ │ │ └── test_logger.py │ │ ├── processing │ │ │ ├── __init__.py │ │ │ ├── redis │ │ │ │ ├── __init__.py │ │ │ │ └── test_redis_processing_service.py │ │ │ ├── asyncio │ │ │ │ ├── __init__.py │ │ │ │ └── test_asyncio_processing_service.py │ │ │ ├── celery │ │ │ │ ├── __init__.py │ │ │ │ └── test_celery_processing_service.py │ │ │ ├── executor │ │ │ │ ├── __init__.py │ │ │ │ └── test_executor_processing_service.py │ │ │ ├── fastapi │ │ │ │ ├── __init__.py │ │ │ │ └── test_fastapi_processing_service.py │ │ │ └── processing_service_test.py │ │ └── subscriptions │ │ │ ├── __init__.py │ │ │ └── test_subscription.py │ ├── events │ │ ├── __init__.py │ │ ├── emitters │ │ │ ├── __init__.py │ │ │ └── test_event_emitter_utils.py │ │ ├── linkers │ │ │ └── __init__.py │ │ └── subscribers │ │ │ └── __init__.py │ └── reactive │ │ ├── __init__.py │ │ ├── observables │ │ ├── __init__.py │ │ └── test_observable_utils.py │ │ └── subscribers │ │ ├── __init__.py │ │ └── test_subscriber.py ├── utils │ ├── __init__.py │ └── object_utils.py ├── benchmarks │ └── __init__.py └── fixtures │ ├── __init__.py │ └── event_fixtures.py ├── src └── pyventus │ ├── reactive │ ├── observers │ │ ├── __init__.py │ │ └── observer.py │ ├── subscribers │ │ └── __init__.py │ ├── observables │ │ ├── __init__.py │ │ └── observable_utils.py │ └── __init__.py │ ├── core │ ├── __init__.py │ ├── collections │ │ └── __init__.py │ ├── constants │ │ ├── __init__.py │ │ └── stdout_colors.py │ ├── loggers │ │ ├── __init__.py │ │ └── logger.py │ ├── processing │ │ ├── redis │ │ │ ├── __init__.py │ │ │ └── redis_processing_service.py │ │ ├── asyncio │ │ │ ├── __init__.py │ │ │ └── asyncio_processing_service.py │ │ ├── celery │ │ │ ├── __init__.py │ │ │ └── celery_processing_service.py │ │ ├── fastapi │ │ │ ├── __init__.py │ │ │ └── fastapi_processing_service.py │ │ ├── executor │ │ │ ├── __init__.py │ │ │ └── executor_processing_service.py │ │ ├── __init__.py │ │ └── processing_service.py │ ├── exceptions │ │ ├── __init__.py │ │ ├── pyventus_exception.py │ │ └── pyventus_import_exception.py │ ├── subscriptions │ │ ├── __init__.py │ │ ├── unsubscribable.py │ │ └── subscription.py │ └── utils │ │ ├── __init__.py │ │ └── repr_utils.py │ ├── events │ ├── handlers │ │ ├── __init__.py │ │ └── event_handler.py │ ├── linkers │ │ └── __init__.py │ ├── subscribers │ │ ├── __init__.py │ │ └── event_subscriber.py │ ├── emitters │ │ ├── __init__.py │ │ └── event_emitter_utils.py │ └── __init__.py │ └── __init__.py ├── docs ├── api │ ├── events │ │ ├── emitters │ │ │ ├── index.md │ │ │ └── event_emitter_utils.md │ │ ├── linkers │ │ │ └── event_linker.md │ │ ├── handlers │ │ │ └── event_handler.md │ │ ├── subscribers │ │ │ └── event_subscriber.md │ │ └── index.md │ ├── reactive │ │ ├── observers │ │ │ └── observer.md │ │ ├── observables │ │ │ ├── index.md │ │ │ ├── observable_utils.md │ │ │ └── observable_task.md │ │ ├── subscribers │ │ │ └── subscriber.md │ │ └── index.md │ ├── core │ │ ├── collections │ │ │ └── multi_bidict.md │ │ ├── index.md │ │ ├── processing │ │ │ ├── index.md │ │ │ ├── redis_processing_service.md │ │ │ ├── celery_processing_service.md │ │ │ ├── asyncio_processing_service.md │ │ │ ├── executor_processing_service.md │ │ │ └── fastapi_processing_service.md │ │ ├── subscriptions │ │ │ ├── subscription.md │ │ │ ├── unsubscribable.md │ │ │ └── subscription_context.md │ │ └── exceptions │ │ │ ├── pyventus_exception.md │ │ │ └── pyventus_import_exception.md │ └── index.md ├── images │ ├── logo │ │ ├── pyventus-logo.png │ │ ├── pyventus-logo-black.png │ │ ├── pyventus-logo-name.png │ │ ├── pyventus-logo-white.png │ │ └── pyventus-logo-name-slogan.png │ ├── favicon │ │ └── pyventus-logo.ico │ └── examples │ │ ├── debug-mode-example.png │ │ ├── black-circuit-board.jpg │ │ ├── events-debug-mode-example.png │ │ └── rack-of-electronic-equipment.jpg ├── .overrides │ ├── outdated.html │ ├── main.html │ ├── announce.html │ └── 404.html ├── learn │ ├── reactive │ │ └── index.md │ ├── index.md │ ├── events │ │ ├── index.md │ │ └── emitters │ │ │ ├── asyncio.md │ │ │ ├── fastapi.md │ │ │ ├── executor.md │ │ │ ├── redis.md │ │ │ └── index.md │ └── upgrade_guide.md ├── javascripts │ ├── announcement.js │ └── mathjax.js ├── stylesheets │ ├── search-bar.css │ └── announcement.css └── getting-started.md ├── .github ├── CONTRIBUTING.md ├── SECURITY.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── publish-to-pypi.yml │ ├── deploy-docs.yml │ └── run-tests.yml └── CODE_OF_CONDUCT.md ├── CITATION.cff ├── LICENSE ├── .gitignore └── mkdocs.yml /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/events/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/reactive/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/collections/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/loggers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/processing/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/events/emitters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/events/linkers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/processing/redis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/subscriptions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/events/subscribers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/reactive/observables/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/reactive/subscribers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/processing/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/processing/celery/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/processing/executor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/pyventus/core/processing/fastapi/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pyventus/reactive/observers/__init__.py: -------------------------------------------------------------------------------- 1 | from .observer import Observer 2 | -------------------------------------------------------------------------------- /src/pyventus/core/__init__.py: -------------------------------------------------------------------------------- 1 | """The essential building blocks of Pyventus.""" 2 | -------------------------------------------------------------------------------- /src/pyventus/core/collections/__init__.py: -------------------------------------------------------------------------------- 1 | from .multi_bidict import MultiBidict 2 | -------------------------------------------------------------------------------- /src/pyventus/core/constants/__init__.py: -------------------------------------------------------------------------------- 1 | from .stdout_colors import StdOutColors 2 | -------------------------------------------------------------------------------- /src/pyventus/events/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_handler import EventHandler 2 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .object_utils import get_private_attr, has_private_attr 2 | -------------------------------------------------------------------------------- /tests/benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_emitter_benchmark import EventEmitterBenchmark 2 | -------------------------------------------------------------------------------- /docs/api/events/emitters/index.md: -------------------------------------------------------------------------------- 1 | # `EventEmitter` class 2 | 3 | ::: pyventus.events.EventEmitter 4 | -------------------------------------------------------------------------------- /docs/api/reactive/observers/observer.md: -------------------------------------------------------------------------------- 1 | # `Observer` class 2 | 3 | ::: pyventus.reactive.Observer 4 | -------------------------------------------------------------------------------- /docs/api/events/linkers/event_linker.md: -------------------------------------------------------------------------------- 1 | # `EventLinker` class 2 | 3 | ::: pyventus.events.EventLinker 4 | -------------------------------------------------------------------------------- /docs/api/reactive/observables/index.md: -------------------------------------------------------------------------------- 1 | # `Observable` class 2 | 3 | ::: pyventus.reactive.Observable 4 | -------------------------------------------------------------------------------- /src/pyventus/events/linkers/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_linker import EventLinker, SubscribableEventType 2 | -------------------------------------------------------------------------------- /docs/api/events/handlers/event_handler.md: -------------------------------------------------------------------------------- 1 | # `EventHandler` class 2 | 3 | ::: pyventus.events.EventHandler 4 | -------------------------------------------------------------------------------- /docs/api/reactive/subscribers/subscriber.md: -------------------------------------------------------------------------------- 1 | # `Subscriber` class 2 | 3 | ::: pyventus.reactive.Subscriber 4 | -------------------------------------------------------------------------------- /src/pyventus/core/loggers/__init__.py: -------------------------------------------------------------------------------- 1 | from .logger import Logger 2 | from .stdout_logger import StdOutLogger 3 | -------------------------------------------------------------------------------- /src/pyventus/core/processing/redis/__init__.py: -------------------------------------------------------------------------------- 1 | from .redis_processing_service import RedisProcessingService 2 | -------------------------------------------------------------------------------- /docs/api/core/collections/multi_bidict.md: -------------------------------------------------------------------------------- 1 | # `MultiBidict` class 2 | 3 | ::: pyventus.core.collections.MultiBidict 4 | -------------------------------------------------------------------------------- /src/pyventus/core/processing/asyncio/__init__.py: -------------------------------------------------------------------------------- 1 | from .asyncio_processing_service import AsyncIOProcessingService 2 | -------------------------------------------------------------------------------- /src/pyventus/core/processing/celery/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery_processing_service import CeleryProcessingService 2 | -------------------------------------------------------------------------------- /src/pyventus/core/processing/fastapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .fastapi_processing_service import FastAPIProcessingService 2 | -------------------------------------------------------------------------------- /docs/api/core/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - feedback 4 | --- 5 | 6 | # `Core` module 7 | 8 | ::: pyventus.core 9 | -------------------------------------------------------------------------------- /docs/api/core/processing/index.md: -------------------------------------------------------------------------------- 1 | # `ProcessingService` class 2 | 3 | ::: pyventus.core.processing.ProcessingService 4 | -------------------------------------------------------------------------------- /docs/api/events/subscribers/event_subscriber.md: -------------------------------------------------------------------------------- 1 | # `EventSubscriber` class 2 | 3 | ::: pyventus.events.EventSubscriber 4 | -------------------------------------------------------------------------------- /docs/images/logo/pyventus-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/logo/pyventus-logo.png -------------------------------------------------------------------------------- /src/pyventus/core/processing/executor/__init__.py: -------------------------------------------------------------------------------- 1 | from .executor_processing_service import ExecutorProcessingService 2 | -------------------------------------------------------------------------------- /docs/api/core/subscriptions/subscription.md: -------------------------------------------------------------------------------- 1 | # `Subscription` class 2 | 3 | ::: pyventus.core.subscriptions.Subscription 4 | -------------------------------------------------------------------------------- /docs/images/favicon/pyventus-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/favicon/pyventus-logo.ico -------------------------------------------------------------------------------- /src/pyventus/core/processing/__init__.py: -------------------------------------------------------------------------------- 1 | from .processing_service import ProcessingService, ProcessingServiceCallbackType 2 | -------------------------------------------------------------------------------- /docs/api/core/subscriptions/unsubscribable.md: -------------------------------------------------------------------------------- 1 | # `Unsubscribable` class 2 | 3 | ::: pyventus.core.subscriptions.Unsubscribable 4 | -------------------------------------------------------------------------------- /docs/images/logo/pyventus-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/logo/pyventus-logo-black.png -------------------------------------------------------------------------------- /docs/images/logo/pyventus-logo-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/logo/pyventus-logo-name.png -------------------------------------------------------------------------------- /docs/images/logo/pyventus-logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/logo/pyventus-logo-white.png -------------------------------------------------------------------------------- /docs/api/core/exceptions/pyventus_exception.md: -------------------------------------------------------------------------------- 1 | # `PyventusException` class 2 | 3 | ::: pyventus.core.exceptions.PyventusException 4 | -------------------------------------------------------------------------------- /docs/images/examples/debug-mode-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/examples/debug-mode-example.png -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .callable_fixtures import CallableMock, DummyCallable 2 | from .event_fixtures import EventFixtures 3 | -------------------------------------------------------------------------------- /docs/images/examples/black-circuit-board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/examples/black-circuit-board.jpg -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please read the [Contributing](https://mdapena.github.io/pyventus/latest/contributing/) guidelines in the documentation site. -------------------------------------------------------------------------------- /docs/api/core/subscriptions/subscription_context.md: -------------------------------------------------------------------------------- 1 | # `SubscriptionContext` class 2 | 3 | ::: pyventus.core.subscriptions.SubscriptionContext 4 | -------------------------------------------------------------------------------- /docs/images/logo/pyventus-logo-name-slogan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/logo/pyventus-logo-name-slogan.png -------------------------------------------------------------------------------- /docs/api/core/exceptions/pyventus_import_exception.md: -------------------------------------------------------------------------------- 1 | # `PyventusImportException` class 2 | 3 | ::: pyventus.core.exceptions.PyventusImportException 4 | -------------------------------------------------------------------------------- /docs/images/examples/events-debug-mode-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/examples/events-debug-mode-example.png -------------------------------------------------------------------------------- /docs/api/core/processing/redis_processing_service.md: -------------------------------------------------------------------------------- 1 | # `RedisProcessingService` class 2 | 3 | ::: pyventus.core.processing.redis.RedisProcessingService 4 | -------------------------------------------------------------------------------- /docs/images/examples/rack-of-electronic-equipment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdapena/pyventus/HEAD/docs/images/examples/rack-of-electronic-equipment.jpg -------------------------------------------------------------------------------- /docs/api/core/processing/celery_processing_service.md: -------------------------------------------------------------------------------- 1 | # `CeleryProcessingService` class 2 | 3 | ::: pyventus.core.processing.celery.CeleryProcessingService 4 | -------------------------------------------------------------------------------- /src/pyventus/core/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .pyventus_exception import PyventusException 2 | from .pyventus_import_exception import PyventusImportException 3 | -------------------------------------------------------------------------------- /docs/.overrides/outdated.html: -------------------------------------------------------------------------------- 1 | You're not viewing the latest version. 2 | 3 | Click here to go to latest. 4 | -------------------------------------------------------------------------------- /docs/api/core/processing/asyncio_processing_service.md: -------------------------------------------------------------------------------- 1 | # `AsyncIOProcessingService` class 2 | 3 | ::: pyventus.core.processing.asyncio.AsyncIOProcessingService 4 | -------------------------------------------------------------------------------- /docs/api/core/processing/executor_processing_service.md: -------------------------------------------------------------------------------- 1 | # `ExecutorProcessingService` class 2 | 3 | ::: pyventus.core.processing.executor.ExecutorProcessingService 4 | -------------------------------------------------------------------------------- /docs/api/core/processing/fastapi_processing_service.md: -------------------------------------------------------------------------------- 1 | # `FastAPIProcessingService` class 2 | 3 | ::: pyventus.core.processing.fastapi.FastAPIProcessingService 4 | -------------------------------------------------------------------------------- /docs/api/events/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - feedback 4 | --- 5 | 6 | # `Events` module 7 | 8 | ::: pyventus.events 9 | options: 10 | members: 11 | - __doc__ 12 | -------------------------------------------------------------------------------- /src/pyventus/reactive/subscribers/__init__.py: -------------------------------------------------------------------------------- 1 | from .subscriber import ( 2 | CompleteCallbackType, 3 | ErrorCallbackType, 4 | NextCallbackType, 5 | Subscriber, 6 | ) 7 | -------------------------------------------------------------------------------- /docs/api/reactive/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - feedback 4 | --- 5 | 6 | # `Reactive` module 7 | 8 | ::: pyventus.reactive 9 | options: 10 | members: 11 | - __doc__ 12 | -------------------------------------------------------------------------------- /src/pyventus/core/subscriptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .subscription import Subscription 2 | from .subscription_context import SubscriptionContext 3 | from .unsubscribable import Unsubscribable 4 | -------------------------------------------------------------------------------- /docs/api/reactive/observables/observable_utils.md: -------------------------------------------------------------------------------- 1 | ::: pyventus.reactive.observables.observable_utils 2 | options: 3 | show_root_toc_entry: false 4 | members: 5 | - as_observable_task -------------------------------------------------------------------------------- /src/pyventus/events/subscribers/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_subscriber import ( 2 | EventCallbackType, 3 | EventSubscriber, 4 | FailureCallbackType, 5 | SuccessCallbackType, 6 | ) 7 | -------------------------------------------------------------------------------- /docs/learn/reactive/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | - feedback 5 | --- 6 | 7 | # Reactive Programming 8 | 9 | !!! warning "🏗️ Work in Progress" 10 | 11 | This section is currently a work in progress. 12 | -------------------------------------------------------------------------------- /docs/.overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block outdated %} 4 | {% include 'outdated.html' %} 5 | {% endblock %} 6 | 7 | {% block announce %} 8 | {% include 'announce.html' ignore missing %} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /src/pyventus/reactive/observables/__init__.py: -------------------------------------------------------------------------------- 1 | from .observable import Completed, Observable 2 | from .observable_task import ObservableTask, ObservableTaskCallbackReturnType, ObservableTaskCallbackType 3 | from .observable_utils import as_observable_task 4 | -------------------------------------------------------------------------------- /docs/api/reactive/observables/observable_task.md: -------------------------------------------------------------------------------- 1 | # `ObservableTask` class 2 | 3 | ::: pyventus.reactive.ObservableTask 4 | options: 5 | filters: ["!^(_.*$)", "^(__init__|__init_subclass__|__call__|__enter__|__exit__)$", "!^ObservableSubCtx$", "!^Completed$"] 6 | -------------------------------------------------------------------------------- /src/pyventus/core/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .callable_utils import ( 2 | CallableWrapper, 3 | get_callable_name, 4 | is_callable_async, 5 | is_callable_generator, 6 | validate_callable, 7 | ) 8 | from .repr_utils import attributes_repr, formatted_repr, hex_id_repr, summarized_repr 9 | -------------------------------------------------------------------------------- /src/pyventus/__init__.py: -------------------------------------------------------------------------------- 1 | """A powerful Python library for event-driven and reactive programming.""" 2 | 3 | __version__ = "0.7.2" 4 | 5 | from .core.exceptions import PyventusException, PyventusImportException 6 | 7 | __all__ = [ 8 | "PyventusException", 9 | "PyventusImportException", 10 | ] 11 | -------------------------------------------------------------------------------- /src/pyventus/events/emitters/__init__.py: -------------------------------------------------------------------------------- 1 | from .event_emitter import EmittableEventType, EventEmitter 2 | from .event_emitter_utils import ( 3 | AsyncIOEventEmitter, 4 | CeleryEventEmitter, 5 | ExecutorEventEmitter, 6 | ExecutorEventEmitterCtx, 7 | FastAPIEventEmitter, 8 | RedisEventEmitter, 9 | ) 10 | -------------------------------------------------------------------------------- /docs/api/events/emitters/event_emitter_utils.md: -------------------------------------------------------------------------------- 1 | ::: pyventus.events.emitters.event_emitter_utils 2 | options: 3 | show_root_toc_entry: false 4 | members: 5 | - AsyncIOEventEmitter 6 | - CeleryEventEmitter 7 | - ExecutorEventEmitter 8 | - ExecutorEventEmitterCtx 9 | - FastAPIEventEmitter 10 | - RedisEventEmitter 11 | -------------------------------------------------------------------------------- /docs/javascripts/announcement.js: -------------------------------------------------------------------------------- 1 | const texts = document.querySelectorAll(".announcement-subtitle"); 2 | let currentIndex = 0; 3 | 4 | if (texts.length > 1) { 5 | setInterval(() => { 6 | texts[currentIndex].classList.remove("active"); // Hide current text 7 | currentIndex = (currentIndex + 1) % texts.length; // Move to the next text 8 | texts[currentIndex].classList.add("active"); // Show next text 9 | }, 5000); // Change text every 5 seconds 10 | } 11 | -------------------------------------------------------------------------------- /docs/javascripts/mathjax.js: -------------------------------------------------------------------------------- 1 | window.MathJax = { 2 | tex: { 3 | inlineMath: [["\\(", "\\)"]], 4 | displayMath: [["\\[", "\\]"]], 5 | processEscapes: true, 6 | processEnvironments: true 7 | }, 8 | options: { 9 | ignoreHtmlClass: ".*|", 10 | processHtmlClass: "arithmatex" 11 | } 12 | }; 13 | 14 | document$.subscribe(() => { 15 | MathJax.startup.output.clearCache() 16 | MathJax.typesetClear() 17 | MathJax.texReset() 18 | MathJax.typesetPromise() 19 | }) -------------------------------------------------------------------------------- /docs/stylesheets/search-bar.css: -------------------------------------------------------------------------------- 1 | .md-search__form { 2 | border-radius: 1rem; 3 | } 4 | 5 | .md-search__form { 6 | border-top-right-radius: 1rem !important; 7 | border-top-left-radius: 1rem !important; 8 | } 9 | 10 | .md-search__output { 11 | border-bottom-left-radius: 1rem !important; 12 | border-bottom-right-radius: 1rem !important; 13 | } 14 | 15 | @media screen and (max-width: 59.9844em) { 16 | .md-search__form { 17 | border-top-right-radius: inherit !important; 18 | border-top-left-radius: inherit !important; 19 | } 20 | .md-search__output { 21 | border-bottom-left-radius: inherit !important; 22 | border-bottom-right-radius: inherit !important; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pyventus/core/subscriptions/unsubscribable.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | 4 | class Unsubscribable(ABC): 5 | """An abstract base class representing an object that can be unsubscribed from a subscribed source.""" 6 | 7 | # Allow subclasses to define __slots__ 8 | __slots__ = () 9 | 10 | @abstractmethod 11 | def unsubscribe(self) -> bool: 12 | """ 13 | Release or clean up any resources associated with the subscribed source. 14 | 15 | :return: `True` if the unsubscribe operation was successful; `False` if it was already unsubscribed. 16 | :raises Exception: Any exception raised during the unsubscription process. 17 | """ 18 | pass 19 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you think you have identified a security issue in Pyventus, please **do not open a public issue or discussion**. Instead, follow these steps to securely report the vulnerability: 4 | 5 | 1. Navigate to the `Security` tab of the repository. 6 | 2. Click on `Report a vulnerability` to submit a new report. 7 | 3. Provide a detailed description of the vulnerability, including steps to reproduce if possible. 8 | 4. Include any additional information that would be helpful in understanding and addressing the issue. 9 | 10 | By following this process, you will make sure that these types of issues are handled discreetly and efficiently, keeping both Pyventus and its community secure. 11 | 12 | Thanks for your help! 13 | -------------------------------------------------------------------------------- /src/pyventus/reactive/__init__.py: -------------------------------------------------------------------------------- 1 | """The reactive programming module of Pyventus.""" 2 | 3 | from .observables import ( 4 | Completed, 5 | Observable, 6 | ObservableTask, 7 | ObservableTaskCallbackReturnType, 8 | ObservableTaskCallbackType, 9 | as_observable_task, 10 | ) 11 | from .observers import Observer 12 | from .subscribers import ( 13 | CompleteCallbackType, 14 | ErrorCallbackType, 15 | NextCallbackType, 16 | Subscriber, 17 | ) 18 | 19 | __all__ = [ 20 | "Completed", 21 | "Observable", 22 | "ObservableTask", 23 | "ObservableTaskCallbackReturnType", 24 | "ObservableTaskCallbackType", 25 | "as_observable_task", 26 | "Observer", 27 | "CompleteCallbackType", 28 | "ErrorCallbackType", 29 | "NextCallbackType", 30 | "Subscriber", 31 | ] 32 | -------------------------------------------------------------------------------- /src/pyventus/core/exceptions/pyventus_exception.py: -------------------------------------------------------------------------------- 1 | class PyventusException(Exception): 2 | """ 3 | A custom exception class for the Pyventus package. 4 | 5 | **Notes:** 6 | 7 | - This class provides a robust mechanism for handling and identifying potential 8 | exceptions within the Pyventus package. 9 | 10 | - This class inherits from the base `Exception` class in Python, allowing it to be 11 | raised as needed. 12 | """ 13 | 14 | def __init__(self, errors: str | list[str] | None = None): 15 | """ 16 | Initialize an instance of `PyventusException`. 17 | 18 | :param errors: The error messages associated with the exception. Defaults to `None`. 19 | """ 20 | self.errors: str | list[str] = errors if errors else self.__class__.__name__ 21 | super().__init__(errors) 22 | -------------------------------------------------------------------------------- /docs/api/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | - feedback 5 | --- 6 | 7 | # API Reference 8 | 9 |
10 | Welcome to the Pyventus API Reference, a comprehensive guide that provides detailed information about the classes, functions, parameters, attributes, and other components available in Pyventus. 11 |
12 | 13 |14 | In the API Reference, you will find detailed documentation for each component, including clear explanations, parameter details, and return values. You can navigate through the reference using the search functionality or by browsing the different sections and categories to find the specific information you need. 15 |
16 | 17 |21 | *Let's explore the Pyventus API Reference!* 22 |
23 | -------------------------------------------------------------------------------- /docs/learn/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - toc 4 | - feedback 5 | --- 6 | 7 | # Learn 8 | 9 |10 | Welcome to the Learning Zone of Pyventus! In this section, you will find a variety of educational resources that will not only help you grasp the key concepts and core abstractions of the library, but also show you how to effectively apply them within your projects. 11 |
12 | 13 |14 | Whether you are new to Pyventus or looking to deepen your knowledge, this section is designed to support your learning process across all levels of experience. You will find tutorials, examples, and introductory content that will help you get started and make the most of Pyventus. 15 |
16 | 17 |21 | Let's kickstart your Pyventus experience! 22 |
23 | -------------------------------------------------------------------------------- /docs/stylesheets/announcement.css: -------------------------------------------------------------------------------- 1 | .announcement-subtitle-container { 2 | display: grid; 3 | justify-items: start; 4 | align-items: start; 5 | } 6 | 7 | .announcement-subtitle-container > * { 8 | grid-column-start: 1; 9 | grid-row-start: 1; 10 | } 11 | 12 | .announcement-subtitle { 13 | opacity: 0; /* Start hidden */ 14 | transition: opacity 1s ease; /* Smooth transition for opacity */ 15 | pointer-events: none; /* Allow clicks to pass through */ 16 | font-style: italic; 17 | font-weight: bold; 18 | font-size: 80%; 19 | } 20 | 21 | .announcement-subtitle.active { 22 | opacity: 0.75; /* Fully visible when active */ 23 | pointer-events: auto; /* Allow clicks on active text */ 24 | } 25 | 26 | @keyframes announcementSubtitleAnimation { 27 | 0% { 28 | opacity: 0; /* Start hidden */ 29 | } 30 | 50% { 31 | opacity: 0.75; /* Fully visible */ 32 | } 33 | 100% { 34 | opacity: 0; /* Fade out */ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/utils/object_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | def get_private_attr(obj: object, name: str) -> Any: 5 | """ 6 | Retrieve a private attribute from an object. 7 | 8 | :param obj: The object from which to retrieve the attribute. 9 | :param name: The name of the attribute to retrieve. 10 | :return: The value of the requested attribute. 11 | """ 12 | # Access the private attribute using name mangling 13 | return getattr(obj, f"_{type(obj).__name__}{name}") 14 | 15 | 16 | def has_private_attr(obj: object, name: str) -> bool: 17 | """ 18 | Determine if an object has a private attribute. 19 | 20 | :param obj: The object to check for the private attribute. 21 | :param name: The name of the private attribute to check for. 22 | :return: `True` if the private attribute exists, `False` otherwise. 23 | """ 24 | # Check if the object has the private attribute using name mangling 25 | return hasattr(obj, f"_{type(obj).__name__}{name}") 26 | -------------------------------------------------------------------------------- /src/pyventus/events/__init__.py: -------------------------------------------------------------------------------- 1 | """The event-driven programming module of Pyventus.""" 2 | 3 | from .emitters import ( 4 | AsyncIOEventEmitter, 5 | CeleryEventEmitter, 6 | EmittableEventType, 7 | EventEmitter, 8 | ExecutorEventEmitter, 9 | ExecutorEventEmitterCtx, 10 | FastAPIEventEmitter, 11 | RedisEventEmitter, 12 | ) 13 | from .handlers import EventHandler 14 | from .linkers import EventLinker, SubscribableEventType 15 | from .subscribers import EventCallbackType, EventSubscriber, FailureCallbackType, SuccessCallbackType 16 | 17 | __all__ = [ 18 | "AsyncIOEventEmitter", 19 | "CeleryEventEmitter", 20 | "EmittableEventType", 21 | "EventEmitter", 22 | "ExecutorEventEmitter", 23 | "ExecutorEventEmitterCtx", 24 | "FastAPIEventEmitter", 25 | "RedisEventEmitter", 26 | "EventHandler", 27 | "EventLinker", 28 | "SubscribableEventType", 29 | "EventCallbackType", 30 | "EventSubscriber", 31 | "FailureCallbackType", 32 | "SuccessCallbackType", 33 | ] 34 | -------------------------------------------------------------------------------- /tests/pyventus/core/utils/test_repr_utils.py: -------------------------------------------------------------------------------- 1 | from pyventus.core.utils import attributes_repr, formatted_repr 2 | 3 | 4 | class TestReprUtils: 5 | # ================================= 6 | # Test Cases for formatted_repr 7 | # ================================= 8 | 9 | def test_formatted_repr(self) -> None: 10 | # Arrange 11 | class Inner: ... 12 | 13 | inner = Inner() 14 | expected: str = f"<{Inner.__name__} at 0x{id(inner):016X}>" 15 | 16 | # Act 17 | res = formatted_repr(inner) 18 | 19 | # Assert 20 | assert res == expected 21 | 22 | # ================================= 23 | # Test Cases for attributes_repr 24 | # ================================= 25 | 26 | def test_attributes_repr(self) -> None: 27 | # Arrange 28 | expected: str = "attr1='val1', attr2=b'val2', attr3=None" 29 | 30 | # Act 31 | res = attributes_repr(attr1="val1", attr2=b"val2", attr3=None) 32 | 33 | # Assert 34 | assert res == expected 35 | -------------------------------------------------------------------------------- /docs/.overrides/announce.html: -------------------------------------------------------------------------------- 1 | 🚀 Pyventus v0.7 is now live! — 2 | Don't miss the latest updates, 3 | featuring reactive programming, 4 | major optimizations, and more! 5 |9 | Event-driven programming is a paradigm in which the flow of a program is determined by events. These events can be generated by both internal and external actions, such as user interactions (mouse clicks, keyboard inputs, etc), system-generated signals (timers, network messages, etc), or other internal processes. 10 |
11 | 12 |13 | In the context of Pyventus, event handling is based on the Publisher-Subscriber pattern[^1], which is an event-driven model where events can be published by one or more sources and consumed by one or more subscribers. This design enables different parts of an application to communicate effectively without needing to be aware of each other. 14 |
15 | 16 |17 | What makes Pyventus unique is its distinctive approach to the Publisher-Subscriber pattern. While it provides features similar to traditional event emitter libraries, it also leverages Python's unique characteristics. Pyventus allows you to organize your code around discrete events and their responses in a centralized manner across different contexts, as well as control their emission, propagation, and processing. 18 |
19 | 20 | [^1]: For further reading about the Publisher-Subscriber pattern, you can refer to the Publish–subscribe pattern on Wikipedia. 21 | -------------------------------------------------------------------------------- /src/pyventus/core/exceptions/pyventus_import_exception.py: -------------------------------------------------------------------------------- 1 | from .pyventus_exception import PyventusException 2 | 3 | 4 | class PyventusImportException(PyventusException): 5 | """ 6 | A custom Pyventus exception for handling missing imports within the package. 7 | 8 | **Notes:** 9 | 10 | - This class provides a robust mechanism for handling and identifying potential 11 | import exceptions within the Pyventus package. 12 | 13 | - This class inherits from the base `PyventusException` class, allowing it to be 14 | raised as needed. 15 | """ 16 | 17 | def __init__(self, import_name: str, *, is_optional: bool = False, is_dependency: bool = False) -> None: 18 | """ 19 | Initialize an instance of `PyventusImportException`. 20 | 21 | :param import_name: The name of the missing import. 22 | :param is_optional: A flag indicating whether the missing import is optional 23 | or required for the package to work. Defaults to `False` (required). 24 | :param is_dependency: A flag indicating whether the missing import is an 25 | external dependency or not. Defaults to `False` (local import). 26 | """ 27 | # Store the import name and properties. 28 | self.import_name: str = import_name 29 | self.is_optional: bool = is_optional 30 | self.is_dependency: bool = is_dependency 31 | 32 | # Initialize the base PyventusException class with the error message. 33 | super().__init__( 34 | f"Missing {'optional ' if is_optional else ''}{'dependency' if is_dependency else 'import'}: {import_name}", 35 | ) 36 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | release: 8 | types: 9 | - created 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: write 14 | 15 | jobs: 16 | deploy: 17 | runs-on: ubuntu-latest 18 | env: 19 | GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }} 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v5 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Configure Git Credentials 27 | run: | 28 | git config user.name github-actions[bot] 29 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 30 | - uses: actions/setup-python@v6 31 | with: 32 | python-version: "3.11" 33 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 34 | - uses: actions/cache@v4 35 | with: 36 | key: mkdocs-material-${{ env.cache_id }} 37 | path: .cache 38 | restore-keys: | 39 | mkdocs-material- 40 | - name: Install dependencies 41 | run: pip install .[docs] 42 | - name: Deploy dev documentation 43 | if: github.ref == 'refs/heads/master' 44 | run: mike deploy --push dev 45 | - name: Get Pyventus Docs Version 46 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' 47 | run: | 48 | pyventus_docs_version=$(python -c "import pyventus; print('.'.join(map(str, pyventus.__version__.split('.')[:2])))") 49 | echo "PYVENTUS_DOCS_VERSION=$pyventus_docs_version" >> $GITHUB_ENV 50 | - name: Deploy release documentation 51 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' 52 | run: mike deploy --push --update-aliases $PYVENTUS_DOCS_VERSION latest 53 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: 9 | - opened 10 | - synchronize 11 | 12 | jobs: 13 | test: 14 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ ubuntu-latest, windows-latest, macos-latest ] 20 | python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v5 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Ensure latest pip 31 | run: python -m pip install --upgrade pip 32 | 33 | - name: Install dependencies 34 | run: pip install hatch 35 | 36 | - name: Run all tests 37 | run: hatch run +py=${{ matrix.python-version }} tests:all 38 | 39 | - name: Coveralls Parallel 40 | uses: coverallsapp/github-action@v2 41 | with: 42 | files: coverage.xml 43 | flag-name: py${{ matrix.python-version }}-${{ matrix.os }} 44 | parallel: true 45 | 46 | coveralls-finish: 47 | needs: 48 | - test 49 | 50 | if: ${{ always() }} 51 | runs-on: ubuntu-latest 52 | 53 | steps: 54 | - name: Coveralls Finished 55 | uses: coverallsapp/github-action@v2 56 | with: 57 | parallel-finished: true 58 | 59 | # https://github.com/marketplace/actions/alls-green#why 60 | alls-green: # This job does nothing and is only used for the branch protection 61 | if: always() 62 | needs: 63 | - coveralls-finish 64 | runs-on: ubuntu-latest 65 | steps: 66 | - name: Decide whether the needed jobs succeeded or failed 67 | uses: re-actors/alls-green@release/v1 68 | with: 69 | jobs: ${{ toJSON(needs) }} 70 | -------------------------------------------------------------------------------- /tests/pyventus/reactive/observables/test_observable_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pyventus.reactive import ObservableTask, as_observable_task 3 | from typing_extensions import Any 4 | 5 | from ....fixtures import CallableMock 6 | from ....utils import get_private_attr 7 | 8 | 9 | class TestObservableUtils: 10 | # ================================= 11 | # Test Cases for as_observable_task() 12 | # ================================= 13 | 14 | @pytest.mark.parametrize( 15 | ["callback", "args", "kwargs"], 16 | [ 17 | (CallableMock.Sync(), (), {}), 18 | (CallableMock.SyncGenerator(), ("pi", 3.14), {}), 19 | (CallableMock.Async(), (), {"...": ...}), 20 | (CallableMock.AsyncGenerator(), ("pi", 3.14), {"...": ...}), 21 | ], 22 | ) 23 | def test_as_observable_task_decorator( 24 | self, callback: CallableMock.Base, args: tuple[Any, ...], kwargs: dict[str, Any] 25 | ) -> None: 26 | # Arrange 27 | decorated_callback = as_observable_task(callback) 28 | 29 | # Act 30 | observable_task: ObservableTask[Any] = decorated_callback(*args, **kwargs) 31 | observable_task_callback = get_private_attr(observable_task, "__callback") 32 | 33 | # Assert 34 | assert callable(decorated_callback) 35 | assert isinstance(observable_task, ObservableTask) 36 | assert get_private_attr(observable_task_callback, "__callable") is callback 37 | assert get_private_attr(observable_task, "__args") == args 38 | assert get_private_attr(observable_task, "__kwargs") == kwargs 39 | 40 | # ================================= 41 | 42 | @pytest.mark.parametrize( 43 | ["debug"], 44 | [(False,), (True,)], 45 | ) 46 | def test_as_observable_task_decorator_with_parameters(self, debug: bool) -> None: 47 | # Arrange 48 | callback = CallableMock.Sync() 49 | decorated_callback = as_observable_task(debug=debug)(callback) 50 | 51 | # Act 52 | observable_task = decorated_callback() 53 | 54 | # Assert 55 | assert isinstance(observable_task, ObservableTask) 56 | assert getattr(observable_task, "_Observable__logger").debug_enabled is debug # noqa: B009 57 | -------------------------------------------------------------------------------- /tests/pyventus/core/processing/asyncio/test_asyncio_processing_service.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from pyventus.core.processing.asyncio import AsyncIOProcessingService 4 | from typing_extensions import override 5 | 6 | from .....fixtures import CallableMock 7 | from ..processing_service_test import ProcessingServiceTest 8 | 9 | 10 | class TestAsyncIOProcessingService(ProcessingServiceTest): 11 | # ================================= 12 | # Test Cases for creation 13 | # ================================= 14 | 15 | def test_creation(self) -> None: 16 | # Arrange/Act 17 | processing_service = AsyncIOProcessingService() 18 | 19 | # Assert 20 | assert processing_service is not None 21 | assert isinstance(processing_service, AsyncIOProcessingService) 22 | 23 | # ================================= 24 | # Test Cases for is_loop_running 25 | # ================================= 26 | 27 | def test_is_loop_running_in_sync_context(self) -> None: 28 | # Arrange/Act/Assert 29 | loop_running: bool = AsyncIOProcessingService.is_loop_running() 30 | assert not loop_running 31 | 32 | # ================================= 33 | 34 | async def test_is_loop_running_in_async_context(self) -> None: 35 | # Arrange/Act/Assert 36 | loop_running: bool = AsyncIOProcessingService.is_loop_running() 37 | assert loop_running 38 | 39 | # ================================= 40 | # Test Cases for submission 41 | # ================================= 42 | 43 | @override 44 | def handle_submission_in_sync_context( 45 | self, callback: CallableMock.Base, args: tuple[Any, ...], kwargs: dict[str, Any] 46 | ) -> None: 47 | # Arrange 48 | processing_service = AsyncIOProcessingService() 49 | 50 | # Act 51 | processing_service.submit(callback, *args, **kwargs) 52 | 53 | # ================================= 54 | 55 | @override 56 | async def handle_submission_in_async_context( 57 | self, callback: CallableMock.Base, args: tuple[Any, ...], kwargs: dict[str, Any] 58 | ) -> None: 59 | # Arrange 60 | processing_service = AsyncIOProcessingService() 61 | 62 | # Act 63 | processing_service.submit(callback, *args, **kwargs) 64 | await processing_service.wait_for_tasks() 65 | -------------------------------------------------------------------------------- /docs/.overrides/404.html: -------------------------------------------------------------------------------- 1 | {% extends "main.html" %} 2 | 3 | 4 | {% block content %} 5 | 18 | 19 | 30 | 31 |34 | The requested URL was not found in this documentation. 35 |
36 | 37 |38 | Try searching or go to Pyventus' home page. 39 |
40 | 41 | 47 | 48 | {% endblock %} -------------------------------------------------------------------------------- /src/pyventus/core/utils/repr_utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | 4 | def formatted_repr(instance: object, info: str | None = None) -> str: 5 | """ 6 | Generate a formatted string representation of an object instance. 7 | 8 | This function provides a consistent format for representing an object, including 9 | its class name, memory address, and any additional context. It is designed to help 10 | standardize the output of `__repr__` methods across different classes, making it 11 | easier to identify and debug object instances. 12 | 13 | :param instance: The object to be represented. It should be an instance of any class. 14 | :param info: Optional string providing extra information about the object. Defaults to `None`. 15 | :return: A formatted string representation of the object, structured as: 16 | "<`ClassName` at `MemoryAddress` with `Info`>". 17 | """ 18 | return f"<{instance.__class__.__name__} at {hex_id_repr(instance)}{f' with {info}' if info else ''}>" 19 | 20 | 21 | def attributes_repr(**kwargs: Any) -> str: 22 | """ 23 | Create a formatted string representation of specified attributes. 24 | 25 | :param kwargs: Keyword arguments representing attribute names and their corresponding values. 26 | :return: A string formatted as "key1=value1, key2=value2, ..." that provides a concise 27 | overview of the attributes. 28 | """ 29 | return ", ".join(f"{key}={value!r}" for key, value in kwargs.items()) 30 | 31 | 32 | def summarized_repr(instance: object) -> str: 33 | """ 34 | Generate a summary representation of an object instance. 35 | 36 | This function returns a formatted string that includes the name of the class 37 | of the given instance and its unique identifier in the format `ClassName4 | In Pyventus, you can easily integrate Event Emitters with the AsyncIO framework through the AsyncIO Processing Service. Simply create an instance of the AsyncIO Processing Service and pass it as the event processor when setting up the Event Emitter, or you can use the factory method called AsyncIO Event Emitter to handle the setup in a single step. 5 |
6 | 7 | === ":material-console: Manual Configuration" 8 | 9 | ```Python linenums="1" hl_lines="1 2 4" 10 | from pyventus.events import EventEmitter 11 | from pyventus.core.processing.asyncio import AsyncIOProcessingService 12 | 13 | event_emitter = EventEmitter(event_processor=AsyncIOProcessingService()) 14 | event_emitter.emit("MyEvent") 15 | ``` 16 | 17 | === ":material-factory: Factory Method" 18 | 19 | ```Python linenums="1" hl_lines="1 4" 20 | from pyventus.events import AsyncIOEventEmitter, EventEmitter 21 | 22 | 23 | event_emitter: EventEmitter = AsyncIOEventEmitter() 24 | event_emitter.emit("MyEvent") 25 | ``` 26 | 27 |28 | By utilizing the AsyncIO Processing Service, the execution of each event emission will be handled by the AsyncIO event loop. 29 |
30 | 31 | ## AsyncIO Behavior 32 | 33 |34 | It is important to note that the AsyncIO Processing Service handles the execution of each event emission differently depending on whether an AsyncIO loop is already running (async context) or not (sync context). If there isn’t an active loop, it uses the `asyncio.run()` method to execute the event emission, creating a new loop, waiting for the event emission to finish, and finally closing it. If a loop is already running, the service simply schedules the event emission as a background task using the `asyncio.create_task()`. 35 |
36 | 37 | !!! tip "Event Emission is Silently Discarded" 38 | 39 |40 | When working with async contexts, it is important to properly handle the underlying AsyncIO loop, as the AsyncIO Processing Service simply schedules tasks to it. If the AsyncIO loop closes before all submitted callbacks are complete, they will be discarded. 41 |
42 | 43 | ## Practical Example 44 | 45 | === "`Sync` contexts" 46 | 47 | ```Python linenums="1" hl_lines="3 12 18 19" 48 | import asyncio 49 | 50 | from pyventus.core.processing.asyncio import AsyncIOProcessingService 51 | from pyventus.events import EventEmitter, EventLinker 52 | 53 | 54 | async def handle_event_with_delay(): 55 | await asyncio.sleep(1.5) 56 | print("Done!") 57 | 58 | 59 | def main(): 60 | print("Starting...") 61 | 62 | EventLinker.subscribe("MyEvent", event_callback=handle_event_with_delay) 63 | EventLinker.subscribe("MyEvent", event_callback=handle_event_with_delay) 64 | 65 | event_processor = AsyncIOProcessingService() 66 | event_emitter = EventEmitter(event_processor=event_processor) 67 | 68 | event_emitter.emit("MyEvent") 69 | event_emitter.emit("MyEvent") 70 | 71 | print("Closing...") 72 | 73 | 74 | 75 | main() 76 | ``` 77 | 78 | === "`Async` contexts" 79 | 80 | ```Python linenums="1" hl_lines="3 12 18 19 25" 81 | import asyncio 82 | 83 | from pyventus.core.processing.asyncio import AsyncIOProcessingService 84 | from pyventus.events import EventEmitter, EventLinker 85 | 86 | 87 | async def handle_event_with_delay(): 88 | await asyncio.sleep(1.5) 89 | print("Done!") 90 | 91 | 92 | async def main(): 93 | print("Starting...") 94 | 95 | EventLinker.subscribe("MyEvent", event_callback=handle_event_with_delay) 96 | EventLinker.subscribe("MyEvent", event_callback=handle_event_with_delay) 97 | 98 | event_processor = AsyncIOProcessingService() 99 | event_emitter = EventEmitter(event_processor=event_processor) 100 | 101 | event_emitter.emit("MyEvent") 102 | event_emitter.emit("MyEvent") 103 | 104 | print("Closing...") 105 | await event_processor.wait_for_tasks() 106 | 107 | 108 | asyncio.run(main()) 109 | ``` 110 | -------------------------------------------------------------------------------- /src/pyventus/core/loggers/logger.py: -------------------------------------------------------------------------------- 1 | from ..utils import attributes_repr, formatted_repr, summarized_repr 2 | from .stdout_logger import ExcInfoType, StdOutLogger 3 | 4 | 5 | class Logger: 6 | """A custom logger class that wraps the `StdOutLogger` and provides additional functionality.""" 7 | 8 | # Attributes for the Logger 9 | __slots__ = ("__source", "__debug") 10 | 11 | def __init__(self, source: str | type | object | None = None, debug: bool = False): 12 | """ 13 | Initialize an instance of `Logger`. 14 | 15 | :param source: The source of the log message, which can be a string, type, or object. Defaults to None. 16 | :param debug: A flag indicating whether debug mode is enabled. 17 | """ 18 | # Variable to hold the name of the source. 19 | source_name: str 20 | 21 | # Determine the name of the source based on its type. 22 | if source is None: 23 | source_name = summarized_repr(self) 24 | elif isinstance(source, str): 25 | source_name = source 26 | elif isinstance(source, type): 27 | source_name = f"{source.__name__}(ClassReference)" 28 | else: 29 | source_name = summarized_repr(source) 30 | 31 | # Store the determined source name and debug flag. 32 | self.__source: str = source_name 33 | self.__debug = debug 34 | 35 | def __repr__(self) -> str: 36 | """ 37 | Retrieve a string representation of the instance. 38 | 39 | :return: A string representation of the instance. 40 | """ 41 | return formatted_repr( 42 | instance=self, 43 | info=attributes_repr( 44 | source=self.__source, 45 | debug=self.__debug, 46 | ), 47 | ) 48 | 49 | @property 50 | def debug_enabled(self) -> bool: 51 | """ 52 | Return a boolean value indicating whether debug mode is enabled. 53 | 54 | :return: `True` if debug mode is enabled, `False` otherwise. 55 | """ 56 | return self.__debug 57 | 58 | def critical(self, msg: str, action: str | None = None, exc_info: ExcInfoType = None) -> None: 59 | """ 60 | Log a CRITICAL level message. 61 | 62 | :param msg: The message to be logged. 63 | :param action: The action or method associated with the log. Defaults to None. 64 | :param exc_info: Exception information to be logged. Defaults to None. 65 | :return: None. 66 | """ 67 | StdOutLogger.critical(msg=msg, source=self.__source, action=action, exc_info=exc_info) 68 | 69 | def error(self, msg: str, action: str | None = None, exc_info: ExcInfoType = None) -> None: 70 | """ 71 | Log an ERROR level message. 72 | 73 | :param msg: The message to be logged. 74 | :param action: The action or method associated with the log. Defaults to None. 75 | :param exc_info: Exception information to be logged. Defaults to None. 76 | :return: None. 77 | """ 78 | StdOutLogger.error(msg=msg, source=self.__source, action=action, exc_info=exc_info) 79 | 80 | def warning(self, msg: str, action: str | None = None) -> None: 81 | """ 82 | Log a WARNING level message. 83 | 84 | :param msg: The message to be logged. 85 | :param action: The action or method associated with the log. Defaults to None. 86 | :return: None. 87 | """ 88 | StdOutLogger.warning(msg=msg, source=self.__source, action=action) 89 | 90 | def info(self, msg: str, action: str | None = None) -> None: 91 | """ 92 | Log an INFO level message. 93 | 94 | :param msg: The message to be logged. 95 | :param action: The action or method associated with the log. Defaults to None. 96 | :return: None. 97 | """ 98 | StdOutLogger.info(msg=msg, source=self.__source, action=action) 99 | 100 | def debug(self, msg: str, action: str | None = None) -> None: 101 | """ 102 | Log a DEBUG level message. 103 | 104 | :param msg: The message to be logged. 105 | :param action: The action or method associated with the log. Defaults to None. 106 | :return: None. 107 | """ 108 | StdOutLogger.debug(msg=msg, source=self.__source, action=action) 109 | -------------------------------------------------------------------------------- /docs/learn/events/emitters/fastapi.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | # FastAPI Event Emitter 11 | 12 |13 | In Pyventus, you can easily integrate Event Emitters with the FastAPI framework through the FastAPI Processing Service. Simply create an instance of the FastAPI Processing Service and pass it as the event processor when setting up the Event Emitter, or you can use the factory method called FastAPI Event Emitter to handle the setup in a single step. 14 |
15 | 16 | === ":material-console: Manual Configuration" 17 | 18 | ```Python linenums="1" hl_lines="1-3 5 9 10" 19 | from fastapi import BackgroundTasks, FastAPI 20 | from pyventus.core.processing.fastapi import FastAPIProcessingService 21 | from pyventus.events import EventEmitter 22 | 23 | app = FastAPI() 24 | 25 | 26 | @app.get("/") 27 | def read_root(background_tasks: BackgroundTasks): 28 | event_emitter = EventEmitter(event_processor=FastAPIProcessingService(background_tasks)) 29 | event_emitter.emit("MyEvent") 30 | return {"Event": "Emitted"} 31 | ``` 32 | 33 | === ":material-factory: Factory Method" 34 | 35 | ```Python linenums="1" hl_lines="1-2 4 8" 36 | from fastapi import Depends, FastAPI 37 | from pyventus.events import EventEmitter, FastAPIEventEmitter 38 | 39 | app = FastAPI() 40 | 41 | 42 | @app.get("/") 43 | def read_root(event_emitter: EventEmitter = Depends(FastAPIEventEmitter())): 44 | event_emitter.emit("MyEvent") 45 | return {"Event": "Emitted"} 46 | ``` 47 | 48 |49 | By utilizing the FastAPI Processing Service, the execution of each event emission will be handled by the FastAPI's Background Tasks system. 50 |
51 | 52 | ## Practical Example 53 | 54 |55 | To start using the Event Emitter with FastAPI, follow these steps: 56 |
57 | 58 | 1.Install Dependencies: 59 | Before proceeding, ensure that you have installed the optional [FastAPI dependency](../../../getting-started.md/#optional-dependencies). 60 |
61 | 62 | 2.Subscribe and Emit: 63 | Once you have everything installed and configured, using Pyventus with FastAPI is straightforward. Simply define your subscribers and events, instantiate an Event Emitter configured for FastAPI, and emit your events. 64 |
65 | 66 | ```Python title="main.py" linenums="1" hl_lines="3-4 7-8 14 20 22" 67 | import time 68 | 69 | from fastapi import Depends, FastAPI 70 | from pyventus.events import EventEmitter, EventLinker, FastAPIEventEmitter 71 | 72 | 73 | @EventLinker.on("SendEmail") 74 | def handle_email_notification(email: str) -> None: 75 | print(f"Sending email to: {email}") 76 | time.sleep(2.5) # Simulate sending delay. 77 | print("Email sent successfully!") 78 | 79 | 80 | app = FastAPI() 81 | 82 | 83 | @app.get("/email") 84 | def send_email( 85 | email_to: str, 86 | event_emitter: EventEmitter = Depends(FastAPIEventEmitter()), 87 | ) -> None: 88 | event_emitter.emit("SendEmail", email_to) 89 | return {"message": "Email sent!"} 90 | ``` 91 | 92 | To start the FastAPI server, run the following command: 93 | 94 |4 | In Pyventus, you can easily integrate Event Emitters with threads or processes through the Executor Processing Service. Simply create an instance of the Executor Processing Service and pass it as the event processor when setting up the Event Emitter, or you can use the factory method called Executor Event Emitter to handle the setup in a single step. 5 |
6 | 7 | === ":material-console: Manual Configuration" 8 | 9 | ```Python linenums="1" hl_lines="1 3 8 10" 10 | from concurrent.futures import ThreadPoolExecutor 11 | 12 | from pyventus.core.processing.executor import ExecutorProcessingService 13 | from pyventus.events import EventEmitter 14 | 15 | 16 | if __name__ == "__main__": 17 | executor = ThreadPoolExecutor() 18 | 19 | event_emitter = EventEmitter(event_processor=ExecutorProcessingService(executor)) 20 | event_emitter.emit("MyEvent") 21 | 22 | executor.shutdown() 23 | ``` 24 | 25 | === ":material-factory: Factory Method" 26 | 27 | ```Python linenums="1" hl_lines="1 3 8 10" 28 | from concurrent.futures import ThreadPoolExecutor 29 | 30 | from pyventus.events import ExecutorEventEmitter 31 | 32 | 33 | 34 | if __name__ == "__main__": 35 | executor = ThreadPoolExecutor() 36 | 37 | event_emitter = ExecutorEventEmitter(executor) 38 | event_emitter.emit("MyEvent") 39 | 40 | executor.shutdown() 41 | ``` 42 | 43 | === ":material-code-block-tags: Context Manager" 44 | 45 | ```Python linenums="1" hl_lines="1 3 7" 46 | from concurrent.futures import ThreadPoolExecutor 47 | 48 | from pyventus.events import ExecutorEventEmitterCtx 49 | 50 | 51 | if __name__ == "__main__": 52 | with ExecutorEventEmitterCtx(executor=ThreadPoolExecutor()) as event_emitter: 53 | event_emitter.emit("MyEvent") 54 | ``` 55 | 56 |57 | By utilizing the Executor Processing Service, the execution of each event emission will be handled by the given Thread/Process executor. 58 |
59 | 60 | ## Executor Management 61 | 62 |63 | It is important to properly manage the underlying Executor when using the Executor Processing Service. Once you've finished emitting events, call the `shutdown()` method to signal the executor to free any resources associated with pending futures, or use the `with` statement, which will automatically shut down the Executor. 64 |
65 | 66 | ## Practical Example 67 | 68 | === "Using `ThreadPoolExecutor`" 69 | 70 | ```Python linenums="1" hl_lines="2 4 18-21" 71 | import time 72 | from concurrent.futures import ThreadPoolExecutor 73 | 74 | from pyventus.events import EventLinker, ExecutorEventEmitter 75 | 76 | 77 | def handle_event_with_delay(): 78 | time.sleep(1.5) 79 | print("Done!") 80 | 81 | 82 | if __name__ == "__main__": 83 | print("Starting...") 84 | 85 | EventLinker.subscribe("MyEvent", event_callback=handle_event_with_delay) 86 | EventLinker.subscribe("MyEvent", event_callback=handle_event_with_delay) 87 | 88 | with ThreadPoolExecutor() as executor: 89 | event_emitter = ExecutorEventEmitter(executor) 90 | event_emitter.emit("MyEvent") 91 | event_emitter.emit("MyEvent") 92 | 93 | print("Closing...") 94 | ``` 95 | 96 | === "Using `ProcessPoolExecutor`" 97 | 98 | ```Python linenums="1" hl_lines="1 3 19-22" 99 | from concurrent.futures import ProcessPoolExecutor 100 | 101 | from pyventus.events import EventLinker, ExecutorEventEmitter 102 | 103 | def fibonacci(n): 104 | if n <= 1: 105 | return n 106 | return fibonacci(n - 1) + fibonacci(n - 2) 107 | 108 | if __name__ == "__main__": 109 | print("Starting...") 110 | 111 | EventLinker.subscribe( 112 | "Fibonacci", 113 | event_callback=fibonacci, 114 | success_callback=print, 115 | ) 116 | 117 | with ProcessPoolExecutor() as executor: 118 | event_emitter = ExecutorEventEmitter(executor) 119 | event_emitter.emit("Fibonacci", 35) 120 | event_emitter.emit("Fibonacci", 35) 121 | 122 | print("Closing...") 123 | ``` 124 | 125 | !!! warning "Picklable Objects Required for `ProcessPoolExecutor`" 126 | 127 |128 | When working with the `ProcessPoolExecutor`, it is essential to ensure that all objects involved in the event emission are picklable. 129 |
130 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 | 14 | 15 |16 | Welcome to the Getting Started section! In this guide, you will learn how to install Pyventus, as well as enable any of its optional dependencies. For more detailed information on how to use this library, you can refer to the [API](api/index.md) and [Learn](learn/index.md) sections. 17 |
18 | 19 | ## Installation 20 | 21 |
22 | Pyventus is published as a Python package and can be installed using pip, ideally in a virtual environment for proper dependency isolation. To get started, open up a terminal and install Pyventus with the following command:
23 |
32 | By default, Pyventus relies on the Python standard library and requires Python 3.10 or higher with no additional dependencies aside from typing-extensions, which is primarily used to support advanced typing features in older versions of Python.
33 |
38 | While Pyventus primarily relies on the Python standard library, it also supports optional dependencies to access additional features, such as different processing services[^1]. Below is a list of supported integrations: 39 |
40 | 41 | ### Supported Library Integrations 42 | 43 | -Celery ─ 44 | Pyventus integrates with Celery through the `CeleryProcessingService`, which is a concrete implementation of the `ProcessingService` interface that leverages the Celery framework to handle the execution of calls. To install Pyventus with Celery support, use the following command: 45 |
46 | 47 |55 | This package also includes optional dependencies. For more information, please visit the Celery documentation. 56 |
57 | 58 | *** 59 | 60 | -Redis Queue (RQ) ─ 61 | Pyventus integrates with Redis Queue through the `RedisProcessingService`, which is a concrete implementation of the `ProcessingService` interface that leverages the Redis Queue framework to handle the execution of calls. To install Pyventus with Redis Queue support, use the following command: 62 |
63 | 64 |FastAPI ─ 73 | Pyventus integrates with FastAPI through the `FastAPIProcessingService`, which is a concrete implementation of the `ProcessingService` interface that utilizes the FastAPI's `BackgroundTasks` to handle the execution of calls. To install Pyventus with FastAPI integration, use the following command: 74 |
75 | 76 |84 | This package also includes optional dependencies. For more information, please visit the FastAPI documentation. 85 |
86 | 87 | --- 88 | 89 | You can install all of these integrations simultaneously using: 90 | 91 |13 | In Pyventus, you can easily integrate Event Emitters with the Redis Queue framework through the Redis Processing Service. Simply create an instance of the Redis Processing Service and pass it as the event processor when setting up the Event Emitter, or you can use the factory method called Redis Event Emitter to handle the setup in a single step. 14 |
15 | 16 | === ":material-console: Manual Configuration" 17 | 18 | ```Python linenums="1" hl_lines="1-4 6-7 10" 19 | from pyventus.core.processing.redis import RedisProcessingService 20 | from pyventus.events import EventEmitter 21 | from redis import Redis 22 | from rq import Queue 23 | 24 | redis_conn = Redis.from_url("redis://default:redispw@localhost:6379") 25 | default_queue: Queue = Queue(name="default", connection=redis_conn) 26 | 27 | if __name__ == "__main__": 28 | event_emitter: EventEmitter = EventEmitter(event_processor=RedisProcessingService(queue=default_queue)) 29 | event_emitter.emit("MyEvent") 30 | ``` 31 | 32 | === ":material-factory: Factory Method" 33 | 34 | ```Python linenums="1" hl_lines="1-3 5-6 9" 35 | from pyventus.events import EventEmitter, RedisEventEmitter 36 | from redis import Redis 37 | from rq import Queue 38 | 39 | redis_conn = Redis.from_url("redis://default:redispw@localhost:6379") 40 | default_queue: Queue = Queue(name="default", connection=redis_conn) 41 | 42 | if __name__ == "__main__": 43 | event_emitter: EventEmitter = RedisEventEmitter(queue=default_queue) 44 | event_emitter.emit("MyEvent") 45 | ``` 46 | 47 |48 | By utilizing the Redis Processing Service, the execution of each event emission will be handled by a Redis Queue worker. 49 |
50 | 51 | ## Practical Example 52 | 53 |54 | To start using the Event Emitter with Redis Queue, follow these steps: 55 |
56 | 57 | 1.Install Dependencies: 58 | Before proceeding, ensure that you have installed the optional [Redis Queue dependency](../../../getting-started.md/#optional-dependencies). 59 |
60 | 61 | 2.Define Subscribers: 62 | If you're using Python's built-in functions, you can skip this step. If you're working with your own functions, you'll need to let Redis Queue know where they are defined. However, to avoid circular dependencies between modules, it's important to place these functions in a separate module from both your worker module and the event emitter. 63 |
64 | 65 | ```Python title="subscribers.py" linenums="1" 66 | from pyventus.events import EventLinker 67 | 68 | 69 | @EventLinker.on("MyEvent") 70 | def handle_my_event() -> None: 71 | print("Handling 'MyEvent'!") 72 | ``` 73 | 74 | 3.Create a Worker: 75 | Now that you’ve defined your subscribers, the next step is to create the script for the Redis Queue worker. This worker will listen to the Redis Queue pub/sub channel and process each event emission. For more information about Redis Queue workers, you can refer to the official documentation: [RQ Workers](https://python-rq.org/docs/workers/). 76 |
77 | 78 | === ":material-apple: macOS / :material-linux: Linux" 79 | 80 | ```Python title="worker.py" linenums="1" hl_lines="1 3-4 6 8-9 12 21-24 26-27" 81 | from multiprocessing import Process 82 | 83 | from redis import Redis 84 | from rq import Queue, SimpleWorker 85 | 86 | from .subscribers import handle_my_event # (1)! 87 | 88 | redis_conn = Redis.from_url("redis://default:redispw@localhost:6379") 89 | default_queue: Queue = Queue(name="default", connection=redis_conn) 90 | 91 | 92 | def worker_process() -> None: # (2)! 93 | worker = SimpleWorker(connection=redis_conn, queues=[default_queue]) 94 | worker.work() 95 | 96 | 97 | if __name__ == "__main__": 98 | num_workers = 1 # (3)! 99 | worker_processes: list[Process] = [] 100 | 101 | for _ in range(num_workers): # (4)! 102 | p = Process(target=worker_process) 103 | worker_processes.append(p) 104 | p.start() 105 | 106 | for process in worker_processes: 107 | process.join() # (5)! 108 | ``` 109 | 110 | 1. Import the `subscribers.py` module to let Redis Queue know about the available functions. 111 | 2. Creates a new Worker instance and starts the work loop. 112 | 3. Set the number of workers. For auto-assignment use: `multiprocessing.cpu_count()`. 113 | 4. Creates and starts new Processes for each worker. 114 | 5. Join every worker process. 115 | 116 | === ":fontawesome-brands-windows: Windows" 117 | 118 | ```Python title="worker.py" linenums="1" hl_lines="1 3-5 7 9-10 13-15 25-28 30-31" 119 | from multiprocessing import Process 120 | 121 | from redis import Redis 122 | from rq import Queue, SimpleWorker 123 | from rq.timeouts import TimerDeathPenalty 124 | 125 | from .subscribers import handle_my_event # (1)! 126 | 127 | redis_conn = Redis.from_url("redis://default:redispw@localhost:6379") 128 | default_queue: Queue = Queue(name="default", connection=redis_conn) 129 | 130 | 131 | def worker_process() -> None: # (2)! 132 | class WindowsSimpleWorker(SimpleWorker): # (3)! 133 | death_penalty_class = TimerDeathPenalty 134 | 135 | worker = WindowsSimpleWorker(connection=redis_conn, queues=[default_queue]) 136 | worker.work() 137 | 138 | 139 | if __name__ == "__main__": 140 | num_workers = 1 # (4)! 141 | worker_processes: list[Process] = [] 142 | 143 | for _ in range(num_workers): # (5)! 144 | p = Process(target=worker_process) 145 | worker_processes.append(p) 146 | p.start() 147 | 148 | for process in worker_processes: 149 | process.join() # (6)! 150 | ``` 151 | 152 | 1. Import the `subscribers.py` module to let Redis Queue know about the available functions. 153 | 2. Creates a new Worker instance and starts the work loop. 154 | 3. A class that inherits from `SimpleWorker` and is used to create a new worker instance in a Windows based system. 155 | 4. Set the number of workers. For auto-assignment use: `multiprocessing.cpu_count()`. 156 | 5. Creates and starts new Processes for each worker. 157 | 6. Join every worker process. 158 | 159 |160 | With the previous configuration in place, you can now launch the Redis Queue worker. 161 |
162 | 163 |Emitting events: 170 | Now that your workers are up and running, it’s time to start emitting events! Just create an Event Emitter configured with the Redis Processing Service, and you’re all set to emit an event. 171 |
172 | 173 | ```Python title="main.py" linenums="1" hl_lines="1 3 6" 174 | from pyventus.events import EventEmitter, RedisEventEmitter 175 | 176 | from .worker import default_queue 177 | 178 | if __name__ == "__main__": 179 | event_emitter: EventEmitter = RedisEventEmitter(queue=default_queue) 180 | event_emitter.emit("MyEvent") 181 | ``` 182 | -------------------------------------------------------------------------------- /tests/pyventus/reactive/subscribers/test_subscriber.py: -------------------------------------------------------------------------------- 1 | from pickle import dumps, loads 2 | from typing import Any 3 | 4 | import pytest 5 | from pyventus import PyventusException 6 | from pyventus.reactive import Subscriber 7 | 8 | from ....fixtures import CallableMock, DummyCallable 9 | from ....utils import has_private_attr 10 | 11 | 12 | class TestSubscriber: 13 | # ================================= 14 | # Test Cases for creation 15 | # ================================= 16 | 17 | @pytest.mark.parametrize( 18 | ["next_callback", "error_callback", "complete_callback"], 19 | [ 20 | (CallableMock.Sync(), None, None), 21 | (None, CallableMock.Async(), None), 22 | (None, None, CallableMock.Async()), 23 | (CallableMock.Async(), None, CallableMock.Sync()), 24 | (CallableMock.Async(), CallableMock.Sync(), None), 25 | (None, CallableMock.Sync(), CallableMock.Async()), 26 | ], 27 | ) 28 | def test_creation_with_valid_input( 29 | self, next_callback: CallableMock.Base, error_callback: CallableMock.Base, complete_callback: CallableMock.Base 30 | ) -> None: 31 | # Arrange/Act 32 | subscriber = Subscriber[Any]( 33 | teardown_callback=lambda sub: True, 34 | next_callback=next_callback, 35 | error_callback=error_callback, 36 | complete_callback=complete_callback, 37 | force_async=False, 38 | ) 39 | 40 | # Assert 41 | assert subscriber is not None 42 | assert isinstance(subscriber, Subscriber) 43 | assert subscriber.has_next_callback is bool(next_callback is not None) 44 | assert subscriber.has_error_callback is bool(error_callback is not None) 45 | assert subscriber.has_complete_callback is bool(complete_callback is not None) 46 | 47 | # ================================= 48 | 49 | @pytest.mark.parametrize( 50 | ["next_callback", "error_callback", "complete_callback", "exception"], 51 | [ 52 | (None, None, None, PyventusException), 53 | (DummyCallable.Sync.Generator.func, None, None, PyventusException), 54 | (None, DummyCallable.Async.Generator.func, None, PyventusException), 55 | (None, None, DummyCallable.Async.Generator.func, PyventusException), 56 | ], 57 | ) 58 | def test_creation_with_invalid_input( 59 | self, next_callback: Any, error_callback: Any, complete_callback: Any, exception: type[Exception] 60 | ) -> None: 61 | # Arrange/Act/Assert 62 | with pytest.raises(exception): 63 | Subscriber[Any]( 64 | teardown_callback=lambda sub: True, 65 | next_callback=next_callback, 66 | error_callback=error_callback, 67 | complete_callback=complete_callback, 68 | force_async=False, 69 | ) 70 | 71 | # ================================= 72 | # Test Cases for the Next method execution. 73 | # ================================= 74 | 75 | @pytest.mark.parametrize( 76 | ["next_callback"], 77 | [ 78 | (None,), 79 | (CallableMock.Sync(),), 80 | (CallableMock.Async(),), 81 | ], 82 | ) 83 | async def test_next_method_execution(self, next_callback: CallableMock.Base | None) -> None: 84 | # Arrange 85 | value: Any = object() 86 | error_callback: CallableMock.Base = CallableMock.Sync() 87 | complete_callback: CallableMock.Base = CallableMock.Async() 88 | subscriber = Subscriber[Any]( 89 | teardown_callback=lambda sub: True, 90 | next_callback=next_callback, 91 | error_callback=error_callback, 92 | complete_callback=complete_callback, 93 | force_async=False, 94 | ) 95 | 96 | # Act 97 | await subscriber.next(value) 98 | 99 | # Assert 100 | if next_callback: 101 | assert next_callback.call_count == 1 102 | assert next_callback.last_args == (value,) 103 | assert next_callback.last_kwargs == {} 104 | assert error_callback.call_count == 0 105 | assert complete_callback.call_count == 0 106 | 107 | # ================================= 108 | # Test Cases for the Error method execution. 109 | # ================================= 110 | 111 | @pytest.mark.parametrize( 112 | ["error_callback"], 113 | [ 114 | (None,), 115 | (CallableMock.Sync(),), 116 | (CallableMock.Async(),), 117 | ], 118 | ) 119 | async def test_error_method_execution(self, error_callback: CallableMock.Base | None) -> None: 120 | # Arrange 121 | exception: Exception = ValueError() 122 | next_callback: CallableMock.Base = CallableMock.Async() 123 | complete_callback: CallableMock.Base = CallableMock.Sync() 124 | subscriber = Subscriber[Any]( 125 | teardown_callback=lambda sub: True, 126 | next_callback=next_callback, 127 | error_callback=error_callback, 128 | complete_callback=complete_callback, 129 | force_async=False, 130 | ) 131 | 132 | # Act 133 | await subscriber.error(exception) 134 | 135 | # Assert 136 | if error_callback: 137 | assert error_callback.call_count == 1 138 | assert error_callback.last_args == (exception,) 139 | assert error_callback.last_kwargs == {} 140 | assert next_callback.call_count == 0 141 | assert complete_callback.call_count == 0 142 | 143 | # ================================= 144 | # Test Cases for the Complete method execution. 145 | # ================================= 146 | 147 | @pytest.mark.parametrize( 148 | ["complete_callback"], 149 | [ 150 | (None,), 151 | (CallableMock.Sync(),), 152 | (CallableMock.Async(),), 153 | ], 154 | ) 155 | async def test_complete_method_execution(self, complete_callback: CallableMock.Base | None) -> None: 156 | # Arrange 157 | next_callback: CallableMock.Base = CallableMock.Async() 158 | error_callback: CallableMock.Base = CallableMock.Sync() 159 | subscriber = Subscriber[Any]( 160 | teardown_callback=lambda sub: True, 161 | next_callback=next_callback, 162 | error_callback=error_callback, 163 | complete_callback=complete_callback, 164 | force_async=False, 165 | ) 166 | 167 | # Act 168 | await subscriber.complete() 169 | 170 | # Assert 171 | if complete_callback: 172 | assert complete_callback.call_count == 1 173 | assert complete_callback.last_args == () 174 | assert complete_callback.last_kwargs == {} 175 | assert next_callback.call_count == 0 176 | assert error_callback.call_count == 0 177 | 178 | # ================================= 179 | # Test Cases for Serialization/Deserialization 180 | # ================================= 181 | 182 | def test_pickle_serialization_and_deserialization(self) -> None: 183 | # Arrange 184 | subscriber = Subscriber( 185 | teardown_callback=CallableMock.Sync(return_value=False), 186 | next_callback=CallableMock.Sync(), 187 | error_callback=None, 188 | complete_callback=None, 189 | force_async=True, 190 | ) 191 | 192 | # Act 193 | data = dumps(subscriber) 194 | restored = loads(data) 195 | 196 | # Assert 197 | for attr in Subscriber.__slots__: 198 | assert ( 199 | hasattr(restored, attr) 200 | if not attr.startswith("__") or attr.endswith("__") 201 | else has_private_attr(restored, attr) 202 | ) 203 | -------------------------------------------------------------------------------- /docs/learn/events/emitters/index.md: -------------------------------------------------------------------------------- 1 | # Event Emitters 2 | 3 |4 | Now that your events and responses are all linked up, you may need a way to dispatch these events throughout your application and trigger their corresponding responses. For this purpose, Pyventus provides you with what is known as an Event Emitter. 5 |
6 | 7 |8 | The Event Emitter in Pyventus is essentially a base class that allows you to orchestrate the emission of events within your application in a modular and decoupled manner. 9 |
10 | 11 | ## Getting Started 12 | 13 |14 | In order to start working with the Event Emitter, the first thing you will need to do is to create a new instance of it. To do so, you will need what is known as an event processor, which is an instance of the Processing Service interface. This processor is crucial for the Event Emitter, as it is responsible for processing the emission of events. 15 |
16 | 17 | === ":material-console: Manual Creation" 18 | 19 | ```Python linenums="1" hl_lines="1 2 4" 20 | from pyventus.events import EventEmitter 21 | from pyventus.core.processing.asyncio import AsyncIOProcessingService # (1)! 22 | 23 | event_emitter = EventEmitter(event_processor=AsyncIOProcessingService()) 24 | event_emitter.emit("MyEvent") 25 | ``` 26 | 27 | 1. You can import and use any of the supported processing services, or even create your own custom ones. 28 | 29 | === ":material-factory: Factory Method" 30 | 31 | ```Python linenums="1" hl_lines="1 4" 32 | from pyventus.events import AsyncIOEventEmitter, EventEmitter 33 | 34 | 35 | event_emitter: EventEmitter = AsyncIOEventEmitter() # (1)! 36 | event_emitter.emit("MyEvent") 37 | ``` 38 | 39 | 1. There is a factory method for each of the supported processing services. For reference, you can check the [Event Emitter utilities](../../../api/events/emitters/event_emitter_utils.md). 40 | 41 | ### Using Custom Event Linkers 42 | 43 |44 | By default, the Event Emitter comes with the base Event Linker class configured as the primary access point for events and their responses. However, you have the flexibility to specify the Event Linker class that the Event Emitter will use. 45 |
46 | 47 | === ":material-console: Manual Creation" 48 | 49 | ```Python linenums="1" hl_lines="5 7 11" 50 | from pyventus.events import EventEmitter, EventLinker 51 | from pyventus.core.processing.asyncio import AsyncIOProcessingService 52 | 53 | 54 | class CustomEventLinker(EventLinker): ... 55 | 56 | CustomEventLinker.subscribe("MyEvent", event_callback=print) 57 | EventLinker.subscribe("MyEvent", event_callback=print) # (1)! 58 | 59 | event_emitter = EventEmitter( 60 | event_linker=CustomEventLinker, 61 | event_processor=AsyncIOProcessingService(), 62 | ) 63 | event_emitter.emit("MyEvent", "Hello, World!") 64 | ``` 65 | 66 | 1. This event subscriber will not be triggered, as it is subscribed to the Event Linker base class rather than the custom one that was created. 67 | 68 | === ":material-factory: Factory Method" 69 | 70 | ```Python linenums="1" hl_lines="4 7 12" 71 | from pyventus.events import AsyncIOEventEmitter, EventEmitter, EventLinker 72 | 73 | 74 | class CustomEventLinker(EventLinker): ... 75 | 76 | 77 | CustomEventLinker.subscribe("MyEvent", event_callback=print) 78 | EventLinker.subscribe("MyEvent", event_callback=print) # (1)! 79 | 80 | 81 | event_emitter: EventEmitter = AsyncIOEventEmitter( 82 | event_linker=CustomEventLinker, 83 | ) 84 | event_emitter.emit("MyEvent", "Hello, World!") 85 | ``` 86 | 87 | 1. This event subscriber will not be triggered, as it is subscribed to the Event Linker base class rather than the custom one that was created. 88 | 89 | ### Using Custom Event Processors 90 | 91 |92 | If you need a different strategy for processing event emissions, you can easily do so by providing an instance of a subclass of the Processing Service interface to the Event Emitter. 93 |
94 | 95 | ```Python linenums="1" hl_lines="3 7 8 14" 96 | import asyncio 97 | 98 | from pyventus.core.processing import ProcessingService 99 | from pyventus.events import EventEmitter, EventLinker 100 | 101 | 102 | class CustomProcessingService(ProcessingService): 103 | def submit(self, callback, *args, **kwargs): # (1)! 104 | coro = callback(*args, **kwargs) # (2)! 105 | if asyncio.iscoroutine(coro): 106 | asyncio.run(coro) 107 | 108 | 109 | event_emitter = EventEmitter(event_processor=CustomProcessingService()) 110 | 111 | EventLinker.subscribe("MyEvent", event_callback=print) 112 | event_emitter.emit("MyEvent", "Hello, World!") 113 | ``` 114 | 115 | 1. Implement this method to define the processing strategy. See the example below. 116 | 2. The given `callback` can be either a synchronous or an asynchronous Python function. For typing, you can refer to the base [submit()](../../../api/core/processing/#pyventus.core.processing.ProcessingService.submit) method. 117 | 118 | ## Runtime Flexibility 119 | 120 |121 | Thanks to the separation of concerns between the Event Linker and the Event Emitter, you can easily change the Event Emitter at runtime without needing to reconfigure all connections or implement complex logic. 122 |
123 | 124 | ```Python linenums="1" hl_lines="6 14 15" 125 | from concurrent.futures import ThreadPoolExecutor 126 | 127 | from pyventus.events import AsyncIOEventEmitter, EventEmitter, EventLinker, ExecutorEventEmitter 128 | 129 | 130 | def main(event_emitter: EventEmitter) -> None: 131 | event_emitter.emit("MyEvent", f"Using: {event_emitter}!") 132 | 133 | 134 | if __name__ == "__main__": 135 | executor = ThreadPoolExecutor() 136 | EventLinker.subscribe("MyEvent", event_callback=print) 137 | 138 | main(event_emitter=AsyncIOEventEmitter()) 139 | main(event_emitter=ExecutorEventEmitter(executor)) 140 | 141 | executor.shutdown() 142 | ``` 143 | 144 | ## Debug Mode 145 | 146 |147 | Pyventus' Event Emitter offers a useful debug mode feature to help you understand the flow of events and troubleshoot your event-driven application. You can enable debug mode in the Event Emitter using the following options: 148 |
149 | 150 | ### Global Debug Mode 151 | 152 |153 | By default, Pyventus leverages Python's global debug tracing feature to determine whether the code is running in debug mode or not. When this mode is enabled, all local debug flags are set to `True` unless they are already configured. To activate global debug mode, simply run your code in a debugger like [pdb](https://docs.python.org/3/library/pdb.html). 154 |
155 | 156 |
157 |
158 |
163 | Alternatively, if you want to enable or disable debug mode for a specific Event Emitter instance, you can use the `debug` flag. Setting the `debug` flag to `True` enables debug mode for that instance, while setting it to `False` disables debug mode. 164 |
165 | 166 | === "Debug Mode `On`" 167 | 168 | ```Python linenums="1" hl_lines="3" 169 | from pyventus.events import AsyncIOEventEmitter, EventEmitter 170 | 171 | event_emitter: EventEmitter = AsyncIOEventEmitter(debug=True) 172 | event_emitter.emit("MyEvent", "Hello, World!") 173 | ``` 174 | 175 | === "Debug Mode `Off`" 176 | 177 | ```Python linenums="1" hl_lines="3" 178 | from pyventus.events import AsyncIOEventEmitter, EventEmitter 179 | 180 | event_emitter: EventEmitter = AsyncIOEventEmitter(debug=False) 181 | event_emitter.emit("MyEvent", "Hello, World!") 182 | ``` 183 | -------------------------------------------------------------------------------- /src/pyventus/events/subscribers/event_subscriber.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Awaitable, Callable 2 | from typing import Any, TypeAlias, final 3 | 4 | from typing_extensions import Self, override 5 | 6 | from ...core.exceptions import PyventusException 7 | from ...core.subscriptions import Subscription 8 | from ...core.utils import CallableWrapper, attributes_repr, formatted_repr 9 | from ...events.handlers import EventHandler 10 | 11 | EventCallbackType: TypeAlias = Callable[..., Any] 12 | """Type alias for the event callback invoked when the event occurs.""" 13 | 14 | SuccessCallbackType: TypeAlias = Callable[..., Awaitable[None] | None] 15 | """Type alias for the callback invoked when the event response completes successfully.""" 16 | 17 | FailureCallbackType: TypeAlias = Callable[[Exception], Awaitable[None] | None] 18 | """Type alias for the callback invoked when the event response fails.""" 19 | 20 | 21 | @final 22 | class EventSubscriber(EventHandler, Subscription): 23 | """ 24 | A class that represents an `EventHandler` subscribed to an `EventLinker`. 25 | 26 | **Notes:** 27 | 28 | - This class combines the `EventHandler` interface with the `Subscription` 29 | base class, providing a convenient way to handle events and manage the 30 | subscription lifecycle. 31 | 32 | - This class is not intended to be subclassed or instantiated directly. 33 | """ 34 | 35 | # Attributes for the EventSubscriber 36 | __slots__ = ("__event_callback", "__success_callback", "__failure_callback", "__once") 37 | 38 | def __init__( 39 | self, 40 | teardown_callback: Callable[[Self], bool], 41 | event_callback: EventCallbackType, 42 | success_callback: SuccessCallbackType | None, 43 | failure_callback: FailureCallbackType | None, 44 | force_async: bool, 45 | once: bool, 46 | ) -> None: 47 | """ 48 | Initialize an instance of `EventSubscriber`. 49 | 50 | :param teardown_callback: A callback function invoked during the unsubscription process to perform 51 | cleanup or teardown operations associated with the subscription. It should return `True` if the 52 | cleanup was successful, or `False` if the teardown has already been executed and the subscription 53 | is no longer active. 54 | :param event_callback: The callback to be executed when the event occurs. 55 | :param success_callback: The callback to be executed when the event response completes successfully. 56 | :param failure_callback: The callback to be executed when the event response fails. 57 | :param force_async: Determines whether to force all callbacks to run asynchronously. 58 | If `True`, synchronous callbacks will be converted to run asynchronously in a 59 | thread pool, using the `asyncio.to_thread` function. If `False`, callbacks 60 | will run synchronously or asynchronously as defined. 61 | :param once: Specifies if the event subscriber is a one-time subscription. 62 | """ 63 | # Initialize the base Subscription class with the teardown callback 64 | super().__init__(teardown_callback=teardown_callback) 65 | 66 | # Ensure the 'once' parameter is a boolean 67 | if not isinstance(once, bool): 68 | raise PyventusException("The 'once' argument must be a boolean value.") 69 | 70 | # Wrap and set the event callback 71 | self.__event_callback = CallableWrapper[..., Any](event_callback, force_async=force_async) 72 | 73 | # Ensure that the event callback is not a generator. 74 | if self.__event_callback.is_generator: 75 | raise PyventusException("The 'event_callback' cannot be a generator.") 76 | 77 | # Wrap and set the success callback, if provided. 78 | self.__success_callback = ( 79 | CallableWrapper[..., None]( 80 | success_callback, 81 | force_async=force_async, 82 | ) 83 | if success_callback 84 | else None 85 | ) 86 | 87 | # Ensure that the success callback is not a generator. 88 | if self.__success_callback and self.__success_callback.is_generator: 89 | raise PyventusException("The 'success_callback' cannot be a generator.") 90 | 91 | # Wrap and set the failure callback, if provided. 92 | self.__failure_callback = ( 93 | CallableWrapper[[Exception], None]( 94 | failure_callback, 95 | force_async=force_async, 96 | ) 97 | if failure_callback 98 | else None 99 | ) 100 | 101 | # Ensure that the failure callback is not a generator. 102 | if self.__failure_callback and self.__failure_callback.is_generator: 103 | raise PyventusException("The 'failure_callback' cannot be a generator.") 104 | 105 | # Store the one-time subscription flag. 106 | self.__once: bool = once 107 | 108 | @override 109 | def __repr__(self) -> str: 110 | return formatted_repr( 111 | instance=self, 112 | info=( 113 | attributes_repr( 114 | event_callback=self.__event_callback, 115 | success_callback=self.__success_callback, 116 | failure_callback=self.__failure_callback, 117 | once=self.__once, 118 | ) 119 | + f", {super().__repr__()}" 120 | ), 121 | ) 122 | 123 | @property 124 | def once(self) -> bool: 125 | """ 126 | Determine if the event subscriber is a one-time subscription. 127 | 128 | :return: A boolean value indicating if the event subscriber 129 | is a one-time subscription. 130 | """ 131 | return self.__once 132 | 133 | @override 134 | async def _handle_event(self, *args: Any, **kwargs: Any) -> Any: 135 | # Execute the event callback with the provided arguments and return the result 136 | return await self.__event_callback.execute(*args, **kwargs) 137 | 138 | @override 139 | async def _handle_success(self, results: Any) -> None: 140 | if self.__success_callback is None: 141 | # If no success callback is set, exit early 142 | return 143 | elif results is None: 144 | # If results are None, invoke the success callback without parameters 145 | await self.__success_callback.execute() 146 | else: 147 | # Invoke the success callback with the given results 148 | await self.__success_callback.execute(results) 149 | 150 | @override 151 | async def _handle_failure(self, exception: Exception) -> None: 152 | if self.__failure_callback is None: 153 | # If no failure callback is set, exit early 154 | return 155 | else: 156 | # Invoke the failure callback with the provided exception 157 | await self.__failure_callback.execute(exception) 158 | 159 | @override 160 | def __getstate__(self) -> dict[str, Any]: 161 | # Retrieve the state of the base Subscription class 162 | state: dict[str, Any] = super().__getstate__() 163 | 164 | # Add the state of the EventSubscriber attributes 165 | state["__event_callback"] = self.__event_callback 166 | state["__success_callback"] = self.__success_callback 167 | state["__failure_callback"] = self.__failure_callback 168 | state["__once"] = self.__once 169 | 170 | # Return the complete state for serialization 171 | return state 172 | 173 | @override 174 | def __setstate__(self, state: dict[str, Any]) -> None: 175 | # Restore the state of the base Subscription class 176 | super().__setstate__(state) 177 | 178 | # Restore the state of the EventSubscriber attributes 179 | self.__event_callback = state["__event_callback"] 180 | self.__success_callback = state["__success_callback"] 181 | self.__failure_callback = state["__failure_callback"] 182 | self.__once = state["__once"] 183 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: Pyventus 3 | site_url: https://mdapena.github.io/pyventus/ 4 | site_author: Manuel Da Pena 5 | site_description: >- 6 | A powerful Python library for event-driven and reactive programming. 7 | 8 | # Repository 9 | repo_name: mdapena/pyventus 10 | repo_url: https://github.com/mdapena/pyventus 11 | 12 | # Copyright 13 | copyright: | 14 | Copyright © 2023-2025 Manuel Da Pena 15 | 16 | # Configuration 17 | theme: 18 | name: material 19 | language: en 20 | custom_dir: docs/.overrides 21 | logo: images/logo/pyventus-logo-white.png 22 | favicon: images/favicon/pyventus-logo.ico 23 | palette: 24 | # Palette toggle for automatic mode 25 | - media: "(prefers-color-scheme)" 26 | toggle: 27 | icon: material/weather-sunny 28 | name: Switch to light mode 29 | # Palette toggle for light mode 30 | - media: '(prefers-color-scheme: light)' 31 | scheme: default 32 | primary: cyan 33 | accent: amber 34 | toggle: 35 | icon: material/weather-night 36 | name: Switch to dark mode 37 | # Palette toggle for dark mode 38 | - media: '(prefers-color-scheme: dark)' 39 | scheme: slate 40 | primary: cyan 41 | accent: amber 42 | toggle: 43 | icon: material/brightness-auto 44 | name: Switch to system preference 45 | features: 46 | - navigation.instant 47 | - navigation.instant.progress 48 | - navigation.tracking 49 | - navigation.tabs 50 | - navigation.indexes 51 | - navigation.top 52 | - navigation.sections 53 | - navigation.footer 54 | - toc.follow 55 | - search.suggest 56 | - search.highlight 57 | - content.tabs.link 58 | - content.code.annotation 59 | - content.code.copy 60 | - content.tooltips 61 | - content.code.annotate 62 | - announce.dismiss 63 | 64 | # Customization 65 | extra: 66 | version: 67 | provider: mike 68 | default: 69 | - latest 70 | - dev 71 | social: 72 | - icon: fontawesome/brands/linkedin 73 | link: https://linkedin.com/in/manuel-da-pena 74 | - icon: fontawesome/brands/github 75 | link: https://github.com/mdapena 76 | analytics: 77 | provider: google 78 | property: !ENV GOOGLE_ANALYTICS_KEY 79 | feedback: 80 | title: Was this page helpful? 81 | ratings: 82 | - icon: material/thumb-up-outline 83 | name: This page was helpful 84 | data: 1 85 | note: >- 86 | Thanks for your feedback! 87 | - icon: material/thumb-down-outline 88 | name: This page could be improved 89 | data: 0 90 | note: >- 91 | Thanks for your feedback! 92 | 93 | # Plugins 94 | plugins: 95 | - search 96 | - git-committers: 97 | enabled: !ENV [ CI, false ] 98 | repository: mdapena/pyventus 99 | branch: master 100 | exclude: 101 | - api/* 102 | - learn/index.md 103 | - index.md 104 | - git-revision-date-localized: 105 | type: timeago 106 | enable_creation_date: true 107 | exclude: 108 | - api/* 109 | - learn/index.md 110 | - index.md 111 | - mkdocstrings: 112 | handlers: 113 | python: 114 | options: 115 | docstring_style: sphinx 116 | show_if_no_docstring: true 117 | inherited_members: true 118 | members_order: source 119 | separate_signature: true 120 | unwrap_annotated: true 121 | filters: ["!^(_.*$)", "^(__init__|__init_subclass__|__call__|__enter__|__exit__)$"] 122 | docstring_section_style: spacy 123 | signature_crossrefs: true 124 | show_symbol_type_heading: true 125 | show_symbol_type_toc: true 126 | show_signature_annotations: true 127 | show_category_heading: true 128 | - mike: 129 | alias_type: symlink 130 | canonical_version: latest 131 | - social: 132 | enabled: !ENV [ CI, false ] 133 | 134 | # Extensions 135 | markdown_extensions: 136 | - attr_list 137 | - md_in_html 138 | - pymdownx.arithmatex: 139 | generic: true 140 | - pymdownx.highlight: 141 | anchor_linenums: true 142 | - pymdownx.tasklist: 143 | custom_checkbox: true 144 | clickable_checkbox: true 145 | - pymdownx.inlinehilite 146 | - pymdownx.snippets: 147 | base_path: 148 | - !relative $docs_dir 149 | - admonition 150 | - footnotes 151 | - pymdownx.details 152 | - pymdownx.superfences 153 | - pymdownx.mark 154 | - abbr 155 | - pymdownx.emoji: 156 | emoji_index: !!python/name:material.extensions.emoji.twemoji 157 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 158 | - pymdownx.tabbed: 159 | alternate_style: true 160 | - toc: 161 | permalink: true 162 | 163 | # Extra Stylesheets 164 | extra_css: 165 | - stylesheets/announcement.css 166 | - stylesheets/search-bar.css 167 | 168 | # Extra JavaScript for MathJax 169 | extra_javascript: 170 | - javascripts/mathjax.js 171 | - javascripts/announcement.js 172 | - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js 173 | 174 | # Page tree 175 | nav: 176 | - Pyventus: index.md 177 | - Getting Started: getting-started.md 178 | - Learn: 179 | - learn/index.md 180 | - Upgrade Guide: learn/upgrade_guide.md 181 | - Events: 182 | - learn/events/index.md 183 | - Event Types: learn/events/types.md 184 | - Event Linkers: learn/events/linkers.md 185 | - Event Emitters: 186 | - learn/events/emitters/index.md 187 | - AsyncIO Event Emitter: learn/events/emitters/asyncio.md 188 | - Celery Event Emitter: learn/events/emitters/celery.md 189 | - Executor Event Emitter: learn/events/emitters/executor.md 190 | - FastAPI Event Emitter: learn/events/emitters/fastapi.md 191 | - Redis Event Emitter: learn/events/emitters/redis.md 192 | - Reactive: 193 | - learn/reactive/index.md 194 | - API Reference: 195 | - api/index.md 196 | - Core: 197 | - api/core/index.md 198 | - Collections: 199 | - MultiBidict: api/core/collections/multi_bidict.md 200 | - Exceptions: 201 | - PyventusException: api/core/exceptions/pyventus_exception.md 202 | - PyventusImportException: api/core/exceptions/pyventus_import_exception.md 203 | - Processing: 204 | - api/core/processing/index.md 205 | - AsyncIOProcessingService: api/core/processing/asyncio_processing_service.md 206 | - CeleryProcessingService: api/core/processing/celery_processing_service.md 207 | - ExecutorProcessingService: api/core/processing/executor_processing_service.md 208 | - FastAPIProcessingService: api/core/processing/fastapi_processing_service.md 209 | - RedisProcessingService: api/core/processing/redis_processing_service.md 210 | - Subscriptions: 211 | - SubscriptionContext: api/core/subscriptions/subscription_context.md 212 | - Subscription: api/core/subscriptions/subscription.md 213 | - Unsubscribable: api/core/subscriptions/unsubscribable.md 214 | - Events: 215 | - api/events/index.md 216 | - EventEmitter: 217 | - api/events/emitters/index.md 218 | - EventEmitter Utils: api/events/emitters/event_emitter_utils.md 219 | - EventHandler: api/events/handlers/event_handler.md 220 | - EventLinker: api/events/linkers/event_linker.md 221 | - EventSubscriber: api/events/subscribers/event_subscriber.md 222 | - Reactive: 223 | - api/reactive/index.md 224 | - Observable: 225 | - api/reactive/observables/index.md 226 | - ObservableTask: api/reactive/observables/observable_task.md 227 | - Observable Utils: api/reactive/observables/observable_utils.md 228 | - Observer: api/reactive/observers/observer.md 229 | - Subscriber: api/reactive/subscribers/subscriber.md 230 | - Contributing: contributing.md 231 | - Release Notes: release-notes.md 232 | -------------------------------------------------------------------------------- /src/pyventus/events/emitters/event_emitter_utils.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Generator 2 | from concurrent.futures import Executor, ThreadPoolExecutor 3 | from contextlib import contextmanager 4 | from typing import Any 5 | 6 | from ..linkers import EventLinker 7 | from .event_emitter import EventEmitter 8 | 9 | 10 | def AsyncIOEventEmitter( # noqa: N802 11 | event_linker: type[EventLinker] = EventLinker, 12 | debug: bool | None = None, 13 | ) -> EventEmitter: 14 | """ 15 | Create an `EventEmitter` instance configured with the `AsyncIOProcessingService`. 16 | 17 | :param event_linker: Specifies the type of event linker used to manage and access events along with their 18 | corresponding subscribers. Defaults to `EventLinker`. 19 | :param debug: Specifies the debug mode for the logger. If `None`, it is determined based on the 20 | execution environment. 21 | :return: An instance of `EventEmitter` configured with the `AsyncIOProcessingService`. 22 | """ 23 | from ...core.processing.asyncio import AsyncIOProcessingService 24 | 25 | processing_service = AsyncIOProcessingService() 26 | 27 | return EventEmitter( 28 | event_processor=processing_service, 29 | event_linker=event_linker, 30 | debug=debug, 31 | ) 32 | 33 | 34 | def CeleryEventEmitter( # noqa: N802 35 | celery: Any, 36 | queue: str | None = None, 37 | event_linker: type[EventLinker] = EventLinker, 38 | debug: bool | None = None, 39 | ) -> EventEmitter: 40 | """ 41 | Create an `EventEmitter` instance configured with the `CeleryProcessingService`. 42 | 43 | :param celery: The Celery object used to enqueue and process event emissions. 44 | :param queue: The name of the queue where the event emission will be enqueued. 45 | Defaults to None, which uses the task_default_queue from the Celery configuration. 46 | :param event_linker: Specifies the type of event linker used to manage and access events along with their 47 | corresponding subscribers. Defaults to `EventLinker`. 48 | :param debug: Specifies the debug mode for the logger. If `None`, it is determined based on the 49 | execution environment. 50 | :return: An instance of `EventEmitter` configured with the `CeleryProcessingService`. 51 | """ 52 | from ...core.processing.celery import CeleryProcessingService 53 | 54 | processing_service = CeleryProcessingService(celery=celery, queue=queue) 55 | 56 | return EventEmitter( 57 | event_processor=processing_service, 58 | event_linker=event_linker, 59 | debug=debug, 60 | ) 61 | 62 | 63 | def ExecutorEventEmitter( # noqa: N802 64 | executor: Executor, 65 | event_linker: type[EventLinker] = EventLinker, 66 | debug: bool | None = None, 67 | ) -> EventEmitter: 68 | """ 69 | Create an `EventEmitter` instance configured with the `ExecutorProcessingService`. 70 | 71 | :param executor: The executor object used to handle the execution of event emissions. 72 | :param event_linker: Specifies the type of event linker used to manage and access events along with their 73 | corresponding subscribers. Defaults to `EventLinker`. 74 | :param debug: Specifies the debug mode for the logger. If `None`, it is determined based on the 75 | execution environment. 76 | :return: An instance of `EventEmitter` configured with the `ExecutorProcessingService`. 77 | """ 78 | from ...core.processing.executor import ExecutorProcessingService 79 | 80 | processing_service = ExecutorProcessingService(executor=executor) 81 | 82 | return EventEmitter( 83 | event_processor=processing_service, 84 | event_linker=event_linker, 85 | debug=debug, 86 | ) 87 | 88 | 89 | @contextmanager 90 | def ExecutorEventEmitterCtx( # noqa: N802 91 | executor: Executor | None = None, 92 | event_linker: type[EventLinker] = EventLinker, 93 | debug: bool | None = None, 94 | ) -> Generator[EventEmitter, None, None]: 95 | """ 96 | Context manager that creates an `EventEmitter` instance configured with the `ExecutorProcessingService`. 97 | 98 | This context manager yields an `EventEmitter` instance, which can be used within a `with` statement. 99 | Upon exiting the context, the processing service is properly shut down. 100 | 101 | :param executor: The executor object used to handle the execution of event emissions. If `None`, 102 | a `ThreadPoolExecutor` with default settings will be created. 103 | :param event_linker: Specifies the type of event linker used to manage and access events along with their 104 | corresponding subscribers. Defaults to `EventLinker`. 105 | :param debug: Specifies the debug mode for the logger. If `None`, it is determined based on the 106 | execution environment. 107 | :return: An instance of `EventEmitter` configured with the `ExecutorProcessingService`. 108 | """ 109 | from ...core.processing.executor import ExecutorProcessingService 110 | 111 | processing_service = ExecutorProcessingService(executor=(executor if executor else ThreadPoolExecutor())) 112 | 113 | yield EventEmitter( 114 | event_processor=processing_service, 115 | event_linker=event_linker, 116 | debug=debug, 117 | ) 118 | 119 | processing_service.shutdown() 120 | 121 | 122 | def FastAPIEventEmitter( # noqa: N802 123 | event_linker: type[EventLinker] = EventLinker, 124 | debug: bool | None = None, 125 | ) -> Callable[[Any], EventEmitter]: 126 | """ 127 | Create an `EventEmitter` instance configured with the `FastAPIProcessingService`. 128 | 129 | This function is compatible with FastAPI's dependency injection system and should be 130 | used with the `Depends` method to automatically provide the `BackgroundTasks` instance. 131 | 132 | :param event_linker: Specifies the type of event linker used to manage and access events along with their 133 | corresponding subscribers. Defaults to `EventLinker`. 134 | :param debug: Specifies the debug mode for the logger. If `None`, it is determined based on the 135 | execution environment. 136 | :return: An instance of `EventEmitter` configured with the `FastAPIProcessingService`. 137 | """ 138 | from fastapi import BackgroundTasks 139 | 140 | from ...core.processing.fastapi import FastAPIProcessingService 141 | 142 | def create_event_emitter(background_tasks: BackgroundTasks) -> EventEmitter: 143 | """ 144 | Create and return an `EventEmitter` instance using the provided `BackgroundTasks`. 145 | 146 | :param background_tasks: The FastAPI `BackgroundTasks` object used to handle the execution of event emissions. 147 | :return: An instance of `EventEmitter` configured with the `FastAPIProcessingService`. 148 | """ 149 | processing_service = FastAPIProcessingService(background_tasks=background_tasks) 150 | 151 | return EventEmitter( 152 | event_processor=processing_service, 153 | event_linker=event_linker, 154 | debug=debug, 155 | ) 156 | 157 | return create_event_emitter 158 | 159 | 160 | def RedisEventEmitter( # noqa: N802 161 | queue: Any, 162 | options: dict[str, Any] | None = None, 163 | event_linker: type[EventLinker] = EventLinker, 164 | debug: bool | None = None, 165 | ) -> EventEmitter: 166 | """ 167 | Create an `EventEmitter` instance configured with the `RedisProcessingService`. 168 | 169 | :param queue: The Redis queue object used to enqueue and process event emissions. 170 | :param options: Additional options for the RQ package enqueueing method. Defaults to None (an empty dictionary). 171 | :param event_linker: Specifies the type of event linker used to manage and access events along with their 172 | corresponding subscribers. Defaults to `EventLinker`. 173 | :param debug: Specifies the debug mode for the logger. If `None`, it is determined based on the 174 | execution environment. 175 | :return: An instance of `EventEmitter` configured with the `RedisProcessingService`. 176 | """ 177 | from ...core.processing.redis import RedisProcessingService 178 | 179 | processing_service = RedisProcessingService(queue=queue, options=options) 180 | 181 | return EventEmitter( 182 | event_processor=processing_service, 183 | event_linker=event_linker, 184 | debug=debug, 185 | ) 186 | -------------------------------------------------------------------------------- /tests/pyventus/events/emitters/test_event_emitter_utils.py: -------------------------------------------------------------------------------- 1 | from pyventus.events import ( 2 | AsyncIOEventEmitter, 3 | CeleryEventEmitter, 4 | EventEmitter, 5 | EventLinker, 6 | ExecutorEventEmitter, 7 | ExecutorEventEmitterCtx, 8 | FastAPIEventEmitter, 9 | ) 10 | 11 | from ....utils import get_private_attr 12 | 13 | 14 | class TestEventEmitterUtils: 15 | # ================================= 16 | # Test Cases for AsyncIOEventEmitter 17 | # ================================= 18 | 19 | def test_AsyncIOEventEmitter(self) -> None: # noqa: N802 20 | from pyventus.core.processing.asyncio import AsyncIOProcessingService 21 | 22 | # Arrange 23 | class IsolatedEventLinker(EventLinker): ... 24 | 25 | # Act 26 | event_emitter = AsyncIOEventEmitter(event_linker=IsolatedEventLinker, debug=True) 27 | event_processor = get_private_attr(event_emitter, "__event_processor") 28 | event_linker = get_private_attr(event_emitter, "__event_linker") 29 | logger = get_private_attr(event_emitter, "__logger") 30 | 31 | # Assert creation and properties 32 | assert event_emitter is not None 33 | assert isinstance(event_emitter, EventEmitter) 34 | assert isinstance(event_processor, AsyncIOProcessingService) 35 | assert event_linker is IsolatedEventLinker 36 | assert logger.debug_enabled is True 37 | 38 | # ================================= 39 | # Test Cases for CeleryEventEmitter 40 | # ================================= 41 | 42 | def test_CeleryEventEmitter(self) -> None: # noqa: N802 43 | from celery import Celery 44 | from pyventus.core.processing.celery import CeleryProcessingService 45 | 46 | # Arrange 47 | class IsolatedEventLinker(EventLinker): ... 48 | 49 | celery = Celery() 50 | queue = "Queue" 51 | 52 | # Act 53 | event_emitter = CeleryEventEmitter(celery=celery, queue=queue, event_linker=IsolatedEventLinker, debug=True) 54 | event_processor = get_private_attr(event_emitter, "__event_processor") 55 | event_linker = get_private_attr(event_emitter, "__event_linker") 56 | logger = get_private_attr(event_emitter, "__logger") 57 | 58 | # Assert creation and properties 59 | assert event_emitter is not None 60 | assert isinstance(event_emitter, EventEmitter) 61 | assert isinstance(event_processor, CeleryProcessingService) 62 | assert get_private_attr(event_processor, "__celery") is celery 63 | assert get_private_attr(event_processor, "__queue") is queue 64 | assert event_linker is IsolatedEventLinker 65 | assert logger.debug_enabled is True 66 | 67 | # ================================= 68 | # Test Cases for ExecutorEventEmitter 69 | # ================================= 70 | 71 | def test_ExecutorEventEmitter(self) -> None: # noqa: N802 72 | from concurrent.futures import ThreadPoolExecutor 73 | 74 | from pyventus.core.processing.executor import ExecutorProcessingService 75 | 76 | # Arrange 77 | class IsolatedEventLinker(EventLinker): ... 78 | 79 | executor = ThreadPoolExecutor() 80 | 81 | # Act 82 | event_emitter = ExecutorEventEmitter(executor=executor, event_linker=IsolatedEventLinker, debug=True) 83 | event_processor = get_private_attr(event_emitter, "__event_processor") 84 | event_linker = get_private_attr(event_emitter, "__event_linker") 85 | logger = get_private_attr(event_emitter, "__logger") 86 | 87 | # Assert creation and properties 88 | assert event_emitter is not None 89 | assert isinstance(event_emitter, EventEmitter) 90 | assert isinstance(event_processor, ExecutorProcessingService) 91 | assert get_private_attr(event_processor, "__executor") is executor 92 | assert event_linker is IsolatedEventLinker 93 | assert logger.debug_enabled is True 94 | assert executor._shutdown is False 95 | 96 | # ================================= 97 | # Test Cases for ExecutorEventEmitterCtx 98 | # ================================= 99 | 100 | def test_ExecutorEventEmitterCtx(self) -> None: # noqa: N802 101 | from concurrent.futures import ThreadPoolExecutor 102 | 103 | from pyventus.core.processing.executor import ExecutorProcessingService 104 | 105 | # Arrange 106 | class IsolatedEventLinker(EventLinker): ... 107 | 108 | executor = ThreadPoolExecutor() 109 | 110 | # Act 111 | with ExecutorEventEmitterCtx(executor=executor, event_linker=IsolatedEventLinker, debug=True) as event_emitter: 112 | event_processor = get_private_attr(event_emitter, "__event_processor") 113 | event_linker = get_private_attr(event_emitter, "__event_linker") 114 | logger = get_private_attr(event_emitter, "__logger") 115 | 116 | # Assert creation and properties 117 | assert event_emitter is not None 118 | assert isinstance(event_emitter, EventEmitter) 119 | assert isinstance(event_processor, ExecutorProcessingService) 120 | assert get_private_attr(event_processor, "__executor") is executor 121 | assert event_linker is IsolatedEventLinker 122 | assert logger.debug_enabled is True 123 | assert executor._shutdown is True 124 | 125 | # ================================= 126 | # Test Cases for FastAPIEventEmitter 127 | # ================================= 128 | 129 | def test_FastAPIEventEmitter(self) -> None: # noqa: N802 130 | from fastapi import BackgroundTasks, Depends, FastAPI 131 | from fastapi.testclient import TestClient 132 | from pyventus.core.processing.fastapi import FastAPIProcessingService 133 | from starlette.status import HTTP_200_OK 134 | 135 | # Arrange 136 | class IsolatedEventLinker(EventLinker): ... 137 | 138 | client = TestClient(FastAPI()) 139 | 140 | # Act 141 | @client.app.get("/") # type: ignore[attr-defined, misc] 142 | def api( 143 | background_tasks: BackgroundTasks, 144 | event_emitter: EventEmitter = Depends(FastAPIEventEmitter(IsolatedEventLinker, True)), # noqa: B008 145 | ) -> None: 146 | event_processor = get_private_attr(event_emitter, "__event_processor") 147 | event_linker = get_private_attr(event_emitter, "__event_linker") 148 | logger = get_private_attr(event_emitter, "__logger") 149 | 150 | # Assert creation and properties 151 | assert event_emitter is not None 152 | assert isinstance(event_emitter, EventEmitter) 153 | assert isinstance(event_processor, FastAPIProcessingService) 154 | assert get_private_attr(event_processor, "__background_tasks") is background_tasks 155 | assert event_linker is IsolatedEventLinker 156 | assert logger.debug_enabled is True 157 | 158 | assert client.get("/").status_code == HTTP_200_OK 159 | 160 | # ================================= 161 | # Test Cases for RedisEventEmitter 162 | # ================================= 163 | 164 | def test_RedisEventEmitter(self) -> None: # noqa: N802 165 | from fakeredis import FakeStrictRedis 166 | from pyventus.core.processing.redis import RedisProcessingService 167 | from pyventus.events import RedisEventEmitter 168 | from rq import Queue 169 | 170 | # Arrange 171 | class IsolatedEventLinker(EventLinker): ... 172 | 173 | queue = Queue(connection=FakeStrictRedis(), is_async=False) 174 | options = {"ttl": 1} 175 | 176 | # Act 177 | event_emitter = RedisEventEmitter(queue=queue, options=options, event_linker=IsolatedEventLinker, debug=True) 178 | event_processor = get_private_attr(event_emitter, "__event_processor") 179 | event_linker = get_private_attr(event_emitter, "__event_linker") 180 | logger = get_private_attr(event_emitter, "__logger") 181 | 182 | # Assert creation and properties 183 | assert event_emitter is not None 184 | assert isinstance(event_emitter, EventEmitter) 185 | assert isinstance(event_processor, RedisProcessingService) 186 | assert get_private_attr(event_processor, "__queue") is queue 187 | assert get_private_attr(event_processor, "__options") is options 188 | assert event_linker is IsolatedEventLinker 189 | assert logger.debug_enabled is True 190 | --------------------------------------------------------------------------------