fastapi-factory-utilities 0.1.0__py3-none-any.whl → 0.2.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 (54) hide show
  1. fastapi_factory_utilities/core/api/v1/sys/health.py +56 -12
  2. fastapi_factory_utilities/core/api/v1/sys/readiness.py +19 -12
  3. fastapi_factory_utilities/core/app/__init__.py +7 -12
  4. fastapi_factory_utilities/core/app/application.py +133 -0
  5. fastapi_factory_utilities/core/app/builder.py +123 -0
  6. fastapi_factory_utilities/core/app/config.py +164 -0
  7. fastapi_factory_utilities/core/app/exceptions.py +20 -0
  8. fastapi_factory_utilities/core/app/fastapi_builder.py +85 -0
  9. fastapi_factory_utilities/core/app/plugin_manager/__init__.py +15 -0
  10. fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +33 -0
  11. fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +190 -0
  12. fastapi_factory_utilities/core/exceptions.py +43 -0
  13. fastapi_factory_utilities/core/plugins/__init__.py +21 -0
  14. fastapi_factory_utilities/core/plugins/example/__init__.py +31 -0
  15. fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +31 -0
  16. fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +74 -17
  17. fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +27 -35
  18. fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -3
  19. fastapi_factory_utilities/core/plugins/odm_plugin/depends.py +30 -0
  20. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +5 -5
  21. fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +7 -7
  22. fastapi_factory_utilities/core/protocols.py +19 -16
  23. fastapi_factory_utilities/core/services/status/__init__.py +14 -0
  24. fastapi_factory_utilities/core/services/status/enums.py +30 -0
  25. fastapi_factory_utilities/core/services/status/exceptions.py +27 -0
  26. fastapi_factory_utilities/core/services/status/health_calculator_strategies.py +48 -0
  27. fastapi_factory_utilities/core/services/status/readiness_calculator_strategies.py +41 -0
  28. fastapi_factory_utilities/core/services/status/services.py +218 -0
  29. fastapi_factory_utilities/core/services/status/types.py +128 -0
  30. fastapi_factory_utilities/core/utils/configs.py +1 -1
  31. fastapi_factory_utilities/core/utils/status.py +71 -0
  32. fastapi_factory_utilities/core/utils/uvicorn.py +7 -8
  33. fastapi_factory_utilities/example/__init__.py +3 -3
  34. fastapi_factory_utilities/example/api/books/routes.py +7 -10
  35. fastapi_factory_utilities/example/app.py +50 -0
  36. fastapi_factory_utilities/example/application.yaml +5 -9
  37. fastapi_factory_utilities/example/services/books/__init__.py +2 -2
  38. fastapi_factory_utilities/example/services/books/services.py +9 -0
  39. fastapi_factory_utilities/py.typed +0 -0
  40. {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.1.dist-info}/METADATA +6 -4
  41. fastapi_factory_utilities-0.2.1.dist-info/RECORD +71 -0
  42. {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.1.dist-info}/WHEEL +1 -1
  43. fastapi_factory_utilities/core/app/base/__init__.py +0 -17
  44. fastapi_factory_utilities/core/app/base/application.py +0 -123
  45. fastapi_factory_utilities/core/app/base/config_abstract.py +0 -78
  46. fastapi_factory_utilities/core/app/base/exceptions.py +0 -25
  47. fastapi_factory_utilities/core/app/base/fastapi_application_abstract.py +0 -88
  48. fastapi_factory_utilities/core/app/base/plugins_manager_abstract.py +0 -136
  49. fastapi_factory_utilities/example/app/__init__.py +0 -6
  50. fastapi_factory_utilities/example/app/app.py +0 -37
  51. fastapi_factory_utilities/example/app/config.py +0 -12
  52. fastapi_factory_utilities-0.1.0.dist-info/RECORD +0 -58
  53. {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.1.dist-info}/LICENSE +0 -0
  54. {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,85 @@
1
+ """Provides the FastAPIBuilder class."""
2
+
3
+ from typing import Any, NamedTuple, Self
4
+
5
+ from fastapi import APIRouter, FastAPI
6
+ from fastapi.middleware import Middleware
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+
9
+ from fastapi_factory_utilities.core.app.config import RootConfig
10
+
11
+
12
+ class MiddlewareArgs(NamedTuple):
13
+ """Middleware arguments."""
14
+
15
+ middleware_class: type[Middleware]
16
+ kwargs: dict[str, Any]
17
+
18
+
19
+ class FastAPIBuilder:
20
+ """FastAPI builder."""
21
+
22
+ def __init__(self, root_config: RootConfig) -> None:
23
+ """Instantiate the FastAPIBuilder."""
24
+ self._root_config: RootConfig = root_config
25
+ self._base_router: APIRouter = APIRouter()
26
+ self._middleware_list: list[MiddlewareArgs] = []
27
+
28
+ def add_api_router(self, router: APIRouter, without_resource_path: bool = False) -> Self:
29
+ """Add the API router to the FastAPI application.
30
+
31
+ Args:
32
+ router: The API router.
33
+ without_resource_path: If True, the resource path will not be added to the router.
34
+
35
+ Returns:
36
+ Self: The FastAPI builder.
37
+ """
38
+ self._base_router.include_router(
39
+ router=router, prefix=self._root_config.application.root_path if not without_resource_path else ""
40
+ )
41
+ return self
42
+
43
+ def add_middleware(self, middleware_class: type[Middleware], **kwargs: Any) -> Self:
44
+ """Add a middleware to the FastAPI application.
45
+
46
+ Args:
47
+ middleware_class: The middleware class.
48
+ *args: The middleware arguments.
49
+ **kwargs: The middleware keyword arguments.
50
+
51
+ Returns:
52
+ Self: The FastAPI builder.
53
+ """
54
+ self._middleware_list.append(MiddlewareArgs(middleware_class=middleware_class, kwargs=kwargs))
55
+ return self
56
+
57
+ def build(self, lifespan: Any) -> FastAPI:
58
+ """Build the FastAPI application.
59
+
60
+ Returns:
61
+ FastAPI: The FastAPI application.
62
+ """
63
+ fastapi = FastAPI(
64
+ title=self._root_config.application.service_name,
65
+ description="",
66
+ version=self._root_config.application.version,
67
+ lifespan=lifespan, # type: ignore
68
+ )
69
+
70
+ fastapi.add_middleware(
71
+ middleware_class=CORSMiddleware,
72
+ allow_origins=self._root_config.cors.allow_origins,
73
+ allow_credentials=self._root_config.cors.allow_credentials,
74
+ allow_methods=self._root_config.cors.allow_methods,
75
+ allow_headers=self._root_config.cors.allow_headers,
76
+ )
77
+
78
+ for middleware_args in self._middleware_list:
79
+ fastapi.add_middleware(
80
+ middleware_class=middleware_args.middleware_class, **middleware_args.kwargs # type: ignore
81
+ )
82
+
83
+ fastapi.include_router(router=self._base_router)
84
+
85
+ return fastapi
@@ -0,0 +1,15 @@
1
+ """Provide PluginManager."""
2
+
3
+ from .exceptions import (
4
+ InvalidPluginError,
5
+ PluginManagerError,
6
+ PluginPreConditionNotMetError,
7
+ )
8
+ from .plugin_manager import PluginManager
9
+
10
+ __all__: list[str] = [
11
+ "PluginManager",
12
+ "PluginManagerError",
13
+ "InvalidPluginError",
14
+ "PluginPreConditionNotMetError",
15
+ ]
@@ -0,0 +1,33 @@
1
+ """Provide the exceptions for the plugin manager."""
2
+
3
+ from fastapi_factory_utilities.core.exceptions import FastAPIFactoryUtilitiesError
4
+
5
+
6
+ class PluginManagerError(FastAPIFactoryUtilitiesError):
7
+ """Generic plugin manager error."""
8
+
9
+
10
+ class InvalidPluginError(PluginManagerError):
11
+ """The plugin is invalid."""
12
+
13
+ def __init__(self, plugin_name: str, message: str) -> None:
14
+ """Instantiate the exception.
15
+
16
+ Args:
17
+ plugin_name (str): The plugin name.
18
+ message (str): The message
19
+ """
20
+ super().__init__(message=f"Invalid plugin: {plugin_name}, {message}")
21
+
22
+
23
+ class PluginPreConditionNotMetError(PluginManagerError):
24
+ """The plugin pre-condition is not met."""
25
+
26
+ def __init__(self, plugin_name: str, message: str) -> None:
27
+ """Instantiate the exception.
28
+
29
+ Args:
30
+ plugin_name (str): The plugin name.
31
+ message (str): The message
32
+ """
33
+ super().__init__(message=f"Plugin pre-condition not met: {plugin_name}, {message}")
@@ -0,0 +1,190 @@
1
+ """Plugin manager.
2
+
3
+ This module provides the plugin manager for the application.
4
+
5
+ Objective:
6
+ - Provide a way to manage plugins for the application.
7
+ - The plugins are activated based on the configuration.
8
+ - The plugins are checked for pre-conditions.
9
+ - The plugins are loaded on application load.
10
+ - The plugins are started on application startup.
11
+ - The plugins are stopped on application shutdown.
12
+ """
13
+
14
+ from importlib import import_module
15
+ from types import ModuleType
16
+ from typing import Self
17
+
18
+ from fastapi_factory_utilities.core.plugins import PluginsEnum, PluginState
19
+ from fastapi_factory_utilities.core.protocols import (
20
+ ApplicationAbstractProtocol,
21
+ PluginProtocol,
22
+ )
23
+
24
+ from .exceptions import InvalidPluginError, PluginPreConditionNotMetError
25
+
26
+
27
+ class PluginManager:
28
+ """Plugin manager for the application."""
29
+
30
+ DEFAULT_PLUGIN_PACKAGE: str = "fastapi_factory_utilities.core.plugins"
31
+
32
+ @classmethod
33
+ def _import_module(cls, name: str, package: str) -> ModuleType:
34
+ """Import the module. This is a wrapper around the import_module function to be able to mock it in the tests.
35
+
36
+ Args:
37
+ name (str): The name of the module.
38
+ package (str): The package of the module.
39
+
40
+ Returns:
41
+ ModuleType: The module.
42
+
43
+ Raises:
44
+ ImportError: If the module cannot be imported.
45
+ ModuleNotFoundError: If the module is not found.
46
+ """
47
+ return import_module(name=f"{package}.{name}")
48
+
49
+ @classmethod
50
+ def _check_pre_conditions(
51
+ cls, plugin_package: str, want_to_activate_plugins: list[PluginsEnum], application: ApplicationAbstractProtocol
52
+ ) -> list[PluginProtocol]:
53
+ """Check the pre-conditions for the plugins.
54
+
55
+ Args:
56
+ plugin_package (str): The package for the plugins.
57
+ want_to_activate_plugins (list[PluginsEnum]): The plugins to activate.
58
+ application (BaseApplicationProtocol): The application.
59
+
60
+ Returns:
61
+ list[PluginProtocol]: The activated plugins.
62
+
63
+ Raises:
64
+ InvalidPluginError: If the plugin is invalid.
65
+ PluginPreConditionNotMetError: If the pre-conditions are not met.
66
+ """
67
+ plugins: list[PluginProtocol] = []
68
+
69
+ for plugin_enum in want_to_activate_plugins:
70
+
71
+ try:
72
+ # Using a custom import function to be able to mock it in the tests.
73
+ plugin_module: ModuleType = cls._import_module(name=plugin_enum.value, package=plugin_package)
74
+ except (ImportError, ModuleNotFoundError) as import_error:
75
+ # TODO: Later, if we introduce extra mecanism to manage dependencies,
76
+ # we must handle a part of the error here. To be able to provide a better error message.
77
+ # For now, we just raise the error.
78
+ raise InvalidPluginError(
79
+ plugin_name=plugin_enum.value, message="Error importing the plugin "
80
+ ) from import_error
81
+
82
+ if not isinstance(plugin_module, PluginProtocol):
83
+ raise InvalidPluginError(
84
+ plugin_name=plugin_enum.value, message="The plugin does not implement the PluginProtocol"
85
+ )
86
+
87
+ if not plugin_module.pre_conditions_check(application=application):
88
+ raise PluginPreConditionNotMetError(
89
+ plugin_name=plugin_enum.value, message="The pre-conditions are not met"
90
+ )
91
+
92
+ plugins.append(plugin_module)
93
+
94
+ return plugins
95
+
96
+ def __init__(self, activated: list[PluginsEnum] | None = None, plugin_package: str | None = None) -> None:
97
+ """Instanciate the plugin manager."""
98
+ self._plugin_package: str = plugin_package or self.DEFAULT_PLUGIN_PACKAGE
99
+ self._plugins_wanted_to_be_activated: list[PluginsEnum] = activated or []
100
+ self._activated_plugins: list[PluginProtocol] = []
101
+ self._states: list[PluginState] = []
102
+
103
+ def activate(self, plugin: PluginsEnum) -> Self:
104
+ """Activate a plugin.
105
+
106
+ Args:
107
+ plugin (PluginsEnum): The plugin to activate.
108
+
109
+ Returns:
110
+ Self: The plugin manager.
111
+ """
112
+ self._plugins_wanted_to_be_activated.append(plugin)
113
+
114
+ return self
115
+
116
+ @property
117
+ def states(self) -> list[PluginState]:
118
+ """Get the states."""
119
+ return self._states
120
+
121
+ def add_application_context(self, application: ApplicationAbstractProtocol) -> Self:
122
+ """Add the application context to the plugins.
123
+
124
+ Args:
125
+ application (BaseApplicationProtocol): The application.
126
+
127
+ Returns:
128
+ Self: The plugin manager.
129
+ """
130
+ self._application: ApplicationAbstractProtocol = application
131
+
132
+ return self
133
+
134
+ def add_states(self, states: list[PluginState]) -> Self:
135
+ """Add the states to the plugin manager.
136
+
137
+ Args:
138
+ states (list[PluginState]): The states.
139
+
140
+ Returns:
141
+ Self: The plugin manager.
142
+ """
143
+ for state in states:
144
+ self._states.append(state)
145
+ return self
146
+
147
+ def clear_states(self) -> Self:
148
+ """Clear the states."""
149
+ self._states = []
150
+ return self
151
+
152
+ def load(self) -> Self:
153
+ """Load the plugins.
154
+
155
+ Returns:
156
+ Self: The plugin manager.
157
+
158
+ Raises:
159
+ InvalidPluginError: If the plugin is invalid.
160
+ PluginPreConditionNotMetError: If the pre-conditions are not met.
161
+
162
+ """
163
+ # Remove duplicates.
164
+ self._plugins_wanted_to_be_activated = list(set(self._plugins_wanted_to_be_activated))
165
+ # Check the pre-conditions for the plugins.
166
+ self._activated_plugins = self._check_pre_conditions(
167
+ plugin_package=self._plugin_package,
168
+ want_to_activate_plugins=self._plugins_wanted_to_be_activated,
169
+ application=self._application,
170
+ )
171
+ # Load from the previous check.
172
+ for plugin in self._activated_plugins:
173
+ self.add_states(states=plugin.on_load(application=self._application) or [])
174
+
175
+ return self
176
+
177
+ async def trigger_startup(self) -> Self:
178
+ """Trigger the startup of the plugins."""
179
+ for plugin in self._activated_plugins:
180
+ states: list[PluginState] | None = await plugin.on_startup(application=self._application)
181
+ self.add_states(states=states or [])
182
+
183
+ return self
184
+
185
+ async def trigger_shutdown(self) -> Self:
186
+ """Trigger the shutdown of the plugins."""
187
+ for plugin in self._activated_plugins:
188
+ await plugin.on_shutdown(application=self._application)
189
+
190
+ return self
@@ -0,0 +1,43 @@
1
+ """FastAPI Factory Utilities exceptions."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from opentelemetry.trace import Span, get_current_span
7
+ from structlog.stdlib import BoundLogger, get_logger
8
+
9
+ _logger: BoundLogger = get_logger()
10
+
11
+
12
+ class FastAPIFactoryUtilitiesError(Exception):
13
+ """Base exception for the FastAPI Factory Utilities."""
14
+
15
+ def __init__(
16
+ self,
17
+ *args: tuple[Any],
18
+ message: str | None = None,
19
+ level: int = logging.ERROR,
20
+ **kwargs: dict[str, Any],
21
+ ) -> None:
22
+ """Instanciate the exception.
23
+
24
+ Args:
25
+ *args (Tuple[Any]): The arguments.
26
+ message (str | None): The message.
27
+ level (int): The logging level.
28
+ **kwargs (dict[str, Any]): The keyword arguments
29
+ """
30
+ # Log the Exception
31
+ if message:
32
+ _logger.log(level=level, event=message)
33
+ self.message = message
34
+ self.level = level
35
+ args = (message, *args) # type: ignore
36
+ # Propagate the exception
37
+ span: Span = get_current_span()
38
+ # If not otel is setup, INVALID_SPAN is retrive from get_current_span
39
+ # and it will respond False to the is_recording method
40
+ if span.is_recording():
41
+ span.record_exception(self)
42
+ # Call the parent class
43
+ super().__init__(*args, **kwargs)
@@ -1,6 +1,7 @@
1
1
  """Package for plugins."""
2
2
 
3
3
  from enum import StrEnum, auto
4
+ from typing import Any
4
5
 
5
6
 
6
7
  class PluginsEnum(StrEnum):
@@ -8,6 +9,26 @@ class PluginsEnum(StrEnum):
8
9
 
9
10
  OPENTELEMETRY_PLUGIN = auto()
10
11
  ODM_PLUGIN = auto()
12
+ HTTPX_PLUGIN = auto()
13
+
14
+
15
+ class PluginState:
16
+ """PluginState represent the state a plugin which to be share to the application and other plugins."""
17
+
18
+ def __init__(self, key: str, value: Any) -> None:
19
+ """Initialize the PluginState."""
20
+ self._key: str = key
21
+ self._value = value
22
+
23
+ @property
24
+ def key(self) -> str:
25
+ """Get the key."""
26
+ return self._key
27
+
28
+ @property
29
+ def value(self) -> Any:
30
+ """Get the value."""
31
+ return self._value
11
32
 
12
33
 
13
34
  __all__: list[str] = [
@@ -0,0 +1,31 @@
1
+ """Example Plugin Module."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from fastapi_factory_utilities.core.plugins import PluginState
7
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
8
+
9
+
10
+ def pre_conditions_check(application: "ApplicationAbstractProtocol") -> bool: # pylint: disable=unused-argument
11
+ """Check the pre-conditions for the example plugin."""
12
+ return True
13
+
14
+
15
+ def on_load(
16
+ application: "ApplicationAbstractProtocol", # pylint: disable=unused-argument
17
+ ) -> list["PluginState"] | None:
18
+ """Actions to perform on load for the example plugin."""
19
+ return
20
+
21
+
22
+ async def on_startup(
23
+ application: "ApplicationAbstractProtocol", # pylint: disable=unused-argument
24
+ ) -> list["PluginState"] | None:
25
+ """Actions to perform on startup for the example plugin."""
26
+ return
27
+
28
+
29
+ async def on_shutdown(application: "ApplicationAbstractProtocol") -> None: # pylint: disable=unused-argument
30
+ """Actions to perform on shutdown for the example plugin."""
31
+ return
@@ -0,0 +1,31 @@
1
+ """Httpx Plugin Module."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from fastapi_factory_utilities.core.plugins import PluginState
7
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
8
+
9
+
10
+ def pre_conditions_check(application: "ApplicationAbstractProtocol") -> bool: # pylint: disable=unused-argument
11
+ """Check the pre-conditions for the example plugin."""
12
+ return True
13
+
14
+
15
+ def on_load(
16
+ application: "ApplicationAbstractProtocol", # pylint: disable=unused-argument
17
+ ) -> list["PluginState"] | None:
18
+ """Actions to perform on load for the example plugin."""
19
+ return
20
+
21
+
22
+ async def on_startup(
23
+ application: "ApplicationAbstractProtocol", # pylint: disable=unused-argument
24
+ ) -> list["PluginState"] | None:
25
+ """Actions to perform on startup for the example plugin."""
26
+ return
27
+
28
+
29
+ async def on_shutdown(application: "ApplicationAbstractProtocol") -> None: # pylint: disable=unused-argument
30
+ """Actions to perform on shutdown for the example plugin."""
31
+ return
@@ -5,16 +5,28 @@ from typing import Any
5
5
 
6
6
  from beanie import init_beanie # pyright: ignore[reportUnknownVariableType]
7
7
  from motor.motor_asyncio import AsyncIOMotorClient
8
+ from reactivex import Subject
8
9
  from structlog.stdlib import BoundLogger, get_logger
9
10
 
10
- from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
11
+ from fastapi_factory_utilities.core.plugins import PluginState
12
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
13
+ from fastapi_factory_utilities.core.services.status.enums import (
14
+ ComponentTypeEnum,
15
+ HealthStatusEnum,
16
+ ReadinessStatusEnum,
17
+ )
18
+ from fastapi_factory_utilities.core.services.status.services import StatusService
19
+ from fastapi_factory_utilities.core.services.status.types import (
20
+ ComponentInstanceType,
21
+ Status,
22
+ )
11
23
 
12
24
  from .builder import ODMBuilder
13
25
 
14
26
  _logger: BoundLogger = get_logger()
15
27
 
16
28
 
17
- def pre_conditions_check(application: BaseApplicationProtocol) -> bool:
29
+ def pre_conditions_check(application: ApplicationAbstractProtocol) -> bool:
18
30
  """Check the pre-conditions for the OpenTelemetry plugin.
19
31
 
20
32
  Args:
@@ -28,8 +40,8 @@ def pre_conditions_check(application: BaseApplicationProtocol) -> bool:
28
40
 
29
41
 
30
42
  def on_load(
31
- application: BaseApplicationProtocol,
32
- ) -> None:
43
+ application: ApplicationAbstractProtocol,
44
+ ) -> list["PluginState"] | None:
33
45
  """Actions to perform on load for the OpenTelemetry plugin.
34
46
 
35
47
  Args:
@@ -43,8 +55,8 @@ def on_load(
43
55
 
44
56
 
45
57
  async def on_startup(
46
- application: BaseApplicationProtocol,
47
- ) -> None:
58
+ application: ApplicationAbstractProtocol,
59
+ ) -> list["PluginState"] | None:
48
60
  """Actions to perform on startup for the ODM plugin.
