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.

Files changed (39) hide show
  1. fastapi_factory_utilities/core/api/v1/sys/health.py +1 -1
  2. fastapi_factory_utilities/core/app/application.py +22 -26
  3. fastapi_factory_utilities/core/app/builder.py +8 -32
  4. fastapi_factory_utilities/core/app/fastapi_builder.py +3 -3
  5. fastapi_factory_utilities/core/plugins/__init__.py +2 -31
  6. fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
  7. fastapi_factory_utilities/core/plugins/aiopika/__init__.py +23 -0
  8. fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
  9. fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
  10. fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
  11. fastapi_factory_utilities/core/plugins/aiopika/exchange.py +70 -0
  12. fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
  13. fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
  14. fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
  15. fastapi_factory_utilities/core/plugins/aiopika/plugins.py +63 -0
  16. fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
  17. fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
  18. fastapi_factory_utilities/core/plugins/aiopika/queue.py +86 -0
  19. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +11 -156
  20. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +1 -1
  21. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
  22. fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +1 -1
  23. fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
  24. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -121
  25. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
  26. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
  27. fastapi_factory_utilities/core/protocols.py +1 -54
  28. fastapi_factory_utilities/core/security/jwt.py +3 -3
  29. fastapi_factory_utilities/example/app.py +13 -4
  30. {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/METADATA +4 -2
  31. {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/RECORD +34 -23
  32. fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
  33. fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
  34. fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -189
  35. fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
  36. fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
  37. {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/WHEEL +0 -0
  38. {fastapi_factory_utilities-0.5.0.dist-info → fastapi_factory_utilities-0.6.0.dist-info}/entry_points.txt +0 -0
  39. {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,7 @@
1
+ """Provides the publisher ports for the Aiopika plugin."""
2
+
3
+ from .abstract import AbstractPublisher
4
+
5
+ __all__: list[str] = [
6
+ "AbstractPublisher",
7
+ ]
@@ -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
- """Oriented Data Model (ODM) plugin package."""
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 OperationError, UnableToCreateEntityDueToDuplicateKeyError
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",
@@ -242,7 +242,7 @@ class ODMBuilder:
242
242
 
243
243
  return self
244
244
 
245
- async def wait_ping(self):
245
+ async def wait_ping(self) -> None:
246
246
  """Wait for the ODM client to be ready.
247
247
 
248
248
  Returns:
@@ -12,4 +12,4 @@ class ODMConfig(BaseModel):
12
12
 
13
13
  database: str = "test"
14
14
 
15
- connection_timeout_ms: int = 1000
15
+ connection_timeout_ms: int = 4000
@@ -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( # pyright: ignore[reportIncompatibleVariableOverride]
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
+ ]