fastapi-factory-utilities 0.2.0__py3-none-any.whl → 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fastapi-factory-utilities might be problematic. Click here for more details.
- fastapi_factory_utilities/core/api/__init__.py +1 -1
- fastapi_factory_utilities/core/api/v1/sys/health.py +1 -1
- fastapi_factory_utilities/core/app/__init__.py +12 -3
- fastapi_factory_utilities/core/app/application.py +24 -26
- fastapi_factory_utilities/core/app/builder.py +23 -37
- fastapi_factory_utilities/core/app/config.py +22 -1
- fastapi_factory_utilities/core/app/fastapi_builder.py +3 -2
- fastapi_factory_utilities/core/exceptions.py +58 -22
- fastapi_factory_utilities/core/plugins/__init__.py +2 -31
- fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
- fastapi_factory_utilities/core/plugins/aiopika/__init__.py +25 -0
- fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
- fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
- fastapi_factory_utilities/core/plugins/aiopika/depends.py +20 -0
- fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
- fastapi_factory_utilities/core/plugins/aiopika/exchange.py +70 -0
- fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
- fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
- fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
- fastapi_factory_utilities/core/plugins/aiopika/plugins.py +84 -0
- fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
- fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
- fastapi_factory_utilities/core/plugins/aiopika/queue.py +86 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +25 -153
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +59 -31
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +2 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/helpers.py +16 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +112 -3
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -115
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +65 -14
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +13 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/__init__.py +29 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/configs.py +12 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/depends.py +51 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/exceptions.py +13 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/plugin.py +41 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/schedulers.py +187 -0
- fastapi_factory_utilities/core/protocols.py +1 -54
- fastapi_factory_utilities/core/security/jwt.py +159 -0
- fastapi_factory_utilities/core/security/kratos.py +98 -0
- fastapi_factory_utilities/core/services/hydra/__init__.py +13 -0
- fastapi_factory_utilities/core/services/hydra/exceptions.py +15 -0
- fastapi_factory_utilities/core/services/hydra/objects.py +26 -0
- fastapi_factory_utilities/core/services/hydra/services.py +122 -0
- fastapi_factory_utilities/core/services/kratos/__init__.py +13 -0
- fastapi_factory_utilities/core/services/kratos/enums.py +11 -0
- fastapi_factory_utilities/core/services/kratos/exceptions.py +15 -0
- fastapi_factory_utilities/core/services/kratos/objects.py +43 -0
- fastapi_factory_utilities/core/services/kratos/services.py +86 -0
- fastapi_factory_utilities/core/services/status/__init__.py +2 -2
- fastapi_factory_utilities/core/utils/status.py +2 -1
- fastapi_factory_utilities/core/utils/uvicorn.py +36 -0
- fastapi_factory_utilities/core/utils/yaml_reader.py +2 -2
- fastapi_factory_utilities/example/app.py +15 -5
- fastapi_factory_utilities/example/entities/books/__init__.py +1 -1
- fastapi_factory_utilities/example/models/books/__init__.py +1 -1
- fastapi_factory_utilities/py.typed +0 -0
- {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/METADATA +23 -14
- fastapi_factory_utilities-0.7.1.dist-info/RECORD +101 -0
- {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/WHEEL +1 -1
- fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
- fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
- fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -190
- fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
- fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
- fastapi_factory_utilities-0.2.0.dist-info/RECORD +0 -70
- {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/entry_points.txt +0 -0
- {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Provides the configuration for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, ClassVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, UrlConstraints
|
|
6
|
+
from pydantic_core import Url, ValidationError
|
|
7
|
+
|
|
8
|
+
from fastapi_factory_utilities.core.utils.importlib import get_path_file_in_package
|
|
9
|
+
from fastapi_factory_utilities.core.utils.yaml_reader import (
|
|
10
|
+
UnableToReadYamlFileError,
|
|
11
|
+
YamlFileReader,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .exceptions import AiopikaPluginConfigError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AiopikaConfig(BaseModel):
|
|
18
|
+
"""Provides the configuration model for the Aiopika plugin.
|
|
19
|
+
|
|
20
|
+
https://docs.aio-pika.com/#aio-pika-connect-robust-function-and-aio-pika-robustconnection-class-specific
|
|
21
|
+
|
|
22
|
+
Possible query parameters for the AMQP URL:
|
|
23
|
+
name (str url encoded) - A string that will be visible in the RabbitMQ management console
|
|
24
|
+
and in the server logs, convenient for diagnostics.
|
|
25
|
+
cafile (str) - Path to Certificate Authority file
|
|
26
|
+
capath (str) - Path to Certificate Authority directory
|
|
27
|
+
cadata (str url encoded) - URL encoded CA certificate content
|
|
28
|
+
keyfile (str) - Path to client ssl private key file
|
|
29
|
+
certfile (str) - Path to client ssl certificate file
|
|
30
|
+
no_verify_ssl - No verify server SSL certificates. 0 by default and means False other value means True.
|
|
31
|
+
heartbeat (int-like) - interval in seconds between AMQP heartbeat packets. 0 disables this feature.
|
|
32
|
+
reconnect_interval (float-like) - is the period in seconds, not more often than the attempts
|
|
33
|
+
to re-establish the connection will take place.
|
|
34
|
+
fail_fast (true/yes/y/enable/on/enabled/1 means True, otherwise False) - special behavior
|
|
35
|
+
for the start connection attempt, if it fails, all other attempts stops
|
|
36
|
+
and an exception will be thrown at the connection stage. Enabled by default, if you are sure you need
|
|
37
|
+
to disable this feature, be ensures for the passed URL is really working.
|
|
38
|
+
Otherwise, your program will go into endless reconnection attempts that can not be successed.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="forbid")
|
|
43
|
+
|
|
44
|
+
amqp_url: Annotated[Url, UrlConstraints(allowed_schemes=["amqp", "amqps"])] = Field(description="The AMQP URL.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_config_from_package(package_name: str) -> AiopikaConfig:
|
|
48
|
+
"""Build the configuration from the package.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
package_name (str): The package name.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
AiopikaConfig: The Aiopika configuration.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
AiopikaPluginConfigError: If the configuration cannot be read or created or the configuration is invalid.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
yaml_file_content: dict[str, Any] = YamlFileReader(
|
|
61
|
+
file_path=get_path_file_in_package(
|
|
62
|
+
filename="application.yaml",
|
|
63
|
+
package=package_name,
|
|
64
|
+
),
|
|
65
|
+
yaml_base_key="aiopika",
|
|
66
|
+
use_environment_injection=True,
|
|
67
|
+
).read()
|
|
68
|
+
except (FileNotFoundError, ImportError, UnableToReadYamlFileError) as exception:
|
|
69
|
+
raise AiopikaPluginConfigError(
|
|
70
|
+
message="Unable to read the application configuration file for the Aiopika plugin in the package.",
|
|
71
|
+
package_name=package_name,
|
|
72
|
+
) from exception
|
|
73
|
+
|
|
74
|
+
# Create the application configuration model
|
|
75
|
+
config: AiopikaConfig
|
|
76
|
+
try:
|
|
77
|
+
config = AiopikaConfig(**yaml_file_content)
|
|
78
|
+
except ValidationError as exception:
|
|
79
|
+
raise AiopikaPluginConfigError(
|
|
80
|
+
message="Unable to create the application configuration model for the Aiopika plugin in the package.",
|
|
81
|
+
package_name=package_name,
|
|
82
|
+
validation_errors=exception.errors(),
|
|
83
|
+
) from exception
|
|
84
|
+
|
|
85
|
+
return config
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Provides the dependencies for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from aio_pika.abc import AbstractRobustConnection
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
|
|
8
|
+
from .exceptions import AiopikaPluginBaseError
|
|
9
|
+
|
|
10
|
+
DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY: str = "aiopika_robust_connection"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def depends_aiopika_robust_connection(request: Request) -> AbstractRobustConnection:
|
|
14
|
+
"""Get the Aiopika robust connection."""
|
|
15
|
+
robust_connection: AbstractRobustConnection | None = cast(
|
|
16
|
+
AbstractRobustConnection | None, getattr(request.app.state, DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY, None)
|
|
17
|
+
)
|
|
18
|
+
if robust_connection is None:
|
|
19
|
+
raise AiopikaPluginBaseError("Aiopika robust connection not found in the application state.")
|
|
20
|
+
return robust_connection
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Provides the exceptions for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi_factory_utilities.core.exceptions import FastAPIFactoryUtilitiesError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AiopikaPluginBaseError(FastAPIFactoryUtilitiesError):
|
|
9
|
+
"""Base class for all exceptions raised by the Aiopika plugin."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
12
|
+
"""Initialize the Aiopika plugin base exception."""
|
|
13
|
+
super().__init__(message, **kwargs)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AiopikaPluginConfigError(AiopikaPluginBaseError):
|
|
17
|
+
"""Exception for the Aiopika plugin configuration."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AiopikaPluginConnectionNotProvidedError(AiopikaPluginBaseError):
|
|
21
|
+
"""Exception for the Aiopika plugin connection not provided."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AiopikaPluginExchangeNotDeclaredError(AiopikaPluginBaseError):
|
|
25
|
+
"""Exception for the Aiopika plugin exchange not declared."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AiopikaPluginQueueNotDeclaredError(AiopikaPluginBaseError):
|
|
29
|
+
"""Exception for the Aiopika plugin queue not declared."""
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Provides the abstract class for the exchange port for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, Self
|
|
4
|
+
|
|
5
|
+
from aio_pika import Exchange as AiopikaExchange
|
|
6
|
+
from aio_pika import ExchangeType
|
|
7
|
+
from aio_pika.abc import TimeoutType
|
|
8
|
+
|
|
9
|
+
from .abstract import AbstractAiopikaResource
|
|
10
|
+
from .exceptions import AiopikaPluginBaseError, AiopikaPluginExchangeNotDeclaredError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Exchange(AbstractAiopikaResource):
|
|
14
|
+
"""Abstract class for the exchange port for the Aiopika plugin."""
|
|
15
|
+
|
|
16
|
+
DEFAULT_OPERATION_TIMEOUT: ClassVar[TimeoutType] = 10.0
|
|
17
|
+
|
|
18
|
+
def __init__( # pylint: disable=too-many-arguments # noqa: PLR0913
|
|
19
|
+
self,
|
|
20
|
+
name: str,
|
|
21
|
+
exchange_type: ExchangeType,
|
|
22
|
+
durable: bool = True,
|
|
23
|
+
auto_delete: bool = False,
|
|
24
|
+
internal: bool = False,
|
|
25
|
+
passive: bool = False,
|
|
26
|
+
timeout: TimeoutType = DEFAULT_OPERATION_TIMEOUT,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Initialize the exchange port."""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._name: str = name
|
|
31
|
+
self._exchange_type: ExchangeType = exchange_type
|
|
32
|
+
self._durable: bool = durable
|
|
33
|
+
self._auto_delete: bool = auto_delete
|
|
34
|
+
self._internal: bool = internal
|
|
35
|
+
self._passive: bool = passive
|
|
36
|
+
self._timeout: TimeoutType = timeout
|
|
37
|
+
self._aiopika_exchange: AiopikaExchange | None = None
|
|
38
|
+
self._is_declared: bool = False
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def exchange(self) -> AiopikaExchange:
|
|
42
|
+
"""Get the Aiopika exchange."""
|
|
43
|
+
if self._aiopika_exchange is None:
|
|
44
|
+
raise AiopikaPluginExchangeNotDeclaredError(message="Exchange not declared.", exchange=self._name)
|
|
45
|
+
return self._aiopika_exchange
|
|
46
|
+
|
|
47
|
+
async def _declare(self) -> Self:
|
|
48
|
+
"""Declare the exchange."""
|
|
49
|
+
try:
|
|
50
|
+
self._aiopika_exchange = await self._channel.declare_exchange( # pyright: ignore
|
|
51
|
+
name=self._name,
|
|
52
|
+
type=self._exchange_type,
|
|
53
|
+
durable=self._durable,
|
|
54
|
+
auto_delete=self._auto_delete,
|
|
55
|
+
internal=self._internal,
|
|
56
|
+
passive=self._passive,
|
|
57
|
+
timeout=self._timeout,
|
|
58
|
+
)
|
|
59
|
+
except Exception as exception:
|
|
60
|
+
raise AiopikaPluginBaseError(
|
|
61
|
+
message="Failed to declare the exchange.",
|
|
62
|
+
) from exception
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
async def setup(self) -> Self:
|
|
66
|
+
"""Setup the exchange."""
|
|
67
|
+
await super().setup()
|
|
68
|
+
if self._aiopika_exchange is None:
|
|
69
|
+
await self._declare()
|
|
70
|
+
return self
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Provides the abstract class for the listener port for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any, ClassVar, Generic, Self, TypeVar, cast, get_args
|
|
6
|
+
|
|
7
|
+
from aio_pika.abc import ConsumerTag, TimeoutType
|
|
8
|
+
from aio_pika.message import IncomingMessage
|
|
9
|
+
|
|
10
|
+
from ..abstract import AbstractAiopikaResource
|
|
11
|
+
from ..message import AbstractMessage
|
|
12
|
+
from ..queue import Queue
|
|
13
|
+
|
|
14
|
+
GenericMessage = TypeVar("GenericMessage", bound=AbstractMessage[Any])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AbstractListener(AbstractAiopikaResource, Generic[GenericMessage]):
|
|
18
|
+
"""Abstract class for the listener port for the Aiopika plugin."""
|
|
19
|
+
|
|
20
|
+
DEFAULT_OPERATION_TIMEOUT: ClassVar[TimeoutType] = 10.0
|
|
21
|
+
|
|
22
|
+
def __init__(self, queue: Queue, name: str | None = None) -> None:
|
|
23
|
+
"""Initialize the listener port."""
|
|
24
|
+
super().__init__()
|
|
25
|
+
self._name: str = name or self.__class__.__name__
|
|
26
|
+
self._queue: Queue = queue
|
|
27
|
+
self._consumer_tag: ConsumerTag | None = None
|
|
28
|
+
generic_args: tuple[Any, ...] = get_args(self.__orig_bases__[0]) # type: ignore
|
|
29
|
+
self._message_type: type[GenericMessage] = generic_args[0]
|
|
30
|
+
|
|
31
|
+
async def setup(self) -> Self:
|
|
32
|
+
"""Setup the listener."""
|
|
33
|
+
await super().setup()
|
|
34
|
+
await self._queue.setup()
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
async def listen(self) -> None:
|
|
38
|
+
"""Listen for messages."""
|
|
39
|
+
self._consumer_tag = await self._queue.queue.consume( # pyright: ignore
|
|
40
|
+
callback=cast(Callable[[IncomingMessage], Awaitable[Any]], self._on_message), # pyright: ignore
|
|
41
|
+
exclusive=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
async def _on_message(self, incoming_message: IncomingMessage) -> None:
|
|
45
|
+
"""On message."""
|
|
46
|
+
message: GenericMessage = self._message_type.model_validate_json(incoming_message.body)
|
|
47
|
+
message.set_incoming_message(incoming_message=incoming_message)
|
|
48
|
+
await self.on_message(message=message)
|
|
49
|
+
|
|
50
|
+
async def close(self) -> None:
|
|
51
|
+
"""Close the listener.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
- None: The listener is closed.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
- AiopikaPluginBaseException: If the listener cannot be closed.
|
|
58
|
+
"""
|
|
59
|
+
if self._consumer_tag is not None:
|
|
60
|
+
await self._queue.queue.cancel(consumer_tag=self._consumer_tag)
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
async def on_message(self, message: GenericMessage) -> None:
|
|
64
|
+
"""On message.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
message (GenericMessage): The message.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
- None: The message is processed.
|
|
71
|
+
"""
|
|
72
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Provides the message for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum, auto
|
|
4
|
+
from typing import ClassVar, Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from aio_pika.abc import DeliveryMode, HeadersType
|
|
7
|
+
from aio_pika.message import IncomingMessage, Message
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
9
|
+
|
|
10
|
+
GenericMessageData = TypeVar("GenericMessageData", bound=BaseModel)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SenderModel(BaseModel):
|
|
14
|
+
"""Sender model."""
|
|
15
|
+
|
|
16
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", frozen=True)
|
|
17
|
+
|
|
18
|
+
name: str = Field(description="The name of the sender.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MessageTypeEnum(StrEnum):
|
|
22
|
+
"""Message type enum."""
|
|
23
|
+
|
|
24
|
+
FUNCTIONAL_EVENT = auto()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AbstractMessage(BaseModel, Generic[GenericMessageData]):
|
|
28
|
+
"""Abstract message."""
|
|
29
|
+
|
|
30
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", frozen=True)
|
|
31
|
+
|
|
32
|
+
message_type: MessageTypeEnum = Field(
|
|
33
|
+
description="The type of the message.", default=MessageTypeEnum.FUNCTIONAL_EVENT
|
|
34
|
+
)
|
|
35
|
+
sender: SenderModel = Field(description="The sender of the message.")
|
|
36
|
+
data: GenericMessageData = Field(description="The data of the message.")
|
|
37
|
+
|
|
38
|
+
_incoming_message: IncomingMessage | None = PrivateAttr()
|
|
39
|
+
_headers: HeadersType = PrivateAttr(default_factory=dict)
|
|
40
|
+
|
|
41
|
+
def get_headers(self) -> HeadersType:
|
|
42
|
+
"""Get the headers of the message."""
|
|
43
|
+
return self._headers
|
|
44
|
+
|
|
45
|
+
def set_headers(self, headers: HeadersType) -> None:
|
|
46
|
+
"""Set the headers of the message."""
|
|
47
|
+
self._headers = headers
|
|
48
|
+
|
|
49
|
+
def set_incoming_message(self, incoming_message: IncomingMessage) -> None:
|
|
50
|
+
"""Set the incoming message."""
|
|
51
|
+
self._incoming_message = incoming_message
|
|
52
|
+
self.set_headers(headers=incoming_message.headers)
|
|
53
|
+
|
|
54
|
+
async def ack(self) -> None:
|
|
55
|
+
"""Ack the message.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
- ValueError: If the incoming message is not set.
|
|
59
|
+
"""
|
|
60
|
+
if self._incoming_message is None:
|
|
61
|
+
raise ValueError("Incoming message is not set.")
|
|
62
|
+
await self._incoming_message.ack(multiple=False)
|
|
63
|
+
|
|
64
|
+
async def reject(self, requeue: bool = True) -> None:
|
|
65
|
+
"""Reject the message.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
requeue (bool): Whether to requeue the message.
|
|
69
|
+
|
|
70
|
+
Raises:
|
|
71
|
+
- ValueError: If the incoming message is not set.
|
|
72
|
+
"""
|
|
73
|
+
if self._incoming_message is None:
|
|
74
|
+
raise ValueError("Incoming message is not set.")
|
|
75
|
+
await self._incoming_message.reject(requeue=requeue)
|
|
76
|
+
|
|
77
|
+
def to_aiopika_message(self) -> Message:
|
|
78
|
+
"""Convert the message to an Aiopika message."""
|
|
79
|
+
return Message(
|
|
80
|
+
body=self.model_dump_json().encode("utf-8"),
|
|
81
|
+
headers=self.get_headers(),
|
|
82
|
+
content_type="application/json",
|
|
83
|
+
content_encoding="utf-8",
|
|
84
|
+
delivery_mode=DeliveryMode.PERSISTENT,
|
|
85
|
+
priority=0,
|
|
86
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Provides the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from aio_pika import connect_robust # pyright: ignore[reportUnknownMemberType]
|
|
6
|
+
from aio_pika.abc import AbstractRobustConnection
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
from opentelemetry.instrumentation.aio_pika import AioPikaInstrumentor # pyright: ignore[reportMissingTypeStubs]
|
|
9
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from structlog.stdlib import BoundLogger, get_logger
|
|
12
|
+
|
|
13
|
+
from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
|
|
14
|
+
|
|
15
|
+
from .configs import AiopikaConfig, build_config_from_package
|
|
16
|
+
from .depends import DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY
|
|
17
|
+
from .exceptions import AiopikaPluginBaseError
|
|
18
|
+
|
|
19
|
+
_logger: BoundLogger = get_logger(__package__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AiopikaPlugin(PluginAbstract):
|
|
23
|
+
"""Aiopika plugin."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, aiopika_config: AiopikaConfig | None = None) -> None:
|
|
26
|
+
"""Initialize the Aiopika plugin."""
|
|
27
|
+
super().__init__()
|
|
28
|
+
self._aiopika_config: AiopikaConfig | None = aiopika_config
|
|
29
|
+
self._robust_connection: AbstractRobustConnection | None = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def robust_connection(self) -> AbstractRobustConnection:
|
|
33
|
+
"""Get the robust connection."""
|
|
34
|
+
assert self._robust_connection is not None
|
|
35
|
+
return self._robust_connection
|
|
36
|
+
|
|
37
|
+
def on_load(self) -> None:
|
|
38
|
+
"""On load."""
|
|
39
|
+
assert self._application is not None
|
|
40
|
+
|
|
41
|
+
# Build the configuration if not provided
|
|
42
|
+
if self._aiopika_config is None:
|
|
43
|
+
self._aiopika_config = build_config_from_package(package_name=self._application.PACKAGE_NAME)
|
|
44
|
+
|
|
45
|
+
async def on_startup(self) -> None:
|
|
46
|
+
"""On startup."""
|
|
47
|
+
assert self._application is not None
|
|
48
|
+
assert self._aiopika_config is not None
|
|
49
|
+
|
|
50
|
+
tracer_provider: TracerProvider | None = cast(
|
|
51
|
+
TracerProvider | None, getattr(self._application.get_asgi_app().state, "tracer_provider", None)
|
|
52
|
+
)
|
|
53
|
+
meter_provider: MeterProvider | None = cast(
|
|
54
|
+
MeterProvider | None, getattr(self._application.get_asgi_app().state, "meter_provider", None)
|
|
55
|
+
)
|
|
56
|
+
if tracer_provider is None or meter_provider is None:
|
|
57
|
+
raise AiopikaPluginBaseError("Tracer provider or meter provider not found in the application state.")
|
|
58
|
+
|
|
59
|
+
AioPikaInstrumentor().instrument(
|
|
60
|
+
tracer_provider=tracer_provider,
|
|
61
|
+
meter_provider=meter_provider,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self._robust_connection = await connect_robust(url=str(self._aiopika_config.amqp_url))
|
|
65
|
+
self._add_to_state(key=DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY, value=self._robust_connection)
|
|
66
|
+
_logger.debug("Aiopika plugin connected to the AMQP server.", amqp_url=self._aiopika_config.amqp_url)
|
|
67
|
+
|
|
68
|
+
async def on_shutdown(self) -> None:
|
|
69
|
+
"""On shutdown."""
|
|
70
|
+
if self._robust_connection is not None:
|
|
71
|
+
await self._robust_connection.close()
|
|
72
|
+
_logger.debug("Aiopika plugin shutdown.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def depends_robust_connection(request: Request) -> AbstractRobustConnection:
|
|
76
|
+
"""Depends on the robust connection.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
request (Request): The request.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
AbstractRobustConnection: The robust connection.
|
|
83
|
+
"""
|
|
84
|
+
return request.app.state.robust_connection
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Provides the abstract class for the publisher port for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, ClassVar, Generic, Self, TypeVar
|
|
4
|
+
|
|
5
|
+
from aio_pika.abc import TimeoutType
|
|
6
|
+
from aio_pika.message import Message
|
|
7
|
+
from aiormq.abc import ConfirmationFrameType, DeliveredMessage
|
|
8
|
+
from pamqp.commands import Basic
|
|
9
|
+
|
|
10
|
+
from ..abstract import AbstractAiopikaResource
|
|
11
|
+
from ..exceptions import AiopikaPluginBaseError
|
|
12
|
+
from ..exchange import Exchange
|
|
13
|
+
from ..message import AbstractMessage
|
|
14
|
+
|
|
15
|
+
GenericMessage = TypeVar("GenericMessage", bound=AbstractMessage[Any])
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AbstractPublisher(AbstractAiopikaResource, Generic[GenericMessage]):
|
|
19
|
+
"""Abstract class for the publisher port for the Aiopika plugin."""
|
|
20
|
+
|
|
21
|
+
DEFAULT_OPERATION_TIMEOUT: ClassVar[TimeoutType] = 10.0
|
|
22
|
+
|
|
23
|
+
def __init__(self, exchange: Exchange, name: str | None = None) -> None:
|
|
24
|
+
"""Initialize the publisher port."""
|
|
25
|
+
super().__init__()
|
|
26
|
+
self._name: str = name or self.__class__.__name__
|
|
27
|
+
self._exchange: Exchange = exchange
|
|
28
|
+
|
|
29
|
+
async def setup(self) -> Self:
|
|
30
|
+
"""Setup the publisher."""
|
|
31
|
+
await super().setup()
|
|
32
|
+
await self._exchange.setup()
|
|
33
|
+
return self
|
|
34
|
+
|
|
35
|
+
async def publish(self, message: GenericMessage, routing_key: str) -> None:
|
|
36
|
+
"""Publish a message."""
|
|
37
|
+
# Transform the message to an Aiopika message
|
|
38
|
+
aiopika_message: Message
|
|
39
|
+
try:
|
|
40
|
+
aiopika_message = message.to_aiopika_message()
|
|
41
|
+
except Exception as exception:
|
|
42
|
+
raise AiopikaPluginBaseError(
|
|
43
|
+
message="Failed to convert the message to an Aiopika message.",
|
|
44
|
+
) from exception
|
|
45
|
+
# Publish the message
|
|
46
|
+
confirmation: ConfirmationFrameType | DeliveredMessage | None
|
|
47
|
+
try:
|
|
48
|
+
confirmation = await self._exchange.exchange.publish( # pyright: ignore
|
|
49
|
+
message=aiopika_message,
|
|
50
|
+
routing_key=routing_key,
|
|
51
|
+
mandatory=True,
|
|
52
|
+
timeout=self.DEFAULT_OPERATION_TIMEOUT,
|
|
53
|
+
)
|
|
54
|
+
except Exception as exception:
|
|
55
|
+
raise AiopikaPluginBaseError(
|
|
56
|
+
message="Failed to publish the message.",
|
|
57
|
+
) from exception
|
|
58
|
+
|
|
59
|
+
if confirmation is None:
|
|
60
|
+
raise AiopikaPluginBaseError(
|
|
61
|
+
message="Failed to publish the message.",
|
|
62
|
+
)
|
|
63
|
+
if isinstance(confirmation, Basic.Return):
|
|
64
|
+
raise AiopikaPluginBaseError(
|
|
65
|
+
message="Failed to publish the message.",
|
|
66
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Provides the queue for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, Self
|
|
4
|
+
|
|
5
|
+
from aio_pika.abc import AbstractQueue, TimeoutType
|
|
6
|
+
|
|
7
|
+
from .abstract import AbstractAiopikaResource
|
|
8
|
+
from .exceptions import AiopikaPluginBaseError, AiopikaPluginQueueNotDeclaredError
|
|
9
|
+
from .exchange import Exchange
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Queue(AbstractAiopikaResource):
|
|
13
|
+
"""Queue."""
|
|
14
|
+
|
|
15
|
+
DEFAULT_OPERATION_TIMEOUT: ClassVar[TimeoutType] = 10.0
|
|
16
|
+
|
|
17
|
+
def __init__( # pylint: disable=too-many-arguments # noqa: PLR0913
|
|
18
|
+
self,
|
|
19
|
+
name: str,
|
|
20
|
+
exchange: Exchange,
|
|
21
|
+
routing_key: str,
|
|
22
|
+
durable: bool = True,
|
|
23
|
+
auto_delete: bool = False,
|
|
24
|
+
exclusive: bool = True,
|
|
25
|
+
timeout: TimeoutType = DEFAULT_OPERATION_TIMEOUT,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize the queue."""
|
|
28
|
+
super().__init__()
|
|
29
|
+
# Initialize the queue properties
|
|
30
|
+
self._name: str = name
|
|
31
|
+
self._routing_key: str = routing_key
|
|
32
|
+
self._durable: bool = durable
|
|
33
|
+
self._auto_delete: bool = auto_delete
|
|
34
|
+
self._exclusive: bool = exclusive
|
|
35
|
+
self._timeout: TimeoutType = timeout
|
|
36
|
+
# Behavior properties
|
|
37
|
+
self._exchange: Exchange = exchange
|
|
38
|
+
self._queue: AbstractQueue | None = None
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def queue(self) -> AbstractQueue:
|
|
42
|
+
"""Get the Aiopika queue."""
|
|
43
|
+
if self._queue is None:
|
|
44
|
+
raise AiopikaPluginQueueNotDeclaredError(
|
|
45
|
+
message="Queue not declared.",
|
|
46
|
+
queue=self._name,
|
|
47
|
+
)
|
|
48
|
+
return self._queue
|
|
49
|
+
|
|
50
|
+
async def _declare(self) -> Self:
|
|
51
|
+
"""Declare the queue."""
|
|
52
|
+
try:
|
|
53
|
+
self._queue = await self._channel.declare_queue( # pyright: ignore
|
|
54
|
+
name=self._name,
|
|
55
|
+
durable=self._durable,
|
|
56
|
+
auto_delete=self._auto_delete,
|
|
57
|
+
exclusive=self._exclusive,
|
|
58
|
+
timeout=self._timeout,
|
|
59
|
+
)
|
|
60
|
+
except Exception as exception:
|
|
61
|
+
raise AiopikaPluginBaseError(
|
|
62
|
+
message="Failed to declare the queue.",
|
|
63
|
+
) from exception
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
async def _bind(self) -> Self:
|
|
67
|
+
"""Bind the queue to the exchange."""
|
|
68
|
+
try:
|
|
69
|
+
await self._queue.bind( # pyright: ignore
|
|
70
|
+
exchange=self._exchange.exchange,
|
|
71
|
+
routing_key=self._routing_key,
|
|
72
|
+
timeout=self._timeout,
|
|
73
|
+
)
|
|
74
|
+
except Exception as exception:
|
|
75
|
+
raise AiopikaPluginBaseError(
|
|
76
|
+
message="Failed to bind the queue to the exchange.",
|
|
77
|
+
) from exception
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
async def setup(self) -> Self:
|
|
81
|
+
"""Setup the queue."""
|
|
82
|
+
await super().setup()
|
|
83
|
+
if self._queue is None:
|
|
84
|
+
await self._declare()
|
|
85
|
+
await self._bind()
|
|
86
|
+
return self
|