fastapi-factory-utilities 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fastapi-factory-utilities might be problematic. Click here for more details.
- fastapi_factory_utilities/core/api/v1/sys/health.py +56 -12
- fastapi_factory_utilities/core/api/v1/sys/readiness.py +19 -12
- fastapi_factory_utilities/core/app/__init__.py +7 -12
- fastapi_factory_utilities/core/app/application.py +133 -0
- fastapi_factory_utilities/core/app/builder.py +123 -0
- fastapi_factory_utilities/core/app/config.py +164 -0
- fastapi_factory_utilities/core/app/exceptions.py +20 -0
- fastapi_factory_utilities/core/app/fastapi_builder.py +85 -0
- fastapi_factory_utilities/core/app/plugin_manager/__init__.py +15 -0
- fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +33 -0
- fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +190 -0
- fastapi_factory_utilities/core/exceptions.py +43 -0
- fastapi_factory_utilities/core/plugins/__init__.py +21 -0
- fastapi_factory_utilities/core/plugins/example/__init__.py +31 -0
- fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +31 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +74 -17
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +27 -35
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -3
- fastapi_factory_utilities/core/plugins/odm_plugin/depends.py +30 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +5 -5
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/builder.py +7 -7
- fastapi_factory_utilities/core/protocols.py +19 -16
- fastapi_factory_utilities/core/services/status/__init__.py +14 -0
- fastapi_factory_utilities/core/services/status/enums.py +30 -0
- fastapi_factory_utilities/core/services/status/exceptions.py +27 -0
- fastapi_factory_utilities/core/services/status/health_calculator_strategies.py +48 -0
- fastapi_factory_utilities/core/services/status/readiness_calculator_strategies.py +41 -0
- fastapi_factory_utilities/core/services/status/services.py +218 -0
- fastapi_factory_utilities/core/services/status/types.py +128 -0
- fastapi_factory_utilities/core/utils/configs.py +1 -1
- fastapi_factory_utilities/core/utils/status.py +71 -0
- fastapi_factory_utilities/core/utils/uvicorn.py +7 -8
- fastapi_factory_utilities/example/__init__.py +3 -3
- fastapi_factory_utilities/example/api/books/routes.py +7 -10
- fastapi_factory_utilities/example/app.py +50 -0
- fastapi_factory_utilities/example/application.yaml +5 -9
- fastapi_factory_utilities/example/services/books/__init__.py +2 -2
- fastapi_factory_utilities/example/services/books/services.py +9 -0
- {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.0.dist-info}/METADATA +6 -4
- fastapi_factory_utilities-0.2.0.dist-info/RECORD +70 -0
- {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.0.dist-info}/WHEEL +1 -1
- fastapi_factory_utilities/core/app/base/__init__.py +0 -17
- fastapi_factory_utilities/core/app/base/application.py +0 -123
- fastapi_factory_utilities/core/app/base/config_abstract.py +0 -78
- fastapi_factory_utilities/core/app/base/exceptions.py +0 -25
- fastapi_factory_utilities/core/app/base/fastapi_application_abstract.py +0 -88
- fastapi_factory_utilities/core/app/base/plugins_manager_abstract.py +0 -136
- fastapi_factory_utilities/example/app/__init__.py +0 -6
- fastapi_factory_utilities/example/app/app.py +0 -37
- fastapi_factory_utilities/example/app/config.py +0 -12
- fastapi_factory_utilities-0.1.0.dist-info/RECORD +0 -58
- {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.0.dist-info}/LICENSE +0 -0
- {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -3,20 +3,19 @@
|
|
|
3
3
|
Provide the Get health endpoint
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from enum import StrEnum
|
|
7
6
|
from http import HTTPStatus
|
|
8
7
|
|
|
9
|
-
from fastapi import APIRouter, Response
|
|
8
|
+
from fastapi import APIRouter, Depends, Response
|
|
10
9
|
from pydantic import BaseModel
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
from fastapi_factory_utilities.core.services.status.enums import HealthStatusEnum
|
|
12
|
+
from fastapi_factory_utilities.core.services.status.services import (
|
|
13
|
+
ComponentInstanceKey,
|
|
14
|
+
StatusService,
|
|
15
|
+
depends_status_service,
|
|
16
|
+
)
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
UNHEALTHY = "unhealthy"
|
|
18
|
+
api_v1_sys_health = APIRouter(prefix="/health")
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
class HealthResponseModel(BaseModel):
|
|
@@ -40,14 +39,59 @@ class HealthResponseModel(BaseModel):
|
|
|
40
39
|
},
|
|
41
40
|
},
|
|
42
41
|
)
|
|
43
|
-
def get_api_v1_sys_health(
|
|
42
|
+
def get_api_v1_sys_health(
|
|
43
|
+
response: Response, status_service: StatusService = Depends(depends_status_service)
|
|
44
|
+
) -> HealthResponseModel:
|
|
44
45
|
"""Get the health of the system.
|
|
45
46
|
|
|
46
47
|
Args:
|
|
47
48
|
response (Response): The response object.
|
|
49
|
+
status_service (StatusService): The status service.
|
|
48
50
|
|
|
49
51
|
Returns:
|
|
50
52
|
HealthResponse: The health status.
|
|
51
53
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
status: HealthStatusEnum = status_service.get_status()["health"]
|
|
55
|
+
match status:
|
|
56
|
+
case HealthStatusEnum.HEALTHY:
|
|
57
|
+
response.status_code = HTTPStatus.OK.value
|
|
58
|
+
case HealthStatusEnum.UNHEALTHY:
|
|
59
|
+
response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
|
|
60
|
+
return HealthResponseModel(status=status)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class ComponentHealthResponseModel(BaseModel):
|
|
64
|
+
"""Component health response schema."""
|
|
65
|
+
|
|
66
|
+
components: dict[ComponentInstanceKey, HealthStatusEnum]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@api_v1_sys_health.get(
|
|
70
|
+
path="/components",
|
|
71
|
+
tags=["sys"],
|
|
72
|
+
response_model=ComponentHealthResponseModel,
|
|
73
|
+
responses={
|
|
74
|
+
HTTPStatus.OK.value: {
|
|
75
|
+
"model": ComponentHealthResponseModel,
|
|
76
|
+
"description": "Health status of all components.",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
)
|
|
80
|
+
def get_api_v1_sys_components_health(
|
|
81
|
+
status_service: StatusService = Depends(depends_status_service),
|
|
82
|
+
) -> ComponentHealthResponseModel:
|
|
83
|
+
"""Get the health of all components.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
status_service (StatusService): The status service.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
list[ComponentHealthResponseModel]: The health status of all components.
|
|
90
|
+
"""
|
|
91
|
+
components_dict: dict[ComponentInstanceKey, HealthStatusEnum] = {}
|
|
92
|
+
|
|
93
|
+
for _, components in status_service.get_components_status_by_type().items():
|
|
94
|
+
for key, status in components.items():
|
|
95
|
+
components_dict[key] = status["health"]
|
|
96
|
+
|
|
97
|
+
return ComponentHealthResponseModel(components=dict(components_dict))
|
|
@@ -3,20 +3,18 @@
|
|
|
3
3
|
Provide the Get readiness endpoint
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from enum import StrEnum
|
|
7
6
|
from http import HTTPStatus
|
|
8
7
|
|
|
9
|
-
from fastapi import APIRouter, Response
|
|
8
|
+
from fastapi import APIRouter, Depends, Response
|
|
10
9
|
from pydantic import BaseModel
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
from fastapi_factory_utilities.core.services.status.enums import ReadinessStatusEnum
|
|
12
|
+
from fastapi_factory_utilities.core.services.status.services import (
|
|
13
|
+
StatusService,
|
|
14
|
+
depends_status_service,
|
|
15
|
+
)
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
NOT_READY = "not_ready"
|
|
17
|
+
api_v1_sys_readiness = APIRouter(prefix="/readiness")
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
class ReadinessResponseModel(BaseModel):
|
|
@@ -40,14 +38,23 @@ class ReadinessResponseModel(BaseModel):
|
|
|
40
38
|
},
|
|
41
39
|
},
|
|
42
40
|
)
|
|
43
|
-
def get_api_v1_sys_readiness(
|
|
41
|
+
def get_api_v1_sys_readiness(
|
|
42
|
+
response: Response, status_service: StatusService = Depends(depends_status_service)
|
|
43
|
+
) -> ReadinessResponseModel:
|
|
44
44
|
"""Get the readiness of the system.
|
|
45
45
|
|
|
46
46
|
Args:
|
|
47
47
|
response (Response): The response object.
|
|
48
|
+
status_service (StatusService): The status service
|
|
48
49
|
|
|
49
50
|
Returns:
|
|
50
51
|
ReadinessResponse: The readiness status.
|
|
51
52
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
status: ReadinessStatusEnum = status_service.get_status()["readiness"]
|
|
54
|
+
|
|
55
|
+
match status:
|
|
56
|
+
case ReadinessStatusEnum.READY:
|
|
57
|
+
response.status_code = HTTPStatus.OK.value
|
|
58
|
+
case ReadinessStatusEnum.NOT_READY:
|
|
59
|
+
response.status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
|
|
60
|
+
return ReadinessResponseModel(status=status)
|
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
"""Provides the core application module for the Python Factory."""
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
ApplicationFactoryException,
|
|
7
|
-
BaseApplication,
|
|
8
|
-
BaseApplicationException,
|
|
9
|
-
)
|
|
3
|
+
from .application import ApplicationAbstract
|
|
4
|
+
from .builder import ApplicationGenericBuilder
|
|
5
|
+
from .config import BaseApplicationConfig, RootConfig
|
|
10
6
|
from .enums import EnvironmentEnum
|
|
11
7
|
|
|
12
8
|
__all__: list[str] = [
|
|
13
|
-
"
|
|
14
|
-
"AppConfigAbstract",
|
|
9
|
+
"BaseApplicationConfig",
|
|
15
10
|
"EnvironmentEnum",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
11
|
+
"ApplicationAbstract",
|
|
12
|
+
"ApplicationGenericBuilder",
|
|
13
|
+
"RootConfig",
|
|
19
14
|
]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Provides the ApplicationAbstract class."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
from contextlib import asynccontextmanager
|
|
6
|
+
from typing import Any, ClassVar
|
|
7
|
+
|
|
8
|
+
from beanie import Document
|
|
9
|
+
from fastapi import FastAPI
|
|
10
|
+
from structlog.stdlib import BoundLogger, get_logger
|
|
11
|
+
|
|
12
|
+
from fastapi_factory_utilities.core.api import api
|
|
13
|
+
from fastapi_factory_utilities.core.app.config import RootConfig
|
|
14
|
+
from fastapi_factory_utilities.core.app.fastapi_builder import FastAPIBuilder
|
|
15
|
+
from fastapi_factory_utilities.core.app.plugin_manager.plugin_manager import (
|
|
16
|
+
PluginManager,
|
|
17
|
+
)
|
|
18
|
+
from fastapi_factory_utilities.core.plugins import PluginsEnum
|
|
19
|
+
from fastapi_factory_utilities.core.services.status.services import StatusService
|
|
20
|
+
|
|
21
|
+
_logger: BoundLogger = get_logger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ApplicationAbstract(ABC):
|
|
25
|
+
"""Application abstract class."""
|
|
26
|
+
|
|
27
|
+
PACKAGE_NAME: ClassVar[str]
|
|
28
|
+
|
|
29
|
+
CONFIG_CLASS: ClassVar[type[RootConfig]] = RootConfig
|
|
30
|
+
|
|
31
|
+
# TODO: Find a way to remove this from here
|
|
32
|
+
ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]]
|
|
33
|
+
|
|
34
|
+
DEFAULT_PLUGINS_ACTIVATED: ClassVar[list[PluginsEnum]] = []
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
root_config: RootConfig,
|
|
39
|
+
plugin_manager: PluginManager,
|
|
40
|
+
fastapi_builder: FastAPIBuilder,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Instantiate the application."""
|
|
43
|
+
self.config: RootConfig = root_config
|
|
44
|
+
self.fastapi_builder: FastAPIBuilder = fastapi_builder
|
|
45
|
+
# Add the API router to the FastAPI application
|
|
46
|
+
self.fastapi_builder.add_api_router(router=api, without_resource_path=True)
|
|
47
|
+
self.plugin_manager: PluginManager = plugin_manager
|
|
48
|
+
self._add_to_state: dict[str, Any] = {}
|
|
49
|
+
|
|
50
|
+
def setup(self) -> None:
|
|
51
|
+
"""Initialize the application."""
|
|
52
|
+
# Initialize FastAPI application
|
|
53
|
+
self._asgi_app: FastAPI = self.fastapi_builder.build(lifespan=self.fastapi_lifespan)
|
|
54
|
+
# Status service
|
|
55
|
+
self.status_service: StatusService = StatusService()
|
|
56
|
+
self.add_to_state(key="status_service", value=self.status_service)
|
|
57
|
+
self._apply_states_to_fastapi_app()
|
|
58
|
+
# Configure the application
|
|
59
|
+
self.configure()
|
|
60
|
+
self._apply_states_to_fastapi_app()
|
|
61
|
+
# Initialize PluginManager
|
|
62
|
+
self.plugin_manager.add_application_context(application=self)
|
|
63
|
+
self.plugin_manager.load()
|
|
64
|
+
# Add the states to the FastAPI app
|
|
65
|
+
self._import_states_from_plugin_manager()
|
|
66
|
+
|
|
67
|
+
def _apply_states_to_fastapi_app(self) -> None:
|
|
68
|
+
# Add manually added states to the FastAPI app
|
|
69
|
+
for key, value in self._add_to_state.items():
|
|
70
|
+
if hasattr(self._asgi_app.state, key):
|
|
71
|
+
_logger.warn(f"Key {key} already exists in the state. Don't set it outside of the application.")
|
|
72
|
+
setattr(self._asgi_app.state, key, value)
|
|
73
|
+
self._add_to_state.clear()
|
|
74
|
+
|
|
75
|
+
def _import_states_from_plugin_manager(self) -> None:
|
|
76
|
+
"""Import the states from the plugins."""
|
|
77
|
+
for state in self.plugin_manager.states:
|
|
78
|
+
self.add_to_state(key=state.key, value=state.value)
|
|
79
|
+
self.plugin_manager.clear_states()
|
|
80
|
+
self._apply_states_to_fastapi_app()
|
|
81
|
+
|
|
82
|
+
def add_to_state(self, key: str, value: Any) -> None:
|
|
83
|
+
"""Add a value to the FastAPI app state."""
|
|
84
|
+
if key in self._add_to_state:
|
|
85
|
+
raise ValueError(f"Key {key} already exists in the state.")
|
|
86
|
+
self._add_to_state[key] = value
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def configure(self) -> None:
|
|
90
|
+
"""Configure the application."""
|
|
91
|
+
raise NotImplementedError
|
|
92
|
+
|
|
93
|
+
@abstractmethod
|
|
94
|
+
async def on_startup(self) -> None:
|
|
95
|
+
"""Startup the application."""
|
|
96
|
+
raise NotImplementedError
|
|
97
|
+
|
|
98
|
+
@abstractmethod
|
|
99
|
+
async def on_shutdown(self) -> None:
|
|
100
|
+
"""Shutdown the application."""
|
|
101
|
+
raise NotImplementedError
|
|
102
|
+
|
|
103
|
+
@asynccontextmanager
|
|
104
|
+
async def fastapi_lifespan(self, fastapi: FastAPI) -> AsyncGenerator[None, None]: # pylint: disable=unused-argument
|
|
105
|
+
"""FastAPI lifespan context manager."""
|
|
106
|
+
await self.plugin_manager.trigger_startup()
|
|
107
|
+
self._import_states_from_plugin_manager()
|
|
108
|
+
await self.on_startup()
|
|
109
|
+
try:
|
|
110
|
+
yield
|
|
111
|
+
finally:
|
|
112
|
+
await self.on_shutdown()
|
|
113
|
+
await self.plugin_manager.trigger_shutdown()
|
|
114
|
+
|
|
115
|
+
def get_config(self) -> RootConfig:
|
|
116
|
+
"""Get the configuration."""
|
|
117
|
+
return self.config
|
|
118
|
+
|
|
119
|
+
def get_asgi_app(self) -> FastAPI:
|
|
120
|
+
"""Get the ASGI application."""
|
|
121
|
+
return self._asgi_app
|
|
122
|
+
|
|
123
|
+
def get_plugin_manager(self) -> PluginManager:
|
|
124
|
+
"""Get the plugin manager."""
|
|
125
|
+
return self.plugin_manager
|
|
126
|
+
|
|
127
|
+
def get_status_service(self) -> StatusService:
|
|
128
|
+
"""Get the status service."""
|
|
129
|
+
return self.status_service
|
|
130
|
+
|
|
131
|
+
async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
|
|
132
|
+
"""Forward the call to the FastAPI app."""
|
|
133
|
+
return await self._asgi_app.__call__(scope=scope, receive=receive, send=send)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Provide the ApplicationGenericBuilder class."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Generic, Self, TypeVar, get_args
|
|
4
|
+
|
|
5
|
+
from fastapi_factory_utilities.core.app.config import GenericConfigBuilder, RootConfig
|
|
6
|
+
from fastapi_factory_utilities.core.app.fastapi_builder import FastAPIBuilder
|
|
7
|
+
from fastapi_factory_utilities.core.app.plugin_manager.plugin_manager import (
|
|
8
|
+
PluginManager,
|
|
9
|
+
)
|
|
10
|
+
from fastapi_factory_utilities.core.plugins import PluginsEnum
|
|
11
|
+
from fastapi_factory_utilities.core.utils.log import LogModeEnum, setup_log
|
|
12
|
+
from fastapi_factory_utilities.core.utils.uvicorn import UvicornUtils
|
|
13
|
+
|
|
14
|
+
from .application import ApplicationAbstract
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T", bound=ApplicationAbstract)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ApplicationGenericBuilder(Generic[T]):
|
|
20
|
+
"""Application generic builder."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, plugins_activation_list: list[PluginsEnum] | None = None) -> None:
|
|
23
|
+
"""Instanciate the ApplicationGenericBuilder."""
|
|
24
|
+
self._root_config: RootConfig | None = None
|
|
25
|
+
self._plugin_manager: PluginManager | None = None
|
|
26
|
+
self._fastapi_builder: FastAPIBuilder | None = None
|
|
27
|
+
generic_args: tuple[Any, ...] = get_args(self.__orig_bases__[0]) # type: ignore
|
|
28
|
+
self._application_class: type[T] = generic_args[0]
|
|
29
|
+
self._plugins_activation_list: list[PluginsEnum]
|
|
30
|
+
if plugins_activation_list is None:
|
|
31
|
+
self._plugins_activation_list = self._application_class.DEFAULT_PLUGINS_ACTIVATED
|
|
32
|
+
else:
|
|
33
|
+
self._plugins_activation_list = plugins_activation_list
|
|
34
|
+
|
|
35
|
+
def add_plugin_to_activate(self, plugin: PluginsEnum) -> Self:
|
|
36
|
+
"""Add a plugin to activate.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
plugin (PluginsEnum): The plugin to activate.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Self: The builder.
|
|
43
|
+
"""
|
|
44
|
+
self._plugins_activation_list.append(plugin)
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def add_config(self, config: RootConfig) -> Self:
|
|
48
|
+
"""Add the configuration to the builder.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
config (RootConfig): The configuration.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Self: The builder.
|
|
55
|
+
"""
|
|
56
|
+
self._root_config = config
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def add_plugin_manager(self, plugin_manager: PluginManager) -> Self:
|
|
60
|
+
"""Add the plugin manager to the builder.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
plugin_manager (PluginManager): The plugin manager.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Self: The builder.
|
|
67
|
+
"""
|
|
68
|
+
self._plugin_manager = plugin_manager
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def add_fastapi_builder(self, fastapi_builder: FastAPIBuilder) -> Self:
|
|
72
|
+
"""Add the FastAPI builder to the builder.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
fastapi_builder (FastAPIBuilder): The FastAPI builder.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Self: The builder.
|
|
79
|
+
"""
|
|
80
|
+
self._fastapi_builder = fastapi_builder
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def _build_from_package_root_config(self) -> RootConfig:
|
|
84
|
+
"""Build the configuration from the package."""
|
|
85
|
+
return GenericConfigBuilder[self._application_class.CONFIG_CLASS](
|
|
86
|
+
package_name=self._application_class.PACKAGE_NAME,
|
|
87
|
+
config_class=self._application_class.CONFIG_CLASS,
|
|
88
|
+
).build()
|
|
89
|
+
|
|
90
|
+
def build(self) -> T:
|
|
91
|
+
"""Build the application."""
|
|
92
|
+
# Plugin manager
|
|
93
|
+
if self._plugin_manager is None:
|
|
94
|
+
self._plugin_manager = PluginManager()
|
|
95
|
+
for plugin in self._plugins_activation_list:
|
|
96
|
+
self._plugin_manager.activate(plugin=plugin)
|
|
97
|
+
# RootConfig
|
|
98
|
+
self._root_config = self._root_config or self._build_from_package_root_config()
|
|
99
|
+
# FastAPIBuilder
|
|
100
|
+
self._fastapi_builder = self._fastapi_builder or FastAPIBuilder(root_config=self._root_config)
|
|
101
|
+
|
|
102
|
+
application: T = self._application_class(
|
|
103
|
+
root_config=self._root_config,
|
|
104
|
+
plugin_manager=self._plugin_manager,
|
|
105
|
+
fastapi_builder=self._fastapi_builder,
|
|
106
|
+
)
|
|
107
|
+
application.setup()
|
|
108
|
+
return application
|
|
109
|
+
|
|
110
|
+
def build_as_uvicorn_utils(self) -> UvicornUtils:
|
|
111
|
+
"""Build the application and provide UvicornUtils."""
|
|
112
|
+
return UvicornUtils(app=self.build())
|
|
113
|
+
|
|
114
|
+
def build_and_serve(self) -> None:
|
|
115
|
+
"""Build the application and serve it with Uvicorn."""
|
|
116
|
+
uvicorn_utils: UvicornUtils = self.build_as_uvicorn_utils()
|
|
117
|
+
|
|
118
|
+
setup_log(mode=LogModeEnum.CONSOLE)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
uvicorn_utils.serve()
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
pass
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Provide the configuration for the app server."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, ClassVar, Generic, TypeVar, get_args
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from fastapi_factory_utilities.core.app.exceptions import ConfigBuilderError
|
|
8
|
+
from fastapi_factory_utilities.core.utils.configs import (
|
|
9
|
+
UnableToReadConfigFileError,
|
|
10
|
+
ValueErrorConfigError,
|
|
11
|
+
build_config_from_file_in_package,
|
|
12
|
+
)
|
|
13
|
+
from fastapi_factory_utilities.core.utils.log import LoggingConfig
|
|
14
|
+
|
|
15
|
+
from .enums import EnvironmentEnum
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def default_allow_all() -> list[str]:
|
|
19
|
+
"""Default allow all."""
|
|
20
|
+
return ["*"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CorsConfig(BaseModel):
|
|
24
|
+
"""CORS configuration."""
|
|
25
|
+
|
|
26
|
+
allow_origins: list[str] = Field(default_factory=default_allow_all, description="Allowed origins")
|
|
27
|
+
allow_credentials: bool = Field(default=True, description="Allow credentials")
|
|
28
|
+
allow_methods: list[str] = Field(default_factory=default_allow_all, description="Allowed methods")
|
|
29
|
+
allow_headers: list[str] = Field(default_factory=default_allow_all, description="Allowed headers")
|
|
30
|
+
expose_headers: list[str] = Field(default_factory=list, description="Exposed headers")
|
|
31
|
+
max_age: int = Field(default=600, description="Max age")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ServerConfig(BaseModel):
|
|
35
|
+
"""Server configuration."""
|
|
36
|
+
|
|
37
|
+
# Pydantic configuration
|
|
38
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="forbid")
|
|
39
|
+
|
|
40
|
+
# Server configuration mainly used by uvicorn
|
|
41
|
+
host: str = Field(default="0.0.0.0", description="Server host")
|
|
42
|
+
port: int = Field(default=8000, description="Server port")
|
|
43
|
+
workers: int = Field(default=1, description="Number of workers")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DevelopmentConfig(BaseModel):
|
|
47
|
+
"""Development configuration."""
|
|
48
|
+
|
|
49
|
+
# Pydantic configuration
|
|
50
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="forbid")
|
|
51
|
+
|
|
52
|
+
# Development configuration
|
|
53
|
+
debug: bool = Field(default=False, description="Debug mode")
|
|
54
|
+
reload: bool = Field(default=False, description="Reload mode")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseApplicationConfig(BaseModel):
|
|
58
|
+
"""Application configuration abstract class."""
|
|
59
|
+
|
|
60
|
+
# Pydantic configuration
|
|
61
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="forbid")
|
|
62
|
+
|
|
63
|
+
# Application configuration
|
|
64
|
+
# (mainly used for monitoring and information reporting)
|
|
65
|
+
service_namespace: str = Field(description="Service namespace")
|
|
66
|
+
environment: EnvironmentEnum = Field(description="Deployed environment")
|
|
67
|
+
service_name: str = Field(description="Service name")
|
|
68
|
+
description: str = Field(description="Service description")
|
|
69
|
+
version: str = Field(description="Service version")
|
|
70
|
+
# Root path for the application
|
|
71
|
+
root_path: str = Field(default="", description="Root path")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RootConfig(BaseModel):
|
|
75
|
+
"""Root configuration."""
|
|
76
|
+
|
|
77
|
+
# Pydantic configuration
|
|
78
|
+
# extra = Extra.ignore, to be able to add extra categories for your application purposes
|
|
79
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="ignore")
|
|
80
|
+
|
|
81
|
+
# Root configuration with all sub configurations
|
|
82
|
+
application: BaseApplicationConfig = Field(description="Application configuration")
|
|
83
|
+
server: ServerConfig = Field(description="Server configuration", default_factory=ServerConfig)
|
|
84
|
+
cors: CorsConfig = Field(description="CORS configuration", default_factory=CorsConfig)
|
|
85
|
+
development: DevelopmentConfig = Field(description="Development configuration", default_factory=DevelopmentConfig)
|
|
86
|
+
logging: list[LoggingConfig] = Field(description="Logging configuration", default_factory=list)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
GenericConfig = TypeVar("GenericConfig", bound=BaseModel)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class GenericConfigBuilder(Generic[GenericConfig]):
|
|
93
|
+
"""Application configuration builder.
|
|
94
|
+
|
|
95
|
+
This class is used to build the application configuration from a YAML file.
|
|
96
|
+
It can be used to build any configuration model
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
DEFAULT_FILENAME: str = "application.yaml"
|
|
100
|
+
DEFAULT_YAML_BASE_KEY: str | None = None
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
package_name: str,
|
|
105
|
+
config_class: type[GenericConfig] | None = None,
|
|
106
|
+
filename: str = DEFAULT_FILENAME,
|
|
107
|
+
yaml_base_key: str | None = DEFAULT_YAML_BASE_KEY,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Instantiate the builder.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
package_name (str): The package name.
|
|
113
|
+
config_class (Type[AppConfigAbstract]): The configuration class.
|
|
114
|
+
filename (str, optional): The filename. Defaults to DEFAULT_FILENAME.
|
|
115
|
+
yaml_base_key (str, optional): The YAML base key. Defaults to DEFAULT_YAML_BASE_KEY.
|
|
116
|
+
|
|
117
|
+
TODO: prevent the double definition of config_class and through the generic type
|
|
118
|
+
"""
|
|
119
|
+
self.package_name: str = package_name
|
|
120
|
+
generic_args: tuple[Any, ...] = get_args(self.__orig_bases__[0]) # type: ignore
|
|
121
|
+
|
|
122
|
+
self.config_class: type[GenericConfig] = config_class if config_class is not None else generic_args[0]
|
|
123
|
+
self.filename: str = filename
|
|
124
|
+
self.yaml_base_key: str | None = yaml_base_key
|
|
125
|
+
|
|
126
|
+
def build(self) -> GenericConfig:
|
|
127
|
+
"""Build the configuration.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
GenericConfig: The configuration.
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
ApplicationConfigFactoryException: Any error occurred
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
config: GenericConfig = build_config_from_file_in_package(
|
|
137
|
+
package_name=self.package_name,
|
|
138
|
+
config_class=self.config_class,
|
|
139
|
+
filename=self.filename,
|
|
140
|
+
yaml_base_key=self.yaml_base_key,
|
|
141
|
+
)
|
|
142
|
+
except UnableToReadConfigFileError as exception:
|
|
143
|
+
raise ConfigBuilderError(
|
|
144
|
+
message="Unable to read the application configuration file.",
|
|
145
|
+
config_class=self.config_class,
|
|
146
|
+
package=self.package_name,
|
|
147
|
+
filename=self.filename,
|
|
148
|
+
) from exception
|
|
149
|
+
except ValueErrorConfigError as exception:
|
|
150
|
+
raise ConfigBuilderError(
|
|
151
|
+
message="Value error when creating the configuration object.",
|
|
152
|
+
config_class=self.config_class,
|
|
153
|
+
package=self.package_name,
|
|
154
|
+
filename=self.filename,
|
|
155
|
+
) from exception
|
|
156
|
+
except Exception as exception:
|
|
157
|
+
raise ConfigBuilderError(
|
|
158
|
+
message="An error occurred while building the application configuration.",
|
|
159
|
+
config_class=self.config_class,
|
|
160
|
+
package=self.package_name,
|
|
161
|
+
filename=self.filename,
|
|
162
|
+
) from exception
|
|
163
|
+
|
|
164
|
+
return config
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Provides the exceptions for the application factory."""
|
|
2
|
+
|
|
3
|
+
from ..exceptions import FastAPIFactoryUtilitiesError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseApplicationException(BaseException):
|
|
7
|
+
"""Base application exception."""
|
|
8
|
+
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigBuilderError(FastAPIFactoryUtilitiesError):
|
|
13
|
+
"""Application configuration factory exception."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, message: str, config_class: type, package: str, filename: str) -> None:
|
|
16
|
+
"""Instantiate the exception."""
|
|
17
|
+
super().__init__(
|
|
18
|
+
message=f"Unable to build the configuration for the package {package} and "
|
|
19
|
+
+ f"the file {filename} with the class {config_class} - {message}"
|
|
20
|
+
)
|