fastapi-factory-utilities 0.1.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/__main__.py +6 -0
- fastapi_factory_utilities/core/__init__.py +1 -0
- fastapi_factory_utilities/core/api/__init__.py +25 -0
- fastapi_factory_utilities/core/api/tags.py +9 -0
- fastapi_factory_utilities/core/api/v1/sys/__init__.py +12 -0
- fastapi_factory_utilities/core/api/v1/sys/health.py +53 -0
- fastapi_factory_utilities/core/api/v1/sys/readiness.py +53 -0
- fastapi_factory_utilities/core/app/__init__.py +19 -0
- fastapi_factory_utilities/core/app/base/__init__.py +17 -0
- fastapi_factory_utilities/core/app/base/application.py +123 -0
- fastapi_factory_utilities/core/app/base/config_abstract.py +78 -0
- fastapi_factory_utilities/core/app/base/exceptions.py +25 -0
- fastapi_factory_utilities/core/app/base/fastapi_application_abstract.py +88 -0
- fastapi_factory_utilities/core/app/base/plugins_manager_abstract.py +136 -0
- fastapi_factory_utilities/core/app/enums.py +11 -0
- fastapi_factory_utilities/core/plugins/__init__.py +15 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +97 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +239 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +17 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +31 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/exceptions.py +25 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +172 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +124 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +266 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +103 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/exceptions.py +13 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/helpers.py +42 -0
- fastapi_factory_utilities/core/protocols.py +82 -0
- fastapi_factory_utilities/core/utils/configs.py +80 -0
- fastapi_factory_utilities/core/utils/importlib.py +28 -0
- fastapi_factory_utilities/core/utils/log.py +178 -0
- fastapi_factory_utilities/core/utils/uvicorn.py +45 -0
- fastapi_factory_utilities/core/utils/yaml_reader.py +166 -0
- fastapi_factory_utilities/example/__init__.py +11 -0
- fastapi_factory_utilities/example/__main__.py +6 -0
- fastapi_factory_utilities/example/api/__init__.py +19 -0
- fastapi_factory_utilities/example/api/books/__init__.py +5 -0
- fastapi_factory_utilities/example/api/books/responses.py +26 -0
- fastapi_factory_utilities/example/api/books/routes.py +62 -0
- fastapi_factory_utilities/example/app/__init__.py +6 -0
- fastapi_factory_utilities/example/app/app.py +37 -0
- fastapi_factory_utilities/example/app/config.py +12 -0
- fastapi_factory_utilities/example/application.yaml +26 -0
- fastapi_factory_utilities/example/entities/books/__init__.py +7 -0
- fastapi_factory_utilities/example/entities/books/entities.py +16 -0
- fastapi_factory_utilities/example/entities/books/enums.py +16 -0
- fastapi_factory_utilities/example/entities/books/types.py +54 -0
- fastapi_factory_utilities/example/models/__init__.py +1 -0
- fastapi_factory_utilities/example/models/books/__init__.py +6 -0
- fastapi_factory_utilities/example/models/books/document.py +20 -0
- fastapi_factory_utilities/example/models/books/repository.py +11 -0
- fastapi_factory_utilities/example/services/books/__init__.py +5 -0
- fastapi_factory_utilities/example/services/books/services.py +167 -0
- fastapi_factory_utilities-0.1.0.dist-info/LICENSE +21 -0
- fastapi_factory_utilities-0.1.0.dist-info/METADATA +131 -0
- fastapi_factory_utilities-0.1.0.dist-info/RECORD +58 -0
- fastapi_factory_utilities-0.1.0.dist-info/WHEEL +4 -0
- fastapi_factory_utilities-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Oriented Data Model (ODM) plugin package."""
|
|
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 structlog.stdlib import BoundLogger, get_logger
|
|
9
|
+
|
|
10
|
+
from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
|
|
11
|
+
|
|
12
|
+
from .builder import ODMBuilder
|
|
13
|
+
|
|
14
|
+
_logger: BoundLogger = get_logger()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def pre_conditions_check(application: BaseApplicationProtocol) -> bool:
|
|
18
|
+
"""Check the pre-conditions for the OpenTelemetry plugin.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
application (BaseApplicationProtocol): The application.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
bool: True if the pre-conditions are met, False otherwise.
|
|
25
|
+
"""
|
|
26
|
+
del application
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def on_load(
|
|
31
|
+
application: BaseApplicationProtocol,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Actions to perform on load for the OpenTelemetry plugin.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
application (BaseApplicationProtocol): The application.
|
|
37
|
+
"""
|
|
38
|
+
del application
|
|
39
|
+
# Configure the pymongo logger to INFO level
|
|
40
|
+
pymongo_logger: Logger = getLogger("pymongo")
|
|
41
|
+
pymongo_logger.setLevel(INFO)
|
|
42
|
+
_logger.debug("ODM plugin loaded.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def on_startup(
|
|
46
|
+
application: BaseApplicationProtocol,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Actions to perform on startup for the ODM plugin.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
application (BaseApplicationProtocol): The application.
|
|
52
|
+
odm_config (ODMConfig): The ODM configuration.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
None
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
odm_factory: ODMBuilder = ODMBuilder(application=application).build_all()
|
|
59
|
+
except Exception as exception: # pylint: disable=broad-except
|
|
60
|
+
_logger.error(f"ODM plugin failed to start. {exception}")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if odm_factory.odm_database is None or odm_factory.odm_client is None:
|
|
64
|
+
_logger.error(
|
|
65
|
+
f"ODM plugin failed to start. Database: {odm_factory.odm_database} - " f"Client: {odm_factory.odm_client}"
|
|
66
|
+
)
|
|
67
|
+
return
|
|
68
|
+
# TODO: Find a way to add type to the state
|
|
69
|
+
application.get_asgi_app().state.odm_client = odm_factory.odm_client
|
|
70
|
+
application.get_asgi_app().state.odm_database = odm_factory.odm_database
|
|
71
|
+
|
|
72
|
+
# TODO: Find a better way to initialize beanie with the document models of the concrete application
|
|
73
|
+
# through an hook in the application ?
|
|
74
|
+
await init_beanie(
|
|
75
|
+
database=odm_factory.odm_database,
|
|
76
|
+
document_models=application.ODM_DOCUMENT_MODELS,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_logger.info(
|
|
80
|
+
f"ODM plugin started. Database: {odm_factory.odm_database.name} - "
|
|
81
|
+
f"Client: {odm_factory.odm_client.address} - "
|
|
82
|
+
f"Document models: {application.ODM_DOCUMENT_MODELS}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def on_shutdown(application: BaseApplicationProtocol) -> None:
|
|
87
|
+
"""Actions to perform on shutdown for the ODM plugin.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
application (BaseApplicationProtocol): The application.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
None
|
|
94
|
+
"""
|
|
95
|
+
client: AsyncIOMotorClient[Any] = application.get_asgi_app().state.odm_client
|
|
96
|
+
client.close()
|
|
97
|
+
_logger.debug("ODM plugin shutdown.")
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Provides the module for the ODM plugin."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Self
|
|
6
|
+
|
|
7
|
+
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
8
|
+
from structlog.stdlib import get_logger
|
|
9
|
+
|
|
10
|
+
from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
|
|
11
|
+
from fastapi_factory_utilities.core.utils.importlib import get_path_file_in_package
|
|
12
|
+
from fastapi_factory_utilities.core.utils.yaml_reader import (
|
|
13
|
+
UnableToReadYamlFileError,
|
|
14
|
+
YamlFileReader,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .configs import ODMConfig
|
|
18
|
+
from .exceptions import ODMPluginConfigError
|
|
19
|
+
|
|
20
|
+
_logger = get_logger()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ODMBuilder:
|
|
24
|
+
"""Factory to create the resources for the ODM plugin.
|
|
25
|
+
|
|
26
|
+
The factory is responsible for creating the resources for the ODM plugin.
|
|
27
|
+
- The ODM configuration.
|
|
28
|
+
- The ODM client.
|
|
29
|
+
- The ODM database.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
# Example of using the ODMFactory
|
|
33
|
+
odm_factory: ODMFactory = ODMFactory(application=application)
|
|
34
|
+
odm_factory.build_all()
|
|
35
|
+
# Access the ODM database created
|
|
36
|
+
database: AsyncIOMotorDatabase[Any] = odm_factory.database
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
application: BaseApplicationProtocol,
|
|
44
|
+
odm_config: ODMConfig | None = None,
|
|
45
|
+
odm_client: AsyncIOMotorClient[Any] | None = None,
|
|
46
|
+
odm_database: AsyncIOMotorDatabase[Any] | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize the ODMFactory.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
application (BaseApplicationProtocol): The application.
|
|
52
|
+
odm_config (ODMConfig): The ODM configuration for injection. (Default is None)
|
|
53
|
+
odm_client (AsyncIOMotorClient): The ODM client for injection. (Default is None)
|
|
54
|
+
odm_database (AsyncIOMotorDatabase): The ODM database for injection. (Default is None)
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
self._application: BaseApplicationProtocol = application
|
|
58
|
+
self._config: ODMConfig | None = odm_config
|
|
59
|
+
self._odm_client: AsyncIOMotorClient[Any] | None = odm_client
|
|
60
|
+
self._odm_database: AsyncIOMotorDatabase[Any] | None = odm_database
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def config(self) -> ODMConfig | None:
|
|
64
|
+
"""Provide the ODM configuration object.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
ODMConfig: The ODM configuration object.
|
|
68
|
+
"""
|
|
69
|
+
return self._config
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def odm_client(self) -> AsyncIOMotorClient[Any] | None:
|
|
73
|
+
"""Provide the ODM client.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
AsyncIOMotorClient | None: The ODM client.
|
|
77
|
+
"""
|
|
78
|
+
return self._odm_client
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def odm_database(self) -> AsyncIOMotorDatabase[Any] | None:
|
|
82
|
+
"""Provide the ODM database.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
AsyncIOMotorDatabase | None: The ODM database.
|
|
86
|
+
"""
|
|
87
|
+
return self._odm_database
|
|
88
|
+
|
|
89
|
+
def build_odm_config(
|
|
90
|
+
self,
|
|
91
|
+
) -> Self:
|
|
92
|
+
"""Build the ODM configuration object.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Self: The ODM factory.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ODMPluginConfigError: If the package name is not set or the configuration file is not found.
|
|
99
|
+
"""
|
|
100
|
+
if self._config is not None:
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
if self._application.PACKAGE_NAME == "":
|
|
104
|
+
raise ODMPluginConfigError("The package name must be set in the concrete application class.")
|
|
105
|
+
# Read the application configuration file
|
|
106
|
+
try:
|
|
107
|
+
yaml_file_content: dict[str, Any] = YamlFileReader(
|
|
108
|
+
file_path=get_path_file_in_package(
|
|
109
|
+
filename="application.yaml",
|
|
110
|
+
package=self._application.PACKAGE_NAME,
|
|
111
|
+
),
|
|
112
|
+
yaml_base_key="odm",
|
|
113
|
+
use_environment_injection=True,
|
|
114
|
+
).read()
|
|
115
|
+
except (FileNotFoundError, ImportError, UnableToReadYamlFileError) as exception:
|
|
116
|
+
raise ODMPluginConfigError("Unable to read the application configuration file.") from exception
|
|
117
|
+
|
|
118
|
+
# Create the application configuration model
|
|
119
|
+
try:
|
|
120
|
+
self._config = ODMConfig(**yaml_file_content)
|
|
121
|
+
except ValueError as exception:
|
|
122
|
+
raise ODMPluginConfigError("Unable to create the application configuration model.") from exception
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _wait_client_to_be_ready(cls, client: AsyncIOMotorClient[Any], timeout_ms: int) -> None:
|
|
127
|
+
"""Wait for the ODM client to be ready.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
client (AsyncIOMotorClient): The ODM client.
|
|
131
|
+
timeout_ms (int): The timeout in milliseconds.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ODMPluginConfigError: If the ODM client is not ready.
|
|
135
|
+
"""
|
|
136
|
+
start_timer = time.monotonic()
|
|
137
|
+
|
|
138
|
+
async def is_connected(client: AsyncIOMotorClient[Any]) -> bool:
|
|
139
|
+
"""Check if the client is connected."""
|
|
140
|
+
try:
|
|
141
|
+
await client.admin.command(
|
|
142
|
+
command="ping",
|
|
143
|
+
) # pyright: ignore
|
|
144
|
+
return True
|
|
145
|
+
except Exception: # pylint: disable=broad-except
|
|
146
|
+
_logger.debug("ODM client is not ready.")
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
|
150
|
+
|
|
151
|
+
while not loop.run_in_executor(None, asyncio.Task(is_connected(client))) and ( # type: ignore
|
|
152
|
+
(time.monotonic() - start_timer) < timeout_ms
|
|
153
|
+
):
|
|
154
|
+
if time.monotonic() - start_timer > timeout_ms:
|
|
155
|
+
raise ODMPluginConfigError("ODM client is not ready.")
|
|
156
|
+
time.sleep(0.01)
|
|
157
|
+
|
|
158
|
+
if not loop.run_in_executor(None, asyncio.Task(is_connected(client))): # type: ignore
|
|
159
|
+
raise ODMPluginConfigError("ODM client is not ready.")
|
|
160
|
+
|
|
161
|
+
def build_client(
|
|
162
|
+
self,
|
|
163
|
+
) -> Self:
|
|
164
|
+
"""Build the ODM client.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Self: The ODM factory.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ODMPluginConfigError: If the ODM configuration is not build or provided.
|
|
171
|
+
"""
|
|
172
|
+
if self._odm_client is not None:
|
|
173
|
+
return self
|
|
174
|
+
|
|
175
|
+
if self._config is None:
|
|
176
|
+
raise ODMPluginConfigError(
|
|
177
|
+
"ODM configuration is not set. Provide the ODM configuration using "
|
|
178
|
+
"build_odm_config method or through parameter."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self._odm_client = AsyncIOMotorClient(
|
|
182
|
+
host=self._config.uri,
|
|
183
|
+
connect=True,
|
|
184
|
+
connectTimeoutMS=self._config.connection_timeout_ms,
|
|
185
|
+
serverSelectionTimeoutMS=self._config.connection_timeout_ms,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self._wait_client_to_be_ready(client=self._odm_client, timeout_ms=self._config.connection_timeout_ms)
|
|
189
|
+
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def build_database(
|
|
193
|
+
self,
|
|
194
|
+
) -> Self:
|
|
195
|
+
"""Build the ODM database.
|
|
196
|
+
|
|
197
|
+
The ODM client and ODM configuration are recommended to be provided through call to the build_client and
|
|
198
|
+
build_odm_config methods.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Any: The ODM database.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ODMPluginConfigError: If the ODM configuration is not build or provided.
|
|
205
|
+
"""
|
|
206
|
+
if self._odm_database is not None:
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
if self._config is None:
|
|
210
|
+
raise ODMPluginConfigError(
|
|
211
|
+
"ODM configuration is not set. Provide the ODM configuration using "
|
|
212
|
+
"build_odm_config method or through parameter."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
database_name: str = self._config.database
|
|
216
|
+
|
|
217
|
+
if self._odm_client is None:
|
|
218
|
+
raise ODMPluginConfigError(
|
|
219
|
+
"ODM client is not set. Provide the ODM client using " "build_client method or through parameter."
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
self._odm_database = self._odm_client.get_database(name=database_name)
|
|
223
|
+
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
def build_all(self) -> Self:
|
|
227
|
+
"""Build all the resources for the ODM plugin.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Self: The ODM factory.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
ODMPluginConfigError: If the ODM configuration is not build or provided.
|
|
234
|
+
"""
|
|
235
|
+
self.build_odm_config()
|
|
236
|
+
self.build_client()
|
|
237
|
+
self.build_database()
|
|
238
|
+
|
|
239
|
+
return self
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Provides the configuration for the ODM plugin."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
S_TO_MS = 1000
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ODMConfig(BaseModel):
|
|
9
|
+
"""Provides the configuration model for the ODM plugin."""
|
|
10
|
+
|
|
11
|
+
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
12
|
+
|
|
13
|
+
uri: str
|
|
14
|
+
|
|
15
|
+
database: str = "test"
|
|
16
|
+
|
|
17
|
+
connection_timeout_ms: int = 1 * S_TO_MS
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Provides base document class for ODM plugins."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
from uuid import UUID, uuid4
|
|
6
|
+
|
|
7
|
+
from beanie import Document, Indexed # pyright: ignore[reportUnknownVariableType]
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
from pymongo import DESCENDING
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseDocument(Document):
|
|
13
|
+
"""Base document class."""
|
|
14
|
+
|
|
15
|
+
# To be agnostic of MongoDN, we use UUID as the document ID.
|
|
16
|
+
id: UUID = Field( # pyright: ignore[reportIncompatibleVariableOverride]
|
|
17
|
+
default_factory=uuid4, description="The document ID."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
created_at: Annotated[datetime.datetime, Indexed(index_type=DESCENDING)] = Field( # pyright: ignore
|
|
21
|
+
default_factory=lambda: datetime.datetime.now(tz=datetime.UTC), description="Creation timestamp."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
updated_at: Annotated[datetime.datetime, Indexed(index_type=DESCENDING)] = Field( # pyright: ignore
|
|
25
|
+
default_factory=lambda: datetime.datetime.now(tz=datetime.UTC), description="Last update timestamp."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
class Settings:
|
|
29
|
+
"""Meta class for BaseDocument."""
|
|
30
|
+
|
|
31
|
+
use_revision = True
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Provides the exceptions for the ODM_Plugin."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ODMPluginBaseException(BaseException):
|
|
5
|
+
"""Base exception for the ODM_Plugin."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ODMPluginConfigError(ODMPluginBaseException):
|
|
11
|
+
"""Exception for the ODM_Plugin configuration."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnableToCreateEntityDueToDuplicateKeyError(ODMPluginBaseException):
|
|
17
|
+
"""Exception for when the entity cannot be created due to a duplicate key error."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class OperationError(ODMPluginBaseException):
|
|
23
|
+
"""Exception for when an operation fails."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Provides the abstract classes for the repositories."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from collections.abc import AsyncGenerator, Callable
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import Any, Generic, TypeVar, get_args
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from motor.motor_asyncio import AsyncIOMotorClientSession, AsyncIOMotorDatabase
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
from pymongo.errors import DuplicateKeyError, PyMongoError
|
|
12
|
+
from pymongo.results import DeleteResult
|
|
13
|
+
|
|
14
|
+
from .documents import BaseDocument
|
|
15
|
+
from .exceptions import OperationError, UnableToCreateEntityDueToDuplicateKeyError
|
|
16
|
+
|
|
17
|
+
DocumentGenericType = TypeVar("DocumentGenericType", bound=BaseDocument) # pylint: disable=invalid-name
|
|
18
|
+
EntityGenericType = TypeVar("EntityGenericType", bound=BaseModel) # pylint: disable=invalid-name
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def managed_session() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
22
|
+
"""Decorator to manage the session.
|
|
23
|
+
|
|
24
|
+
It will introspect the function arguments and check if the session is passed as a keyword argument.
|
|
25
|
+
If it is not, it will create a new session and pass it to the function.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
29
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
30
|
+
if "session" in kwargs:
|
|
31
|
+
return await func(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
async with args[0].get_session() as session:
|
|
34
|
+
return await func(*args, **kwargs, session=session)
|
|
35
|
+
|
|
36
|
+
return wrapper
|
|
37
|
+
|
|
38
|
+
return decorator
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AbstractRepository(ABC, Generic[DocumentGenericType, EntityGenericType]):
|
|
42
|
+
"""Abstract class for the repository."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, database: AsyncIOMotorDatabase[Any]) -> None:
|
|
45
|
+
"""Initialize the repository."""
|
|
46
|
+
super().__init__()
|
|
47
|
+
self._database: AsyncIOMotorDatabase[Any] = database
|
|
48
|
+
# Retrieve the generic concrete types
|
|
49
|
+
generic_args: tuple[Any, ...] = get_args(self.__orig_bases__[0]) # type: ignore
|
|
50
|
+
self._document_type: type[DocumentGenericType] = generic_args[0]
|
|
51
|
+
self._entity_type: type[EntityGenericType] = generic_args[1]
|
|
52
|
+
|
|
53
|
+
@asynccontextmanager
|
|
54
|
+
async def get_session(self) -> AsyncGenerator[AsyncIOMotorClientSession, None]:
|
|
55
|
+
"""Yield a new session."""
|
|
56
|
+
try:
|
|
57
|
+
async with await self._database.client.start_session() as session:
|
|
58
|
+
yield session
|
|
59
|
+
except PyMongoError as error:
|
|
60
|
+
raise OperationError(f"Failed to create session: {error}") from error
|
|
61
|
+
|
|
62
|
+
@managed_session()
|
|
63
|
+
async def insert(
|
|
64
|
+
self, entity: EntityGenericType, session: AsyncIOMotorClientSession | None = None
|
|
65
|
+
) -> EntityGenericType:
|
|
66
|
+
"""Insert the entity into the database.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
entity (EntityGenericType): The entity to insert.
|
|
70
|
+
session (AsyncIOMotorClientSession | None): The session to use. Defaults to None. (managed by decorator)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
EntityGenericType: The entity created.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValueError: If the entity cannot be created from the document.
|
|
77
|
+
UnableToCreateEntityDueToDuplicateKeyError: If the entity cannot be created due to a duplicate key error.
|
|
78
|
+
OperationError: If the operation fails.
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
document: DocumentGenericType = self._document_type(**entity.model_dump())
|
|
82
|
+
except ValueError as error:
|
|
83
|
+
raise ValueError(f"Failed to create document from entity: {error}") from error
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
document_created: DocumentGenericType = await document.save(session=session)
|
|
87
|
+
except DuplicateKeyError as error:
|
|
88
|
+
raise UnableToCreateEntityDueToDuplicateKeyError(f"Failed to insert document: {error}") from error
|
|
89
|
+
except PyMongoError as error:
|
|
90
|
+
raise OperationError(f"Failed to insert document: {error}") from error
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
entity_created: EntityGenericType = self._entity_type(**document_created.model_dump())
|
|
94
|
+
except ValueError as error:
|
|
95
|
+
raise ValueError(f"Failed to create entity from document: {error}") from error
|
|
96
|
+
|
|
97
|
+
return entity_created
|
|
98
|
+
|
|
99
|
+
@managed_session()
|
|
100
|
+
async def get_one_by_id(
|
|
101
|
+
self,
|
|
102
|
+
entity_id: UUID,
|
|
103
|
+
session: AsyncIOMotorClientSession | None = None,
|
|
104
|
+
) -> EntityGenericType | None:
|
|
105
|
+
"""Get the entity by its ID.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
entity_id (UUID): The ID of the entity.
|
|
109
|
+
session (AsyncIOMotorClientSession | None): The session to use. Defaults to None. (managed by decorator)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
EntityGenericType | None: The entity or None if not found.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
OperationError: If the operation fails.
|
|
116
|
+
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
document: DocumentGenericType | None = await self._document_type.get(document_id=entity_id, session=session)
|
|
120
|
+
except PyMongoError as error:
|
|
121
|
+
raise OperationError(f"Failed to get document: {error}") from error
|
|
122
|
+
|
|
123
|
+
# If no document is found, return None
|
|
124
|
+
if document is None:
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Convert the document to an entity
|
|
128
|
+
try:
|
|
129
|
+
entity: EntityGenericType = self._entity_type(**document.model_dump())
|
|
130
|
+
except ValueError as error:
|
|
131
|
+
raise ValueError(f"Failed to create entity from document: {error}") from error
|
|
132
|
+
|
|
133
|
+
return entity
|
|
134
|
+
|
|
135
|
+
@managed_session()
|
|
136
|
+
async def delete_one_by_id(
|
|
137
|
+
self, entity_id: UUID, raise_if_not_found: bool = False, session: AsyncIOMotorClientSession | None = None
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Delete a document by its ID.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
entity_id (UUID): The ID of the entity.
|
|
143
|
+
raise_if_not_found (bool, optional): Raise an exception if the document is not found. Defaults to False.
|
|
144
|
+
session (AsyncIOMotorClientSession | None, optional): The session to use.
|
|
145
|
+
Defaults to None. (managed by decorator)
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ValueError: If the document is not found and raise_if_not_found is True.
|
|
149
|
+
OperationError: If the operation fails.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
document_to_delete: DocumentGenericType | None = await self._document_type.get(
|
|
154
|
+
document_id=entity_id, session=session
|
|
155
|
+
)
|
|
156
|
+
except PyMongoError as error:
|
|
157
|
+
raise OperationError(f"Failed to get document to delete: {error}") from error
|
|
158
|
+
|
|
159
|
+
if document_to_delete is None:
|
|
160
|
+
if raise_if_not_found:
|
|
161
|
+
raise ValueError(f"Failed to find document with ID {entity_id}")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
delete_result: DeleteResult | None = await document_to_delete.delete()
|
|
166
|
+
except PyMongoError as error:
|
|
167
|
+
raise OperationError(f"Failed to delete document: {error}") from error
|
|
168
|
+
|
|
169
|
+
if delete_result is not None and delete_result.deleted_count == 1 and delete_result.acknowledged:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
raise OperationError("Failed to delete document.")
|