fastapi-factory-utilities 0.5.0__py3-none-any.whl → 0.6.0__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/v1/sys/health.py +1 -1
- fastapi_factory_utilities/core/app/application.py +22 -26
- fastapi_factory_utilities/core/app/builder.py +8 -32
- fastapi_factory_utilities/core/app/fastapi_builder.py +3 -3
- 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 +23 -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/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 +63 -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 +11 -156
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -121
- 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/protocols.py +1 -54
- fastapi_factory_utilities/core/security/jwt.py +3 -3
- fastapi_factory_utilities/example/app.py +13 -4
- {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/METADATA +4 -2
- {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/RECORD +34 -23
- 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 -189
- 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.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/WHEEL +0 -0
- {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/entry_points.txt +0 -0
- {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,63 @@
|
|
|
1
|
+
"""Provides the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from aio_pika import connect_robust
|
|
4
|
+
from aio_pika.abc import AbstractRobustConnection
|
|
5
|
+
from fastapi import Request
|
|
6
|
+
from structlog.stdlib import BoundLogger, get_logger
|
|
7
|
+
|
|
8
|
+
from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
|
|
9
|
+
|
|
10
|
+
from .configs import AiopikaConfig, build_config_from_package
|
|
11
|
+
|
|
12
|
+
_logger: BoundLogger = get_logger(__package__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AiopikaPlugin(PluginAbstract):
|
|
16
|
+
"""Aiopika plugin."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, aiopika_config: AiopikaConfig | None = None) -> None:
|
|
19
|
+
"""Initialize the Aiopika plugin."""
|
|
20
|
+
super().__init__()
|
|
21
|
+
self._aiopika_config: AiopikaConfig | None = aiopika_config
|
|
22
|
+
self._robust_connection: AbstractRobustConnection | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def robust_connection(self) -> AbstractRobustConnection:
|
|
26
|
+
"""Get the robust connection."""
|
|
27
|
+
assert self._robust_connection is not None
|
|
28
|
+
return self._robust_connection
|
|
29
|
+
|
|
30
|
+
def on_load(self) -> None:
|
|
31
|
+
"""On load."""
|
|
32
|
+
assert self._application is not None
|
|
33
|
+
|
|
34
|
+
# Build the configuration if not provided
|
|
35
|
+
if self._aiopika_config is None:
|
|
36
|
+
self._aiopika_config = build_config_from_package(package_name=self._application.PACKAGE_NAME)
|
|
37
|
+
|
|
38
|
+
async def on_startup(self) -> None:
|
|
39
|
+
"""On startup."""
|
|
40
|
+
assert self._application is not None
|
|
41
|
+
assert self._aiopika_config is not None
|
|
42
|
+
|
|
43
|
+
self._robust_connection = await connect_robust(url=str(self._aiopika_config.amqp_url))
|
|
44
|
+
self._add_to_state(key="robust_connection", value=self._robust_connection)
|
|
45
|
+
_logger.debug("Aiopika plugin connected to the AMQP server.", amqp_url=self._aiopika_config.amqp_url)
|
|
46
|
+
|
|
47
|
+
async def on_shutdown(self) -> None:
|
|
48
|
+
"""On shutdown."""
|
|
49
|
+
if self._robust_connection is not None:
|
|
50
|
+
await self._robust_connection.close()
|
|
51
|
+
_logger.debug("Aiopika plugin shutdown.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def depends_robust_connection(request: Request) -> AbstractRobustConnection:
|
|
55
|
+
"""Depends on the robust connection.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
request (Request): The request.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
AbstractRobustConnection: The robust connection.
|
|
62
|
+
"""
|
|
63
|
+
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
|
|
@@ -1,168 +1,23 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""ODM Plugin Module."""
|
|
2
2
|
|
|
3
|
-
from logging import INFO, Logger, getLogger
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
from beanie import init_beanie # pyright: ignore[reportUnknownVariableType]
|
|
7
|
-
from motor.motor_asyncio import AsyncIOMotorClient
|
|
8
|
-
from reactivex import Subject
|
|
9
|
-
from structlog.stdlib import BoundLogger, get_logger
|
|
10
|
-
|
|
11
|
-
from fastapi_factory_utilities.core.plugins import PluginState
|
|
12
|
-
from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
|
|
13
|
-
from fastapi_factory_utilities.core.services.status.enums import (
|
|
14
|
-
ComponentTypeEnum,
|
|
15
|
-
HealthStatusEnum,
|
|
16
|
-
ReadinessStatusEnum,
|
|
17
|
-
)
|
|
18
|
-
from fastapi_factory_utilities.core.services.status.services import StatusService
|
|
19
|
-
from fastapi_factory_utilities.core.services.status.types import (
|
|
20
|
-
ComponentInstanceType,
|
|
21
|
-
Status,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
from .builder import ODMBuilder
|
|
25
3
|
from .depends import depends_odm_client, depends_odm_database
|
|
26
4
|
from .documents import BaseDocument
|
|
27
|
-
from .exceptions import
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
ODMPluginBaseException,
|
|
7
|
+
ODMPluginConfigError,
|
|
8
|
+
OperationError,
|
|
9
|
+
UnableToCreateEntityDueToDuplicateKeyError,
|
|
10
|
+
)
|
|
28
11
|
from .helpers import PersistedEntity
|
|
12
|
+
from .plugins import ODMPlugin
|
|
29
13
|
from .repositories import AbstractRepository
|
|
30
14
|
|
|
31
|
-
_logger: BoundLogger = get_logger()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def pre_conditions_check(application: ApplicationAbstractProtocol) -> bool:
|
|
35
|
-
"""Check the pre-conditions for the OpenTelemetry plugin.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
application (BaseApplicationProtocol): The application.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
bool: True if the pre-conditions are met, False otherwise.
|
|
42
|
-
"""
|
|
43
|
-
del application
|
|
44
|
-
return True
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def on_load(
|
|
48
|
-
application: ApplicationAbstractProtocol,
|
|
49
|
-
) -> list["PluginState"] | None:
|
|
50
|
-
"""Actions to perform on load for the OpenTelemetry plugin.
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
application (BaseApplicationProtocol): The application.
|
|
54
|
-
"""
|
|
55
|
-
del application
|
|
56
|
-
# Configure the pymongo logger to INFO level
|
|
57
|
-
pymongo_logger: Logger = getLogger("pymongo")
|
|
58
|
-
pymongo_logger.setLevel(INFO)
|
|
59
|
-
_logger.debug("ODM plugin loaded.")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
async def on_startup(
|
|
63
|
-
application: ApplicationAbstractProtocol,
|
|
64
|
-
) -> list["PluginState"] | None:
|
|
65
|
-
"""Actions to perform on startup for the ODM plugin.
|
|
66
|
-
|
|
67
|
-
Args:
|
|
68
|
-
application (BaseApplicationProtocol): The application.
|
|
69
|
-
odm_config (ODMConfig): The ODM configuration.
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
None
|
|
73
|
-
"""
|
|
74
|
-
states: list[PluginState] = []
|
|
75
|
-
|
|
76
|
-
status_service: StatusService = application.get_status_service()
|
|
77
|
-
component_instance: ComponentInstanceType = ComponentInstanceType(
|
|
78
|
-
component_type=ComponentTypeEnum.DATABASE, identifier="MongoDB"
|
|
79
|
-
)
|
|
80
|
-
monitoring_subject: Subject[Status] = status_service.register_component_instance(
|
|
81
|
-
component_instance=component_instance
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
odm_factory: ODMBuilder = ODMBuilder(application=application).build_all()
|
|
86
|
-
await odm_factory.wait_ping()
|
|
87
|
-
except Exception as exception: # pylint: disable=broad-except
|
|
88
|
-
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
89
|
-
# TODO: Report the error to the status_service
|
|
90
|
-
# this will report the application as unhealthy
|
|
91
|
-
monitoring_subject.on_next(
|
|
92
|
-
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
93
|
-
)
|
|
94
|
-
return states
|
|
95
|
-
|
|
96
|
-
if odm_factory.odm_database is None or odm_factory.odm_client is None:
|
|
97
|
-
_logger.error(
|
|
98
|
-
f"ODM plugin failed to start. Database: {odm_factory.odm_database} - Client: {odm_factory.odm_client}"
|
|
99
|
-
)
|
|
100
|
-
# TODO: Report the error to the status_service
|
|
101
|
-
# this will report the application as unhealthy
|
|
102
|
-
monitoring_subject.on_next(
|
|
103
|
-
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
104
|
-
)
|
|
105
|
-
return states
|
|
106
|
-
|
|
107
|
-
# Add the ODM client and database to the application state
|
|
108
|
-
states.append(
|
|
109
|
-
PluginState(key="odm_client", value=odm_factory.odm_client),
|
|
110
|
-
)
|
|
111
|
-
states.append(
|
|
112
|
-
PluginState(
|
|
113
|
-
key="odm_database",
|
|
114
|
-
value=odm_factory.odm_database,
|
|
115
|
-
),
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
# TODO: Find a better way to initialize beanie with the document models of the concrete application
|
|
119
|
-
# through an hook in the application, a dynamis import ?
|
|
120
|
-
try:
|
|
121
|
-
await init_beanie(
|
|
122
|
-
database=odm_factory.odm_database,
|
|
123
|
-
document_models=application.ODM_DOCUMENT_MODELS,
|
|
124
|
-
)
|
|
125
|
-
except Exception as exception: # pylint: disable=broad-except
|
|
126
|
-
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
127
|
-
# TODO: Report the error to the status_service
|
|
128
|
-
# this will report the application as unhealthy
|
|
129
|
-
monitoring_subject.on_next(
|
|
130
|
-
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
131
|
-
)
|
|
132
|
-
return states
|
|
133
|
-
|
|
134
|
-
_logger.info(
|
|
135
|
-
f"ODM plugin started. Database: {odm_factory.odm_database.name} - "
|
|
136
|
-
f"Client: {odm_factory.odm_client.address} - "
|
|
137
|
-
f"Document models: {application.ODM_DOCUMENT_MODELS}"
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
monitoring_subject.on_next(value=Status(health=HealthStatusEnum.HEALTHY, readiness=ReadinessStatusEnum.READY))
|
|
141
|
-
|
|
142
|
-
return states
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
async def on_shutdown(application: ApplicationAbstractProtocol) -> None:
|
|
146
|
-
"""Actions to perform on shutdown for the ODM plugin.
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
application (BaseApplicationProtocol): The application.
|
|
150
|
-
|
|
151
|
-
Returns:
|
|
152
|
-
None
|
|
153
|
-
"""
|
|
154
|
-
# Skip if the ODM plugin was not started correctly
|
|
155
|
-
if not hasattr(application.get_asgi_app().state, "odm_client"):
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
client: AsyncIOMotorClient[Any] = application.get_asgi_app().state.odm_client
|
|
159
|
-
client.close()
|
|
160
|
-
_logger.debug("ODM plugin shutdown.")
|
|
161
|
-
|
|
162
|
-
|
|
163
15
|
__all__: list[str] = [
|
|
164
16
|
"AbstractRepository",
|
|
165
17
|
"BaseDocument",
|
|
18
|
+
"ODMPlugin",
|
|
19
|
+
"ODMPluginBaseException",
|
|
20
|
+
"ODMPluginConfigError",
|
|
166
21
|
"OperationError",
|
|
167
22
|
"PersistedEntity",
|
|
168
23
|
"UnableToCreateEntityDueToDuplicateKeyError",
|
|
@@ -13,7 +13,7 @@ class BaseDocument(Document):
|
|
|
13
13
|
"""Base document class."""
|
|
14
14
|
|
|
15
15
|
# To be agnostic of MongoDN, we use UUID as the document ID.
|
|
16
|
-
id: UUID = Field( #
|
|
16
|
+
id: UUID = Field( # type: ignore
|
|
17
17
|
default_factory=uuid4, description="The document ID."
|
|
18
18
|
)
|
|
19
19
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Oriented Data Model (ODM) plugin package."""
|
|
2
|
+
|
|
3
|
+
from logging import INFO, Logger, getLogger
|
|
4
|
+
from typing import Any, Self
|
|
5
|
+
|
|
6
|
+
from beanie import Document, init_beanie # pyright: ignore[reportUnknownVariableType]
|
|
7
|
+
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
8
|
+
from reactivex import Subject
|
|
9
|
+
from structlog.stdlib import BoundLogger, get_logger
|
|
10
|
+
|
|
11
|
+
from fastapi_factory_utilities.core.plugins.abstracts import PluginAbstract
|
|
12
|
+
from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
|
|
13
|
+
from fastapi_factory_utilities.core.services.status.enums import (
|
|
14
|
+
ComponentTypeEnum,
|
|
15
|
+
HealthStatusEnum,
|
|
16
|
+
ReadinessStatusEnum,
|
|
17
|
+
)
|
|
18
|
+
from fastapi_factory_utilities.core.services.status.services import StatusService
|
|
19
|
+
from fastapi_factory_utilities.core.services.status.types import (
|
|
20
|
+
ComponentInstanceType,
|
|
21
|
+
Status,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .builder import ODMBuilder
|
|
25
|
+
from .configs import ODMConfig
|
|
26
|
+
from .depends import depends_odm_client, depends_odm_database
|
|
27
|
+
from .documents import BaseDocument
|
|
28
|
+
from .exceptions import OperationError, UnableToCreateEntityDueToDuplicateKeyError
|
|
29
|
+
from .helpers import PersistedEntity
|
|
30
|
+
from .repositories import AbstractRepository
|
|
31
|
+
|
|
32
|
+
_logger: BoundLogger = get_logger()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ODMPlugin(PluginAbstract):
|
|
36
|
+
"""ODM plugin."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self, document_models: list[type[Document]] | None = None, odm_config: ODMConfig | None = None
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Initialize the ODM plugin."""
|
|
42
|
+
super().__init__()
|
|
43
|
+
self._component_instance: ComponentInstanceType | None = None
|
|
44
|
+
self._monitoring_subject: Subject[Status] | None = None
|
|
45
|
+
self._document_models: list[type[Document]] | None = document_models
|
|
46
|
+
self._odm_config: ODMConfig | None = odm_config
|
|
47
|
+
self._odm_client: AsyncIOMotorClient[Any] | None = None
|
|
48
|
+
self._odm_database: AsyncIOMotorDatabase[Any] | None = None
|
|
49
|
+
|
|
50
|
+
def set_application(self, application: ApplicationAbstractProtocol) -> Self:
|
|
51
|
+
"""Set the application."""
|
|
52
|
+
self._document_models = self._document_models or application.ODM_DOCUMENT_MODELS
|
|
53
|
+
return super().set_application(application)
|
|
54
|
+
|
|
55
|
+
def on_load(self) -> None:
|
|
56
|
+
"""Actions to perform on load for the ODM plugin."""
|
|
57
|
+
# Configure the pymongo logger to INFO level
|
|
58
|
+
|
|
59
|
+
pymongo_logger: Logger = getLogger("pymongo")
|
|
60
|
+
pymongo_logger.setLevel(INFO)
|
|
61
|
+
_logger.debug("ODM plugin loaded.")
|
|
62
|
+
|
|
63
|
+
def _setup_status(self) -> None:
|
|
64
|
+
assert self._application is not None
|
|
65
|
+
status_service: StatusService = self._application.get_status_service()
|
|
66
|
+
self._component_instance = ComponentInstanceType(
|
|
67
|
+
component_type=ComponentTypeEnum.DATABASE, identifier="MongoDB"
|
|
68
|
+
)
|
|
69
|
+
self._monitoring_subject = status_service.register_component_instance(
|
|
70
|
+
component_instance=self._component_instance
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def _setup_beanie(self) -> None:
|
|
74
|
+
assert self._application is not None
|
|
75
|
+
assert self._odm_database is not None
|
|
76
|
+
assert self._document_models is not None
|
|
77
|
+
assert self._monitoring_subject is not None
|
|
78
|
+
# TODO: Find a better way to initialize beanie with the document models of the concrete application
|
|
79
|
+
# through an hook in the application, a dynamis import ?
|
|
80
|
+
try:
|
|
81
|
+
await init_beanie(
|
|
82
|
+
database=self._odm_database,
|
|
83
|
+
document_models=self._document_models,
|
|
84
|
+
)
|
|
85
|
+
except Exception as exception: # pylint: disable=broad-except
|
|
86
|
+
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
87
|
+
# TODO: Report the error to the status_service
|
|
88
|
+
# this will report the application as unhealthy
|
|
89
|
+
self._monitoring_subject.on_next(
|
|
90
|
+
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def on_startup(self) -> None:
|
|
94
|
+
"""Actions to perform on startup for the ODM plugin."""
|
|
95
|
+
assert self._application is not None
|
|
96
|
+
self._setup_status()
|
|
97
|
+
assert self._monitoring_subject is not None
|
|
98
|
+
assert self._component_instance is not None
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
odm_factory: ODMBuilder = ODMBuilder(application=self._application, odm_config=self._odm_config).build_all()
|
|
102
|
+
await odm_factory.wait_ping()
|
|
103
|
+
self._odm_database = odm_factory.odm_database
|
|
104
|
+
self._odm_client = odm_factory.odm_client
|
|
105
|
+
except Exception as exception: # pylint: disable=broad-except
|
|
106
|
+
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
107
|
+
# TODO: Report the error to the status_service
|
|
108
|
+
# this will report the application as unhealthy
|
|
109
|
+
self._monitoring_subject.on_next(
|
|
110
|
+
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
if self._odm_database is None or self._odm_client is None:
|
|
115
|
+
_logger.error(
|
|
116
|
+
f"ODM plugin failed to start. Database: {odm_factory.odm_database} - Client: {odm_factory.odm_client}"
|
|
117
|
+
)
|
|
118
|
+
# TODO: Report the error to the status_service
|
|
119
|
+
# this will report the application as unhealthy
|
|
120
|
+
self._monitoring_subject.on_next(
|
|
121
|
+
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
122
|
+
)
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
self._add_to_state(key="odm_client", value=odm_factory.odm_client)
|
|
126
|
+
self._add_to_state(key="odm_database", value=odm_factory.odm_database)
|
|
127
|
+
|
|
128
|
+
await self._setup_beanie()
|
|
129
|
+
|
|
130
|
+
_logger.info(
|
|
131
|
+
f"ODM plugin started. Database: {self._odm_database.name} - "
|
|
132
|
+
f"Client: {self._odm_client.address} - "
|
|
133
|
+
f"Document models: {self._application.ODM_DOCUMENT_MODELS}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self._monitoring_subject.on_next(
|
|
137
|
+
value=Status(health=HealthStatusEnum.HEALTHY, readiness=ReadinessStatusEnum.READY)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
async def on_shutdown(self) -> None:
|
|
141
|
+
"""Actions to perform on shutdown for the ODM plugin."""
|
|
142
|
+
if self._odm_client is not None:
|
|
143
|
+
self._odm_client.close()
|
|
144
|
+
_logger.debug("ODM plugin shutdown.")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__: list[str] = [
|
|
148
|
+
"AbstractRepository",
|
|
149
|
+
"BaseDocument",
|
|
150
|
+
"OperationError",
|
|
151
|
+
"PersistedEntity",
|
|
152
|
+
"UnableToCreateEntityDueToDuplicateKeyError",
|
|
153
|
+
"depends_odm_client",
|
|
154
|
+
"depends_odm_database",
|
|
155
|
+
]
|