49
61
 
50
62
  Args:
@@ -54,36 +66,77 @@ async def on_startup(
54
66
  Returns:
55
67
  None
56
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
+
57
79
  try:
58
80
  odm_factory: ODMBuilder = ODMBuilder(application=application).build_all()
59
81
  except Exception as exception: # pylint: disable=broad-except
60
82
  _logger.error(f"ODM plugin failed to start. {exception}")
61
- return
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
62
89
 
63
90
  if odm_factory.odm_database is None or odm_factory.odm_client is None:
64
91
  _logger.error(
65
92
  f"ODM plugin failed to start. Database: {odm_factory.odm_database} - " f"Client: {odm_factory.odm_client}"
66
93
  )
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
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
71
100
 
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,
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
+ ),
77
110
  )
78
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
+
79
128
  _logger.info(
80
129
  f"ODM plugin started. Database: {odm_factory.odm_database.name} - "
81
130
  f"Client: {odm_factory.odm_client.address} - "
82
131
  f"Document models: {application.ODM_DOCUMENT_MODELS}"
83
132
  )
84
133
 
134
+ monitoring_subject.on_next(value=Status(health=HealthStatusEnum.HEALTHY, readiness=ReadinessStatusEnum.READY))
135
+
136
+ return states
137
+
85
138
 
86
- async def on_shutdown(application: BaseApplicationProtocol) -> None:
139
+ async def on_shutdown(application: ApplicationAbstractProtocol) -> None:
87
140
  """Actions to perform on shutdown for the ODM plugin.
88
141
 
89
142
  Args:
@@ -92,6 +145,10 @@ async def on_shutdown(application: BaseApplicationProtocol) -> None:
92
145
  Returns:
93
146
  None
94
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
+
95
152
  client: AsyncIOMotorClient[Any] = application.get_asgi_app().state.odm_client
96
153
  client.close()
97
154
  _logger.debug("ODM plugin shutdown.")