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.

Files changed (53) 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-0.1.0.dist-info → fastapi_factory_utilities-0.2.0.dist-info}/METADATA +6 -4
  40. fastapi_factory_utilities-0.2.0.dist-info/RECORD +70 -0
  41. {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.0.dist-info}/WHEEL +1 -1
  42. fastapi_factory_utilities/core/app/base/__init__.py +0 -17
  43. fastapi_factory_utilities/core/app/base/application.py +0 -123
  44. fastapi_factory_utilities/core/app/base/config_abstract.py +0 -78
  45. fastapi_factory_utilities/core/app/base/exceptions.py +0 -25
  46. fastapi_factory_utilities/core/app/base/fastapi_application_abstract.py +0 -88
  47. fastapi_factory_utilities/core/app/base/plugins_manager_abstract.py +0 -136
  48. fastapi_factory_utilities/example/app/__init__.py +0 -6
  49. fastapi_factory_utilities/example/app/app.py +0 -37
  50. fastapi_factory_utilities/example/app/config.py +0 -12
  51. fastapi_factory_utilities-0.1.0.dist-info/RECORD +0 -58
  52. {fastapi_factory_utilities-0.1.0.dist-info → fastapi_factory_utilities-0.2.0.dist-info}/LICENSE +0 -0
  53. {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
- api_v1_sys_health = APIRouter(prefix="/health")
13
-
14
-
15
- class HealthStatusEnum(StrEnum):
16
- """Health status enum."""
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
- HEALTHY = "healthy"
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(response: Response) -> HealthResponseModel:
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
- response.status_code = HTTPStatus.OK
53
- return HealthResponseModel(status=HealthStatusEnum.HEALTHY)
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
- api_v1_sys_readiness = APIRouter(prefix="/readiness")
13
-
14
-
15
- class ReadinessStatusEnum(StrEnum):
16
- """Readiness status enum."""
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
- READY = "ready"
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(response: Response) -> ReadinessResponseModel:
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
- response.status_code = HTTPStatus.OK
53
- return ReadinessResponseModel(status=ReadinessStatusEnum.READY)
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 .base import (
4
- AppConfigAbstract,
5
- ApplicationConfigFactoryException,
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
- "BaseApplication",
14
- "AppConfigAbstract",
9
+ "BaseApplicationConfig",
15
10
  "EnvironmentEnum",
16
- "ApplicationConfigFactoryException",
17
- "ApplicationFactoryException",
18
- "BaseApplicationException",
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
+ )