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
|
@@ -1,154 +1,26 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
from
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
1
|
+
"""ODM Plugin Module."""
|
|
2
|
+
|
|
3
|
+
from .depends import depends_odm_client, depends_odm_database
|
|
4
|
+
from .documents import BaseDocument
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
ODMPluginBaseException,
|
|
7
|
+
ODMPluginConfigError,
|
|
8
|
+
OperationError,
|
|
9
|
+
UnableToCreateEntityDueToDuplicateKeyError,
|
|
22
10
|
)
|
|
23
|
-
|
|
24
|
-
from .
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
""
|
|
38
|
-
|
|
39
|
-
return True
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def on_load(
|
|
43
|
-
application: ApplicationAbstractProtocol,
|
|
44
|
-
) -> list["PluginState"] | None:
|
|
45
|
-
"""Actions to perform on load for the OpenTelemetry plugin.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
application (BaseApplicationProtocol): The application.
|
|
49
|
-
"""
|
|
50
|
-
del application
|
|
51
|
-
# Configure the pymongo logger to INFO level
|
|
52
|
-
pymongo_logger: Logger = getLogger("pymongo")
|
|
53
|
-
pymongo_logger.setLevel(INFO)
|
|
54
|
-
_logger.debug("ODM plugin loaded.")
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
async def on_startup(
|
|
58
|
-
application: ApplicationAbstractProtocol,
|
|
59
|
-
) -> list["PluginState"] | None:
|
|
60
|
-
"""Actions to perform on startup for the ODM plugin.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
application (BaseApplicationProtocol): The application.
|
|
64
|
-
odm_config (ODMConfig): The ODM configuration.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
None
|
|
68
|
-
"""
|
|
69
|
-
states: list[PluginState] = []
|
|
70
|
-
|
|
71
|
-
status_service: StatusService = application.get_status_service()
|
|
72
|
-
component_instance: ComponentInstanceType = ComponentInstanceType(
|
|
73
|
-
component_type=ComponentTypeEnum.DATABASE, identifier="MongoDB"
|
|
74
|
-
)
|
|
75
|
-
monitoring_subject: Subject[Status] = status_service.register_component_instance(
|
|
76
|
-
component_instance=component_instance
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
try:
|
|
80
|
-
odm_factory: ODMBuilder = ODMBuilder(application=application).build_all()
|
|
81
|
-
except Exception as exception: # pylint: disable=broad-except
|
|
82
|
-
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
83
|
-
# TODO: Report the error to the status_service
|
|
84
|
-
# this will report the application as unhealthy
|
|
85
|
-
monitoring_subject.on_next(
|
|
86
|
-
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
87
|
-
)
|
|
88
|
-
return states
|
|
89
|
-
|
|
90
|
-
if odm_factory.odm_database is None or odm_factory.odm_client is None:
|
|
91
|
-
_logger.error(
|
|
92
|
-
f"ODM plugin failed to start. Database: {odm_factory.odm_database} - " f"Client: {odm_factory.odm_client}"
|
|
93
|
-
)
|
|
94
|
-
# TODO: Report the error to the status_service
|
|
95
|
-
# this will report the application as unhealthy
|
|
96
|
-
monitoring_subject.on_next(
|
|
97
|
-
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
98
|
-
)
|
|
99
|
-
return states
|
|
100
|
-
|
|
101
|
-
# Add the ODM client and database to the application state
|
|
102
|
-
states.append(
|
|
103
|
-
PluginState(key="odm_client", value=odm_factory.odm_client),
|
|
104
|
-
)
|
|
105
|
-
states.append(
|
|
106
|
-
PluginState(
|
|
107
|
-
key="odm_database",
|
|
108
|
-
value=odm_factory.odm_database,
|
|
109
|
-
),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
# TODO: Find a better way to initialize beanie with the document models of the concrete application
|
|
113
|
-
# through an hook in the application, a dynamis import ?
|
|
114
|
-
try:
|
|
115
|
-
await init_beanie(
|
|
116
|
-
database=odm_factory.odm_database,
|
|
117
|
-
document_models=application.ODM_DOCUMENT_MODELS,
|
|
118
|
-
)
|
|
119
|
-
except Exception as exception: # pylint: disable=broad-except
|
|
120
|
-
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
121
|
-
# TODO: Report the error to the status_service
|
|
122
|
-
# this will report the application as unhealthy
|
|
123
|
-
monitoring_subject.on_next(
|
|
124
|
-
value=Status(health=HealthStatusEnum.UNHEALTHY, readiness=ReadinessStatusEnum.NOT_READY)
|
|
125
|
-
)
|
|
126
|
-
return states
|
|
127
|
-
|
|
128
|
-
_logger.info(
|
|
129
|
-
f"ODM plugin started. Database: {odm_factory.odm_database.name} - "
|
|
130
|
-
f"Client: {odm_factory.odm_client.address} - "
|
|
131
|
-
f"Document models: {application.ODM_DOCUMENT_MODELS}"
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
monitoring_subject.on_next(value=Status(health=HealthStatusEnum.HEALTHY, readiness=ReadinessStatusEnum.READY))
|
|
135
|
-
|
|
136
|
-
return states
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
async def on_shutdown(application: ApplicationAbstractProtocol) -> None:
|
|
140
|
-
"""Actions to perform on shutdown for the ODM plugin.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
application (BaseApplicationProtocol): The application.
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
None
|
|
147
|
-
"""
|
|
148
|
-
# Skip if the ODM plugin was not started correctly
|
|
149
|
-
if not hasattr(application.get_asgi_app().state, "odm_client"):
|
|
150
|
-
return
|
|
151
|
-
|
|
152
|
-
client: AsyncIOMotorClient[Any] = application.get_asgi_app().state.odm_client
|
|
153
|
-
client.close()
|
|
154
|
-
_logger.debug("ODM plugin shutdown.")
|
|
11
|
+
from .helpers import PersistedEntity
|
|
12
|
+
from .plugins import ODMPlugin
|
|
13
|
+
from .repositories import AbstractRepository
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = [
|
|
16
|
+
"AbstractRepository",
|
|
17
|
+
"BaseDocument",
|
|
18
|
+
"ODMPlugin",
|
|
19
|
+
"ODMPluginBaseException",
|
|
20
|
+
"ODMPluginConfigError",
|
|
21
|
+
"OperationError",
|
|
22
|
+
"PersistedEntity",
|
|
23
|
+
"UnableToCreateEntityDueToDuplicateKeyError",
|
|
24
|
+
"depends_odm_client",
|
|
25
|
+
"depends_odm_database",
|
|
26
|
+
]
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Provides the module for the ODM plugin."""
|
|
2
2
|
|
|
3
|
-
import time
|
|
4
3
|
from typing import Any, ClassVar, Self
|
|
5
4
|
|
|
5
|
+
from bson import CodecOptions
|
|
6
6
|
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
7
|
+
from pymongo.server_api import ServerApi, ServerApiVersion
|
|
7
8
|
from structlog.stdlib import get_logger
|
|
8
9
|
|
|
9
10
|
from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
|
|
@@ -124,31 +125,34 @@ class ODMBuilder:
|
|
|
124
125
|
raise ODMPluginConfigError("Unable to create the application configuration model.") from exception
|
|
125
126
|
return self
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
128
|
+
# ======
|
|
129
|
+
# KEEP IT, Waiting for additional tests
|
|
130
|
+
# @classmethod
|
|
131
|
+
# def _wait_client_to_be_ready(cls, client: AsyncIOMotorClient[Any], timeout_s: int) -> None:
|
|
132
|
+
# """Wait for the ODM client to be ready.
|
|
133
|
+
|
|
134
|
+
# Args:
|
|
135
|
+
# client (AsyncIOMotorClient): The ODM client.
|
|
136
|
+
# timeout_s (int): The timeout in seconds.
|
|
137
|
+
|
|
138
|
+
# Raises:
|
|
139
|
+
# TimeoutError: If the ODM client is not ready in the given timeout.
|
|
140
|
+
# """
|
|
141
|
+
# start_time: float = time.time()
|
|
142
|
+
# message_time: float = time.time()
|
|
143
|
+
# while (time.time() - start_time) < (timeout_s):
|
|
144
|
+
# if len(client.nodes) > 0: # type: ignore
|
|
145
|
+
# _logger.info(f"Waiting {(time.time() - start_time)*cls.MS_TO_S}ms for the ODM client to be ready.")
|
|
146
|
+
# return
|
|
147
|
+
|
|
148
|
+
# if (time.time() - message_time) > 1:
|
|
149
|
+
# elaps_time: float = time.time() - start_time
|
|
150
|
+
# _logger.debug(f"Waiting for the ODM client to be ready. (from {int(elaps_time)}s) ")
|
|
151
|
+
# message_time = time.time()
|
|
152
|
+
# time.sleep(cls.SLEEP_TIME_S)
|
|
153
|
+
|
|
154
|
+
# raise TimeoutError("The ODM client is not ready in the given timeout.")
|
|
155
|
+
# ======
|
|
152
156
|
|
|
153
157
|
def build_client(
|
|
154
158
|
self,
|
|
@@ -173,11 +177,14 @@ class ODMBuilder:
|
|
|
173
177
|
self._odm_client = AsyncIOMotorClient(
|
|
174
178
|
host=self._config.uri,
|
|
175
179
|
connect=True,
|
|
176
|
-
connectTimeoutMS=self._config.
|
|
177
|
-
serverSelectionTimeoutMS=self._config.
|
|
180
|
+
connectTimeoutMS=self._config.connection_timeout_ms,
|
|
181
|
+
serverSelectionTimeoutMS=self._config.connection_timeout_ms,
|
|
182
|
+
server_api=ServerApi(version=ServerApiVersion.V1),
|
|
183
|
+
tz_aware=True,
|
|
178
184
|
)
|
|
179
185
|
|
|
180
|
-
|
|
186
|
+
# KEEP IT, Waiting for additional tests
|
|
187
|
+
# self._wait_client_to_be_ready(client=self._odm_client, timeout_s=self._config.connection_timeout_s)
|
|
181
188
|
|
|
182
189
|
return self
|
|
183
190
|
|
|
@@ -208,10 +215,15 @@ class ODMBuilder:
|
|
|
208
215
|
|
|
209
216
|
if self._odm_client is None:
|
|
210
217
|
raise ODMPluginConfigError(
|
|
211
|
-
"ODM client is not set. Provide the ODM client using
|
|
218
|
+
"ODM client is not set. Provide the ODM client using build_client method or through parameter."
|
|
212
219
|
)
|
|
213
220
|
|
|
214
|
-
self._odm_database = self._odm_client.get_database(
|
|
221
|
+
self._odm_database = self._odm_client.get_database(
|
|
222
|
+
name=database_name,
|
|
223
|
+
codec_options=CodecOptions( # pyright: ignore[reportUnknownArgumentType]
|
|
224
|
+
tz_aware=True,
|
|
225
|
+
),
|
|
226
|
+
)
|
|
215
227
|
|
|
216
228
|
return self
|
|
217
229
|
|
|
@@ -229,3 +241,19 @@ class ODMBuilder:
|
|
|
229
241
|
self.build_database()
|
|
230
242
|
|
|
231
243
|
return self
|
|
244
|
+
|
|
245
|
+
async def wait_ping(self) -> None:
|
|
246
|
+
"""Wait for the ODM client to be ready.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Self: The ODM factory.
|
|
250
|
+
"""
|
|
251
|
+
if self._odm_client is None:
|
|
252
|
+
raise ODMPluginConfigError(
|
|
253
|
+
"ODM client is not set. Provide the ODM client using build_client method or through parameter."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
await self._odm_client.admin.command("ping")
|
|
258
|
+
except Exception as exception: # pylint: disable=broad-except
|
|
259
|
+
raise ODMPluginConfigError("Unable to ping the ODM client.") from exception
|
|
@@ -13,10 +13,11 @@ 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
|
|
|
20
|
+
revision_id: UUID | None = Field(default=None, exclude=False)
|
|
20
21
|
created_at: Annotated[datetime.datetime, Indexed(index_type=DESCENDING)] = Field( # pyright: ignore
|
|
21
22
|
default_factory=lambda: datetime.datetime.now(tz=datetime.UTC), description="Creation timestamp."
|
|
22
23
|
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Helper functions for ODM plugins."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PersistedEntity(BaseModel):
|
|
10
|
+
"""Base class for persisted entities."""
|
|
11
|
+
|
|
12
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
13
|
+
|
|
14
|
+
revision_id: uuid.UUID | None = Field(default=None)
|
|
15
|
+
created_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
|
16
|
+
updated_at: datetime.datetime = Field(default_factory=datetime.datetime.now)
|
|
@@ -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
|
+
]
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Provides the abstract classes for the repositories."""
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
from abc import ABC
|
|
4
|
-
from collections.abc import AsyncGenerator, Callable
|
|
5
|
+
from collections.abc import AsyncGenerator, Callable, Mapping
|
|
5
6
|
from contextlib import asynccontextmanager
|
|
6
7
|
from typing import Any, Generic, TypeVar, get_args
|
|
7
8
|
from uuid import UUID
|
|
8
9
|
|
|
10
|
+
from beanie import SortDirection
|
|
9
11
|
from motor.motor_asyncio import AsyncIOMotorClientSession, AsyncIOMotorDatabase
|
|
10
12
|
from pydantic import BaseModel
|
|
11
13
|
from pymongo.errors import DuplicateKeyError, PyMongoError
|
|
@@ -77,13 +79,18 @@ class AbstractRepository(ABC, Generic[DocumentGenericType, EntityGenericType]):
|
|
|
77
79
|
UnableToCreateEntityDueToDuplicateKeyError: If the entity cannot be created due to a duplicate key error.
|
|
78
80
|
OperationError: If the operation fails.
|
|
79
81
|
"""
|
|
82
|
+
insert_time: datetime.datetime = datetime.datetime.now(tz=datetime.UTC)
|
|
80
83
|
try:
|
|
81
|
-
|
|
84
|
+
entity_dump: dict[str, Any] = entity.model_dump()
|
|
85
|
+
entity_dump["created_at"] = insert_time
|
|
86
|
+
entity_dump["updated_at"] = insert_time
|
|
87
|
+
document: DocumentGenericType = self._document_type(**entity_dump)
|
|
88
|
+
|
|
82
89
|
except ValueError as error:
|
|
83
90
|
raise ValueError(f"Failed to create document from entity: {error}") from error
|
|
84
91
|
|
|
85
92
|
try:
|
|
86
|
-
document_created: DocumentGenericType = await document.
|
|
93
|
+
document_created: DocumentGenericType = await document.insert(session=session)
|
|
87
94
|
except DuplicateKeyError as error:
|
|
88
95
|
raise UnableToCreateEntityDueToDuplicateKeyError(f"Failed to insert document: {error}") from error
|
|
89
96
|
except PyMongoError as error:
|
|
@@ -96,6 +103,44 @@ class AbstractRepository(ABC, Generic[DocumentGenericType, EntityGenericType]):
|
|
|
96
103
|
|
|
97
104
|
return entity_created
|
|
98
105
|
|
|
106
|
+
@managed_session()
|
|
107
|
+
async def update(
|
|
108
|
+
self, entity: EntityGenericType, session: AsyncIOMotorClientSession | None = None
|
|
109
|
+
) -> EntityGenericType:
|
|
110
|
+
"""Update the entity in the database.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
entity (EntityGenericType): The entity to update.
|
|
114
|
+
session (AsyncIOMotorClientSession | None): The session to use. Defaults to None. (managed by decorator)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
EntityGenericType: The updated entity.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ValueError: If the entity cannot be created from the document.
|
|
121
|
+
OperationError: If the operation fails.
|
|
122
|
+
"""
|
|
123
|
+
update_time: datetime.datetime = datetime.datetime.now(tz=datetime.UTC)
|
|
124
|
+
try:
|
|
125
|
+
entity_dump: dict[str, Any] = entity.model_dump()
|
|
126
|
+
entity_dump["updated_at"] = update_time
|
|
127
|
+
document: DocumentGenericType = self._document_type(**entity_dump)
|
|
128
|
+
|
|
129
|
+
except ValueError as error:
|
|
130
|
+
raise ValueError(f"Failed to create document from entity: {error}") from error
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
document_updated: DocumentGenericType = await document.save(session=session)
|
|
134
|
+
except PyMongoError as error:
|
|
135
|
+
raise OperationError(f"Failed to update document: {error}") from error
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
entity_updated: EntityGenericType = self._entity_type(**document_updated.model_dump())
|
|
139
|
+
except ValueError as error:
|
|
140
|
+
raise ValueError(f"Failed to create entity from document: {error}") from error
|
|
141
|
+
|
|
142
|
+
return entity_updated
|
|
143
|
+
|
|
99
144
|
@managed_session()
|
|
100
145
|
async def get_one_by_id(
|
|
101
146
|
self,
|
|
@@ -170,3 +215,67 @@ class AbstractRepository(ABC, Generic[DocumentGenericType, EntityGenericType]):
|
|
|
170
215
|
return
|
|
171
216
|
|
|
172
217
|
raise OperationError("Failed to delete document.")
|
|
218
|
+
|
|
219
|
+
@managed_session()
|
|
220
|
+
async def find( # noqa: PLR0913
|
|
221
|
+
self,
|
|
222
|
+
*args: Mapping[str, Any] | bool,
|
|
223
|
+
projection_model: None = None,
|
|
224
|
+
skip: int | None = None,
|
|
225
|
+
limit: int | None = None,
|
|
226
|
+
sort: None | str | list[tuple[str, SortDirection]] = None,
|
|
227
|
+
session: AsyncIOMotorClientSession | None = None,
|
|
228
|
+
ignore_cache: bool = False,
|
|
229
|
+
fetch_links: bool = False,
|
|
230
|
+
lazy_parse: bool = False,
|
|
231
|
+
nesting_depth: int | None = None,
|
|
232
|
+
nesting_depths_per_field: dict[str, int] | None = None,
|
|
233
|
+
**pymongo_kwargs: Any,
|
|
234
|
+
) -> list[EntityGenericType]:
|
|
235
|
+
"""Find documents in the database.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
*args: The arguments to pass to the find method.
|
|
239
|
+
projection_model: The projection model to use.
|
|
240
|
+
skip: The number of documents to skip.
|
|
241
|
+
limit: The number of documents to return.
|
|
242
|
+
sort: The sort order.
|
|
243
|
+
session: The session to use.
|
|
244
|
+
ignore_cache: Whether to ignore the cache.
|
|
245
|
+
fetch_links: Whether to fetch links.
|
|
246
|
+
lazy_parse: Whether to lazy parse the documents.
|
|
247
|
+
nesting_depth: The nesting depth.
|
|
248
|
+
nesting_depths_per_field: The nesting depths per field.
|
|
249
|
+
**pymongo_kwargs: Additional keyword arguments to pass to the find method.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
list[EntityGenericType]: The list of entities.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
OperationError: If the operation fails.
|
|
256
|
+
ValueError: If the entity cannot be created from the document.
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
documents: list[DocumentGenericType] = await self._document_type.find(
|
|
260
|
+
*args,
|
|
261
|
+
projection_model=projection_model,
|
|
262
|
+
skip=skip,
|
|
263
|
+
limit=limit,
|
|
264
|
+
sort=sort,
|
|
265
|
+
session=session,
|
|
266
|
+
ignore_cache=ignore_cache,
|
|
267
|
+
fetch_links=fetch_links,
|
|
268
|
+
lazy_parse=lazy_parse,
|
|
269
|
+
nesting_depth=nesting_depth,
|
|
270
|
+
nesting_depths_per_field=nesting_depths_per_field,
|
|
271
|
+
**pymongo_kwargs,
|
|
272
|
+
).to_list()
|
|
273
|
+
except PyMongoError as error:
|
|
274
|
+
raise OperationError(f"Failed to find documents: {error}") from error
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
entities: list[EntityGenericType] = [self._entity_type(**document.model_dump()) for document in documents]
|
|
278
|
+
except ValueError as error:
|
|
279
|
+
raise ValueError(f"Failed to create entity from document: {error}") from error
|
|
280
|
+
|
|
281
|
+
return entities
|