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
@@ -1,13 +1,12 @@
1
1
  """Provides the module for the ODM plugin."""
2
2
 
3
- import asyncio
4
3
  import time
5
- from typing import Any, Self
4
+ from typing import Any, ClassVar, Self
6
5
 
7
6
  from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
8
7
  from structlog.stdlib import get_logger
9
8
 
10
- from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
9
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
11
10
  from fastapi_factory_utilities.core.utils.importlib import get_path_file_in_package
12
11
  from fastapi_factory_utilities.core.utils.yaml_reader import (
13
12
  UnableToReadYamlFileError,
@@ -38,9 +37,12 @@ class ODMBuilder:
38
37
 
39
38
  """
40
39
 
40
+ MS_TO_S: ClassVar[int] = 1000
41
+ SLEEP_TIME_S: ClassVar[float] = 0.001
42
+
41
43
  def __init__(
42
44
  self,
43
- application: BaseApplicationProtocol,
45
+ application: ApplicationAbstractProtocol,
44
46
  odm_config: ODMConfig | None = None,
45
47
  odm_client: AsyncIOMotorClient[Any] | None = None,
46
48
  odm_database: AsyncIOMotorDatabase[Any] | None = None,
@@ -54,7 +56,7 @@ class ODMBuilder:
54
56
  odm_database (AsyncIOMotorDatabase): The ODM database for injection. (Default is None)
55
57
 
56
58
  """
57
- self._application: BaseApplicationProtocol = application
59
+ self._application: ApplicationAbstractProtocol = application
58
60
  self._config: ODMConfig | None = odm_config
59
61
  self._odm_client: AsyncIOMotorClient[Any] | None = odm_client
60
62
  self._odm_database: AsyncIOMotorDatabase[Any] | None = odm_database
@@ -123,40 +125,30 @@ class ODMBuilder:
123
125
  return self
124
126
 
125
127
  @classmethod
126
- def _wait_client_to_be_ready(cls, client: AsyncIOMotorClient[Any], timeout_ms: int) -> None:
128
+ def _wait_client_to_be_ready(cls, client: AsyncIOMotorClient[Any], timeout_s: int) -> None:
127
129
  """Wait for the ODM client to be ready.
128
130
 
129
131
  Args:
130
132
  client (AsyncIOMotorClient): The ODM client.
131
- timeout_ms (int): The timeout in milliseconds.
133
+ timeout_s (int): The timeout in seconds.
132
134
 
133
135
  Raises:
134
- ODMPluginConfigError: If the ODM client is not ready.
136
+ TimeoutError: If the ODM client is not ready in the given timeout.
135
137
  """
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.")
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.")
160
152
 
161
153
  def build_client(
162
154
  self,
@@ -181,11 +173,11 @@ class ODMBuilder:
181
173
  self._odm_client = AsyncIOMotorClient(
182
174
  host=self._config.uri,
183
175
  connect=True,
184
- connectTimeoutMS=self._config.connection_timeout_ms,
185
- serverSelectionTimeoutMS=self._config.connection_timeout_ms,
176
+ connectTimeoutMS=self._config.connection_timeout_s,
177
+ serverSelectionTimeoutMS=self._config.connection_timeout_s,
186
178
  )
187
179
 
188
- self._wait_client_to_be_ready(client=self._odm_client, timeout_ms=self._config.connection_timeout_ms)
180
+ self._wait_client_to_be_ready(client=self._odm_client, timeout_s=self._config.connection_timeout_s)
189
181
 
190
182
  return self
191
183
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  from pydantic import BaseModel, ConfigDict
4
4
 
5
- S_TO_MS = 1000
6
-
7
5
 
8
6
  class ODMConfig(BaseModel):
9
7
  """Provides the configuration model for the ODM plugin."""
@@ -14,4 +12,4 @@ class ODMConfig(BaseModel):
14
12
 
15
13
  database: str = "test"
16
14
 
17
- connection_timeout_ms: int = 1 * S_TO_MS
15
+ connection_timeout_s: int = 10
@@ -0,0 +1,30 @@
1
+ """Provide FastAPI dependency for ODM."""
2
+
3
+ from typing import Any
4
+
5
+ from fastapi import Request
6
+ from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
7
+
8
+
9
+ def depends_odm_client(request: Request) -> AsyncIOMotorClient[Any]:
10
+ """Acquire the ODM client from the request.
11
+
12
+ Args:
13
+ request (Request): The request.
14
+
15
+ Returns:
16
+ AsyncIOMotorClient: The ODM client.
17
+ """
18
+ return request.app.state.odm_client
19
+
20
+
21
+ def depends_odm_database(request: Request) -> AsyncIOMotorDatabase[Any]:
22
+ """Acquire the ODM database from the request.
23
+
24
+ Args:
25
+ request (Request): The request.
26
+
27
+ Returns:
28
+ AsyncIOMotorClient: The ODM database.
29
+ """
30
+ return request.app.state.odm_database
@@ -10,7 +10,7 @@ from opentelemetry.sdk.metrics import MeterProvider
10
10
  from opentelemetry.sdk.trace import TracerProvider
11
11
  from structlog.stdlib import BoundLogger, get_logger
12
12
 
13
- from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
13
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
14
14
 
15
15
  from .builder import OpenTelemetryPluginBuilder
16
16
  from .configs import OpenTelemetryConfig
@@ -26,7 +26,7 @@ __all__: list[str] = [
26
26
  _logger: BoundLogger = get_logger()
27
27
 
28
28
 
29
- def pre_conditions_check(application: BaseApplicationProtocol) -> bool:
29
+ def pre_conditions_check(application: ApplicationAbstractProtocol) -> bool:
30
30
  """Check the pre-conditions for the OpenTelemetry plugin.
31
31
 
32
32
  Args:
@@ -40,7 +40,7 @@ def pre_conditions_check(application: BaseApplicationProtocol) -> bool:
40
40
 
41
41
 
42
42
  def on_load(
43
- application: BaseApplicationProtocol,
43
+ application: ApplicationAbstractProtocol,
44
44
  ) -> None:
45
45
  """Actions to perform on load for the OpenTelemetry plugin.
46
46
 
@@ -71,7 +71,7 @@ def on_load(
71
71
 
72
72
 
73
73
  async def on_startup(
74
- application: BaseApplicationProtocol,
74
+ application: ApplicationAbstractProtocol,
75
75
  ) -> None:
76
76
  """Actions to perform on startup for the OpenTelemetry plugin.
77
77
 
@@ -85,7 +85,7 @@ async def on_startup(
85
85
  _logger.debug("OpenTelemetry plugin started.")
86
86
 
87
87
 
88
- async def on_shutdown(application: BaseApplicationProtocol) -> None:
88
+ async def on_shutdown(application: ApplicationAbstractProtocol) -> None:
89
89
  """Actions to perform on shutdown for the OpenTelemetry plugin.
90
90
 
91
91
  Args:
@@ -24,7 +24,7 @@ from fastapi_factory_utilities.core.plugins.opentelemetry_plugin.configs import
24
24
  OpenTelemetryMeterConfig,
25
25
  OpenTelemetryTracerConfig,
26
26
  )
27
- from fastapi_factory_utilities.core.protocols import BaseApplicationProtocol
27
+ from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
28
28
  from fastapi_factory_utilities.core.utils.importlib import get_path_file_in_package
29
29
  from fastapi_factory_utilities.core.utils.yaml_reader import (
30
30
  UnableToReadYamlFileError,
@@ -38,13 +38,13 @@ from .exceptions import OpenTelemetryPluginConfigError
38
38
  class OpenTelemetryPluginBuilder:
39
39
  """Configure the injection bindings for OpenTelemetryPlugin."""
40
40
 
41
- def __init__(self, application: BaseApplicationProtocol) -> None:
41
+ def __init__(self, application: ApplicationAbstractProtocol) -> None:
42
42
  """Instantiate the OpenTelemetryPluginFactory.
43
43
 
44
44
  Args:
45
45
  application (BaseApplicationProtocol): The application object.
46
46
  """
47
- self._application: BaseApplicationProtocol = application
47
+ self._application: ApplicationAbstractProtocol = application
48
48
  self._resource: Resource | None = None
49
49
  self._config: OpenTelemetryConfig | None = None
50
50
  self._meter_provider: MeterProvider | None = None
@@ -94,10 +94,10 @@ class OpenTelemetryPluginBuilder:
94
94
  """
95
95
  self._resource = Resource(
96
96
  attributes={
97
- DEPLOYMENT_ENVIRONMENT: self._application.get_config().environment.value,
98
- SERVICE_NAME: self._application.get_config().service_name,
99
- SERVICE_NAMESPACE: self._application.get_config().service_namespace,
100
- SERVICE_VERSION: self._application.get_config().version,
97
+ DEPLOYMENT_ENVIRONMENT: self._application.get_config().application.environment.value,
98
+ SERVICE_NAME: self._application.get_config().application.service_name,
99
+ SERVICE_NAMESPACE: self._application.get_config().application.service_namespace,
100
+ SERVICE_VERSION: self._application.get_config().application.version,
101
101
  }
102
102
  )
103
103
  return self
@@ -6,39 +6,42 @@ from typing import TYPE_CHECKING, ClassVar, Protocol, runtime_checkable
6
6
  from beanie import Document
7
7
  from fastapi import FastAPI
8
8
 
9
+ from fastapi_factory_utilities.core.plugins import PluginsEnum
10
+ from fastapi_factory_utilities.core.services.status.services import StatusService
11
+
9
12
  if TYPE_CHECKING:
10
- from fastapi_factory_utilities.core.app.base.config_abstract import (
11
- AppConfigAbstract,
12
- )
13
+ from fastapi_factory_utilities.core.app.config import RootConfig
14
+ from fastapi_factory_utilities.core.plugins import PluginState
13
15
 
14
16
 
15
- class BaseApplicationProtocol(Protocol):
17
+ class ApplicationAbstractProtocol(Protocol):
16
18
  """Protocol for the base application."""
17
19
 
18
- PACKAGE_NAME: str
20
+ PACKAGE_NAME: ClassVar[str]
19
21
 
20
22
  ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]]
21
23
 
24
+ DEFAULT_PLUGINS_ACTIVATED: ClassVar[list[PluginsEnum]]
25
+
22
26
  @abstractmethod
23
- def get_config(self) -> "AppConfigAbstract":
27
+ def get_config(self) -> "RootConfig":
24
28
  """Get the application configuration."""
25
29
 
26
30
  @abstractmethod
27
31
  def get_asgi_app(self) -> FastAPI:
28
32
  """Get the ASGI application."""
29
33
 
34
+ @abstractmethod
35
+ def get_status_service(self) -> StatusService:
36
+ """Get the status service."""
37
+
30
38
 
31
39
  @runtime_checkable
32
40
  class PluginProtocol(Protocol):
33
- """Defines the protocol for the plugin.
34
-
35
- Attributes:
36
- INJECTOR_MODULE (type[Module]): The module for the plugin.
37
-
38
- """
41
+ """Defines the protocol for the plugins."""
39
42
 
40
43
  @abstractmethod
41
- def pre_conditions_check(self, application: BaseApplicationProtocol) -> bool:
44
+ def pre_conditions_check(self, application: ApplicationAbstractProtocol) -> bool:
42
45
  """Check the pre-conditions for the plugin.
43
46
 
44
47
  Args:
@@ -49,7 +52,7 @@ class PluginProtocol(Protocol):
49
52
  """
50
53
 
51
54
  @abstractmethod
52
- def on_load(self, application: BaseApplicationProtocol) -> None:
55
+ def on_load(self, application: ApplicationAbstractProtocol) -> list["PluginState"] | None:
53
56
  """The actions to perform on load for the plugin.
54
57
 
55
58
  Args:
@@ -60,7 +63,7 @@ class PluginProtocol(Protocol):
60
63
  """
61
64
 
62
65
  @abstractmethod
63
- async def on_startup(self, application: BaseApplicationProtocol) -> None:
66
+ async def on_startup(self, application: ApplicationAbstractProtocol) -> list["PluginState"] | None:
64
67
  """The actions to perform on startup for the plugin.
65
68
 
66
69
  Args:
@@ -71,7 +74,7 @@ class PluginProtocol(Protocol):
71
74
  """
72
75
 
73
76
  @abstractmethod
74
- async def on_shutdown(self, application: BaseApplicationProtocol) -> None:
77
+ async def on_shutdown(self, application: ApplicationAbstractProtocol) -> None:
75
78
  """The actions to perform on shutdown for the plugin.
76
79
 
77
80
  Args:
@@ -0,0 +1,14 @@
1
+ """Package covering the StatusService."""
2
+
3
+ from .enums import ComponentTypeEnum, HealthStatusEnum, ReadinessStatusEnum
4
+ from .services import StatusService
5
+ from .types import ComponentInstanceType, Status
6
+
7
+ __all__: list[str] = [
8
+ "ComponentTypeEnum",
9
+ "ComponentInstanceType",
10
+ "HealthStatusEnum",
11
+ "ReadinessStatusEnum",
12
+ "StatusService",
13
+ "Status",
14
+ ]
@@ -0,0 +1,30 @@
1
+ """Provides the status enums for the service."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class HealthStatusEnum(StrEnum):
7
+ """Health status enum."""
8
+
9
+ HEALTHY = "healthy"
10
+ UNHEALTHY = "unhealthy"
11
+
12
+
13
+ class ReadinessStatusEnum(StrEnum):
14
+ """Readiness status enum."""
15
+
16
+ READY = "ready"
17
+ NOT_READY = "not_ready"
18
+
19
+
20
+ class ComponentTypeEnum(StrEnum):
21
+ """Component type enum."""
22
+
23
+ SERVICE = "service"
24
+ DATABASE = "database"
25
+ CACHE = "cache"
26
+ STORAGE = "storage"
27
+ MESSAGE_BROKER = "message_broker"
28
+ SEARCH_ENGINE = "search_engine"
29
+ TASK_QUEUE = "task_queue"
30
+ OTHER = "other"
@@ -0,0 +1,27 @@
1
+ """Provides exceptions for the status service."""
2
+
3
+ from fastapi_factory_utilities.core.exceptions import FastAPIFactoryUtilitiesError
4
+ from fastapi_factory_utilities.core.services.status.types import ComponentInstanceType
5
+
6
+
7
+ class StatusServiceError(FastAPIFactoryUtilitiesError):
8
+ """Status service error."""
9
+
10
+
11
+ class ComponentRegistrationError(StatusServiceError):
12
+ """Component registration error."""
13
+
14
+ def __init__(
15
+ self,
16
+ component_instance: ComponentInstanceType,
17
+ ) -> None:
18
+ """Initialize the component registration error.
19
+
20
+ Args:
21
+ component_instance (ComponentInstanceType): The component instance.
22
+
23
+ """
24
+ super().__init__(
25
+ message="An error occurred while registering the component instance.",
26
+ component_instance=component_instance, # type: ignore
27
+ )
@@ -0,0 +1,48 @@
1
+ """Health calculator strategies."""
2
+
3
+ from typing import Protocol, runtime_checkable
4
+
5
+ from .enums import HealthStatusEnum
6
+ from .types import ComponentInstanceKey, Status
7
+
8
+
9
+ @runtime_checkable
10
+ class HealthCalculatorStrategy(Protocol):
11
+ """Health calculator strategy."""
12
+
13
+ def __init__(self, components_status: dict[ComponentInstanceKey, Status]) -> None:
14
+ """Initialize the health calculator.
15
+
16
+ Args:
17
+ components_status (dict[str, Status]): The components status.
18
+ """
19
+ raise NotImplementedError
20
+
21
+ def calculate(self) -> HealthStatusEnum:
22
+ """Calculate the health status."""
23
+ raise NotImplementedError
24
+
25
+
26
+ class HealthSimpleCalculatorStrategy:
27
+ """Health calculator.
28
+
29
+ This class calculates the health status based on the components status.
30
+ It's a simple implementation that returns unhealthy if at least one component is unhealthy for now.
31
+ We want to enhance this class to support more complex health calculation in the future.
32
+ ( )
33
+ """
34
+
35
+ def __init__(self, components_status: dict[ComponentInstanceKey, Status]) -> None:
36
+ """Initialize the health calculator.
37
+
38
+ Args:
39
+ components_status (dict[str, Status]): The components status.
40
+ """
41
+ self._components_status: dict[ComponentInstanceKey, Status] = components_status
42
+
43
+ def calculate(self) -> HealthStatusEnum:
44
+ """Calculate the health status."""
45
+ for status in self._components_status.values():
46
+ if status["health"] != HealthStatusEnum.HEALTHY:
47
+ return HealthStatusEnum.UNHEALTHY
48
+ return HealthStatusEnum.HEALTHY
@@ -0,0 +1,41 @@
1
+ """Readiness calculator strategies."""
2
+
3
+ from typing import Protocol
4
+
5
+ from .enums import ReadinessStatusEnum
6
+ from .types import ComponentInstanceKey, Status
7
+
8
+
9
+ class ReadinessCalculatorStrategy(Protocol):
10
+ """Readiness calculator strategy."""
11
+
12
+ def __init__(self, components_status: dict[ComponentInstanceKey, Status]) -> None:
13
+ """Initialize the readiness calculator.
14
+
15
+ Args:
16
+ components_status (dict[str, Status]): The components status.
17
+ """
18
+ raise NotImplementedError
19
+
20
+ def calculate(self) -> ReadinessStatusEnum:
21
+ """Calculate the readiness status."""
22
+ raise NotImplementedError
23
+
24
+
25
+ class ReadinessSimpleCalculatorStrategy:
26
+ """Readiness calculator."""
27
+
28
+ def __init__(self, components_status: dict[ComponentInstanceKey, Status]) -> None:
29
+ """Initialize the readiness calculator.
30
+
31
+ Args:
32
+ components_status (dict[str, Status]): The components status.
33
+ """
34
+ self._components_status: dict[ComponentInstanceKey, Status] = components_status
35
+
36
+ def calculate(self) -> ReadinessStatusEnum:
37
+ """Calculate the readiness status."""
38
+ for status in self._components_status.values():
39
+ if status["readiness"] != ReadinessStatusEnum.READY:
40
+ return ReadinessStatusEnum.NOT_READY
41
+ return ReadinessStatusEnum.READY