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.

Files changed (72) hide show
  1. fastapi_factory_utilities/core/api/__init__.py +1 -1
  2. fastapi_factory_utilities/core/api/v1/sys/health.py +1 -1
  3. fastapi_factory_utilities/core/app/__init__.py +12 -3
  4. fastapi_factory_utilities/core/app/application.py +24 -26
  5. fastapi_factory_utilities/core/app/builder.py +23 -37
  6. fastapi_factory_utilities/core/app/config.py +22 -1
  7. fastapi_factory_utilities/core/app/fastapi_builder.py +3 -2
  8. fastapi_factory_utilities/core/exceptions.py +58 -22
  9. fastapi_factory_utilities/core/plugins/__init__.py +2 -31
  10. fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
  11. fastapi_factory_utilities/core/plugins/aiopika/__init__.py +25 -0
  12. fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
  13. fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
  14. fastapi_factory_utilities/core/plugins/aiopika/depends.py +20 -0
  15. fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
  16. fastapi_factory_utilities/core/plugins/aiopika/exchange.py +70 -0
  17. fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
  18. fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
  19. fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
  20. fastapi_factory_utilities/core/plugins/aiopika/plugins.py +84 -0
  21. fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
  22. fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
  23. fastapi_factory_utilities/core/plugins/aiopika/queue.py +86 -0
  24. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +25 -153
  25. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +59 -31
  26. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
  27. fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +2 -1
  28. fastapi_factory_utilities/core/plugins/odm_plugin/helpers.py +16 -0
  29. fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
  30. fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +112 -3
  31. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -115
  32. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +65 -14
  33. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/configs.py +13 -0
  34. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
  35. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
  36. fastapi_factory_utilities/core/plugins/taskiq_plugins/__init__.py +29 -0
  37. fastapi_factory_utilities/core/plugins/taskiq_plugins/configs.py +12 -0
  38. fastapi_factory_utilities/core/plugins/taskiq_plugins/depends.py +51 -0
  39. fastapi_factory_utilities/core/plugins/taskiq_plugins/exceptions.py +13 -0
  40. fastapi_factory_utilities/core/plugins/taskiq_plugins/plugin.py +41 -0
  41. fastapi_factory_utilities/core/plugins/taskiq_plugins/schedulers.py +187 -0
  42. fastapi_factory_utilities/core/protocols.py +1 -54
  43. fastapi_factory_utilities/core/security/jwt.py +159 -0
  44. fastapi_factory_utilities/core/security/kratos.py +98 -0
  45. fastapi_factory_utilities/core/services/hydra/__init__.py +13 -0
  46. fastapi_factory_utilities/core/services/hydra/exceptions.py +15 -0
  47. fastapi_factory_utilities/core/services/hydra/objects.py +26 -0
  48. fastapi_factory_utilities/core/services/hydra/services.py +122 -0
  49. fastapi_factory_utilities/core/services/kratos/__init__.py +13 -0
  50. fastapi_factory_utilities/core/services/kratos/enums.py +11 -0
  51. fastapi_factory_utilities/core/services/kratos/exceptions.py +15 -0
  52. fastapi_factory_utilities/core/services/kratos/objects.py +43 -0
  53. fastapi_factory_utilities/core/services/kratos/services.py +86 -0
  54. fastapi_factory_utilities/core/services/status/__init__.py +2 -2
  55. fastapi_factory_utilities/core/utils/status.py +2 -1
  56. fastapi_factory_utilities/core/utils/uvicorn.py +36 -0
  57. fastapi_factory_utilities/core/utils/yaml_reader.py +2 -2
  58. fastapi_factory_utilities/example/app.py +15 -5
  59. fastapi_factory_utilities/example/entities/books/__init__.py +1 -1
  60. fastapi_factory_utilities/example/models/books/__init__.py +1 -1
  61. fastapi_factory_utilities/py.typed +0 -0
  62. {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/METADATA +23 -14
  63. fastapi_factory_utilities-0.7.1.dist-info/RECORD +101 -0
  64. {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/WHEEL +1 -1
  65. fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
  66. fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
  67. fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -190
  68. fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
  69. fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
  70. fastapi_factory_utilities-0.2.0.dist-info/RECORD +0 -70
  71. {fastapi_factory_utilities-0.2.0.dist-info → fastapi_factory_utilities-0.7.1.dist-info}/entry_points.txt +0 -0
  72. {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
- """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 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,
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 .builder import ODMBuilder
25
-
26
- _logger: BoundLogger = get_logger()
27
-
28
-
29
- def pre_conditions_check(application: ApplicationAbstractProtocol) -> bool:
30
- """Check the pre-conditions for the OpenTelemetry plugin.
31
-
32
- Args:
33
- application (BaseApplicationProtocol): The application.
34
-
35
- Returns:
36
- bool: True if the pre-conditions are met, False otherwise.
37
- """
38
- del application
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
- @classmethod
128
- def _wait_client_to_be_ready(cls, client: AsyncIOMotorClient[Any], timeout_s: int) -> None:
129
- """Wait for the ODM client to be ready.
130
-
131
- Args:
132
- client (AsyncIOMotorClient): The ODM client.
133
- timeout_s (int): The timeout in seconds.
134
-
135
- Raises:
136
- TimeoutError: If the ODM client is not ready in the given timeout.
137
- """
138
- start_time: float = time.time()
139
- message_time: float = time.time()
140
- while (time.time() - start_time) < (timeout_s):
141
- if len(client.nodes) > 0: # type: ignore
142
- _logger.info(f"Waiting {(time.time() - start_time)*cls.MS_TO_S}ms for the ODM client to be ready.")
143
- return
144
-
145
- if (time.time() - message_time) > 1:
146
- elaps_time: float = time.time() - start_time
147
- _logger.debug(f"Waiting for the ODM client to be ready. (from {int(elaps_time)}s) ")
148
- message_time = time.time()
149
- time.sleep(cls.SLEEP_TIME_S)
150
-
151
- raise TimeoutError("The ODM client is not ready in the given timeout.")
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.connection_timeout_s,
177
- serverSelectionTimeoutMS=self._config.connection_timeout_s,
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
- self._wait_client_to_be_ready(client=self._odm_client, timeout_s=self._config.connection_timeout_s)
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 " "build_client method or through parameter."
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(name=database_name)
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
@@ -12,4 +12,4 @@ class ODMConfig(BaseModel):
12
12
 
13
13
  database: str = "test"
14
14
 
15
- connection_timeout_s: int = 10
15
+ connection_timeout_ms: int = 4000
@@ -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( # pyright: ignore[reportIncompatibleVariableOverride]
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
- document: DocumentGenericType = self._document_type(**entity.model_dump())
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.save(session=session)
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