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.

Files changed (58) hide show
  1. fastapi_factory_utilities/__main__.py +6 -0
  2. fastapi_factory_utilities/core/__init__.py +1 -0
  3. fastapi_factory_utilities/core/api/__init__.py +25 -0
  4. fastapi_factory_utilities/core/api/tags.py +9 -0
  5. fastapi_factory_utilities/core/api/v1/sys/__init__.py +12 -0
  6. fastapi_factory_utilities/core/api/v1/sys/health.py +53 -0
  7. fastapi_factory_utilities/core/api/v1/sys/readiness.py +53 -0
  8. fastapi_factory_utilities/core/app/__init__.py +19 -0
  9. fastapi_factory_utilities/core/app/base/__init__.py +17 -0
  10. fastapi_factory_utilities/core/app/base/application.py +123 -0
  11. fastapi_factory_utilities/core/app/base/config_abstract.py +78 -0
  12. fastapi_factory_utilities/core/app/base/exceptions.py +25 -0
  13. fastapi_factory_utilities/core/app/base/fastapi_application_abstract.py +88 -0
  14. fastapi_factory_utilities/core/app/base/plugins_manager_abstract.py +136 -0
  15. fastapi_factory_utilities/core/app/enums.py +11 -0
  16. fastapi_factory_utilities/core/plugins/__init__.py +15 -0
  17. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +97 -0
  18. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +239 -0
  19. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +17 -0
  20. fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +31 -0
  21. fastapi_factory_utilities/core/plugins/odm_plugin/exceptions.py +25 -0
  22. fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +172 -0
  23. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +124 -0
  24. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +266 -0
  25. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +103 -0
  26. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/exceptions.py +13 -0
  27. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/helpers.py +42 -0
  28. fastapi_factory_utilities/core/protocols.py +82 -0
  29. fastapi_factory_utilities/core/utils/configs.py +80 -0
  30. fastapi_factory_utilities/core/utils/importlib.py +28 -0
  31. fastapi_factory_utilities/core/utils/log.py +178 -0
  32. fastapi_factory_utilities/core/utils/uvicorn.py +45 -0
  33. fastapi_factory_utilities/core/utils/yaml_reader.py +166 -0
  34. fastapi_factory_utilities/example/__init__.py +11 -0
  35. fastapi_factory_utilities/example/__main__.py +6 -0
  36. fastapi_factory_utilities/example/api/__init__.py +19 -0
  37. fastapi_factory_utilities/example/api/books/__init__.py +5 -0
  38. fastapi_factory_utilities/example/api/books/responses.py +26 -0
  39. fastapi_factory_utilities/example/api/books/routes.py +62 -0
  40. fastapi_factory_utilities/example/app/__init__.py +6 -0
  41. fastapi_factory_utilities/example/app/app.py +37 -0
  42. fastapi_factory_utilities/example/app/config.py +12 -0
  43. fastapi_factory_utilities/example/application.yaml +26 -0
  44. fastapi_factory_utilities/example/entities/books/__init__.py +7 -0
  45. fastapi_factory_utilities/example/entities/books/entities.py +16 -0
  46. fastapi_factory_utilities/example/entities/books/enums.py +16 -0
  47. fastapi_factory_utilities/example/entities/books/types.py +54 -0
  48. fastapi_factory_utilities/example/models/__init__.py +1 -0
  49. fastapi_factory_utilities/example/models/books/__init__.py +6 -0
  50. fastapi_factory_utilities/example/models/books/document.py +20 -0
  51. fastapi_factory_utilities/example/models/books/repository.py +11 -0
  52. fastapi_factory_utilities/example/services/books/__init__.py +5 -0
  53. fastapi_factory_utilities/example/services/books/services.py +167 -0
  54. fastapi_factory_utilities-0.1.0.dist-info/LICENSE +21 -0
  55. fastapi_factory_utilities-0.1.0.dist-info/METADATA +131 -0
  56. fastapi_factory_utilities-0.1.0.dist-info/RECORD +58 -0
  57. fastapi_factory_utilities-0.1.0.dist-info/WHEEL +4 -0
  58. 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.")