fastapi-factory-utilities 0.4.0__py3-none-any.whl → 0.8.3__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/__init__.py +1 -1
- fastapi_factory_utilities/core/api/v1/sys/health.py +1 -1
- fastapi_factory_utilities/core/app/__init__.py +4 -4
- fastapi_factory_utilities/core/app/application.py +22 -26
- fastapi_factory_utilities/core/app/builder.py +8 -32
- fastapi_factory_utilities/core/app/fastapi_builder.py +3 -2
- fastapi_factory_utilities/core/exceptions.py +64 -29
- fastapi_factory_utilities/core/plugins/__init__.py +2 -31
- fastapi_factory_utilities/core/plugins/abstracts.py +40 -0
- fastapi_factory_utilities/core/plugins/aiopika/__init__.py +25 -0
- fastapi_factory_utilities/core/plugins/aiopika/abstract.py +48 -0
- fastapi_factory_utilities/core/plugins/aiopika/configs.py +85 -0
- fastapi_factory_utilities/core/plugins/aiopika/depends.py +20 -0
- fastapi_factory_utilities/core/plugins/aiopika/exceptions.py +29 -0
- fastapi_factory_utilities/core/plugins/aiopika/exchange.py +69 -0
- fastapi_factory_utilities/core/plugins/aiopika/listener/__init__.py +7 -0
- fastapi_factory_utilities/core/plugins/aiopika/listener/abstract.py +72 -0
- fastapi_factory_utilities/core/plugins/aiopika/message.py +86 -0
- fastapi_factory_utilities/core/plugins/aiopika/plugins.py +84 -0
- fastapi_factory_utilities/core/plugins/aiopika/publisher/__init__.py +7 -0
- fastapi_factory_utilities/core/plugins/aiopika/publisher/abstract.py +66 -0
- fastapi_factory_utilities/core/plugins/aiopika/queue.py +88 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/__init__.py +14 -157
- fastapi_factory_utilities/core/plugins/odm_plugin/builder.py +3 -3
- fastapi_factory_utilities/core/plugins/odm_plugin/configs.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/documents.py +1 -1
- fastapi_factory_utilities/core/plugins/odm_plugin/helpers.py +16 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/plugins.py +155 -0
- fastapi_factory_utilities/core/plugins/odm_plugin/repositories.py +1 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/__init__.py +8 -121
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/instruments/__init__.py +85 -0
- fastapi_factory_utilities/core/plugins/opentelemetry_plugin/plugins.py +137 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/__init__.py +31 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/configs.py +12 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/depends.py +51 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/exceptions.py +13 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/plugin.py +41 -0
- fastapi_factory_utilities/core/plugins/taskiq_plugins/schedulers.py +187 -0
- fastapi_factory_utilities/core/protocols.py +1 -54
- fastapi_factory_utilities/core/security/__init__.py +5 -0
- fastapi_factory_utilities/core/security/abstracts.py +42 -0
- fastapi_factory_utilities/core/security/jwt/__init__.py +45 -0
- fastapi_factory_utilities/core/security/jwt/configs.py +32 -0
- fastapi_factory_utilities/core/security/jwt/decoders.py +130 -0
- fastapi_factory_utilities/core/security/jwt/exceptions.py +23 -0
- fastapi_factory_utilities/core/security/jwt/objects.py +107 -0
- fastapi_factory_utilities/core/security/jwt/services.py +176 -0
- fastapi_factory_utilities/core/security/jwt/stores.py +43 -0
- fastapi_factory_utilities/core/security/jwt/types.py +9 -0
- fastapi_factory_utilities/core/security/jwt/verifiers.py +46 -0
- fastapi_factory_utilities/core/security/kratos.py +43 -43
- fastapi_factory_utilities/core/services/hydra/__init__.py +10 -3
- fastapi_factory_utilities/core/services/hydra/services.py +112 -34
- fastapi_factory_utilities/core/services/status/__init__.py +2 -2
- fastapi_factory_utilities/core/services/status/exceptions.py +1 -1
- fastapi_factory_utilities/core/utils/status.py +2 -1
- fastapi_factory_utilities/core/utils/yaml_reader.py +1 -1
- fastapi_factory_utilities/example/app.py +15 -5
- fastapi_factory_utilities/example/entities/books/__init__.py +1 -1
- fastapi_factory_utilities/example/models/books/__init__.py +1 -1
- {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info}/METADATA +14 -8
- fastapi_factory_utilities-0.8.3.dist-info/RECORD +111 -0
- {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info}/WHEEL +1 -1
- fastapi_factory_utilities/core/app/plugin_manager/__init__.py +0 -15
- fastapi_factory_utilities/core/app/plugin_manager/exceptions.py +0 -33
- fastapi_factory_utilities/core/app/plugin_manager/plugin_manager.py +0 -190
- fastapi_factory_utilities/core/plugins/example/__init__.py +0 -31
- fastapi_factory_utilities/core/plugins/httpx_plugin/__init__.py +0 -31
- fastapi_factory_utilities/core/security/jwt.py +0 -158
- fastapi_factory_utilities-0.4.0.dist-info/RECORD +0 -82
- {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info}/entry_points.txt +0 -0
- {fastapi_factory_utilities-0.4.0.dist-info → fastapi_factory_utilities-0.8.3.dist-info/licenses}/LICENSE +0 -0
|
@@ -10,10 +10,10 @@ from pydantic import BaseModel
|
|
|
10
10
|
|
|
11
11
|
from fastapi_factory_utilities.core.services.status.enums import HealthStatusEnum
|
|
12
12
|
from fastapi_factory_utilities.core.services.status.services import (
|
|
13
|
-
ComponentInstanceKey,
|
|
14
13
|
StatusService,
|
|
15
14
|
depends_status_service,
|
|
16
15
|
)
|
|
16
|
+
from fastapi_factory_utilities.core.services.status.types import ComponentInstanceKey
|
|
17
17
|
|
|
18
18
|
api_v1_sys_health = APIRouter(prefix="/health")
|
|
19
19
|
|
|
@@ -12,12 +12,12 @@ from .config import (
|
|
|
12
12
|
from .enums import EnvironmentEnum
|
|
13
13
|
|
|
14
14
|
__all__: list[str] = [
|
|
15
|
-
"BaseApplicationConfig",
|
|
16
|
-
"EnvironmentEnum",
|
|
17
15
|
"ApplicationAbstract",
|
|
18
16
|
"ApplicationGenericBuilder",
|
|
19
|
-
"
|
|
20
|
-
"HttpServiceDependencyConfig",
|
|
17
|
+
"BaseApplicationConfig",
|
|
21
18
|
"DependencyConfig",
|
|
19
|
+
"EnvironmentEnum",
|
|
20
|
+
"HttpServiceDependencyConfig",
|
|
21
|
+
"RootConfig",
|
|
22
22
|
"depends_dependency_config",
|
|
23
23
|
]
|
|
@@ -12,10 +12,7 @@ from structlog.stdlib import BoundLogger, get_logger
|
|
|
12
12
|
from fastapi_factory_utilities.core.api import api
|
|
13
13
|
from fastapi_factory_utilities.core.app.config import RootConfig
|
|
14
14
|
from fastapi_factory_utilities.core.app.fastapi_builder import FastAPIBuilder
|
|
15
|
-
from fastapi_factory_utilities.core.
|
|
16
|
-
PluginManager,
|
|
17
|
-
)
|
|
18
|
-
from fastapi_factory_utilities.core.plugins import PluginsEnum
|
|
15
|
+
from fastapi_factory_utilities.core.plugins import PluginAbstract
|
|
19
16
|
from fastapi_factory_utilities.core.services.status.services import StatusService
|
|
20
17
|
|
|
21
18
|
_logger: BoundLogger = get_logger(__name__)
|
|
@@ -31,12 +28,10 @@ class ApplicationAbstract(ABC):
|
|
|
31
28
|
# TODO: Find a way to remove this from here
|
|
32
29
|
ODM_DOCUMENT_MODELS: ClassVar[list[type[Document]]]
|
|
33
30
|
|
|
34
|
-
DEFAULT_PLUGINS_ACTIVATED: ClassVar[list[PluginsEnum]] = []
|
|
35
|
-
|
|
36
31
|
def __init__(
|
|
37
32
|
self,
|
|
38
33
|
root_config: RootConfig,
|
|
39
|
-
|
|
34
|
+
plugins: list[PluginAbstract],
|
|
40
35
|
fastapi_builder: FastAPIBuilder,
|
|
41
36
|
) -> None:
|
|
42
37
|
"""Instantiate the application."""
|
|
@@ -44,9 +39,25 @@ class ApplicationAbstract(ABC):
|
|
|
44
39
|
self.fastapi_builder: FastAPIBuilder = fastapi_builder
|
|
45
40
|
# Add the API router to the FastAPI application
|
|
46
41
|
self.fastapi_builder.add_api_router(router=api, without_resource_path=True)
|
|
47
|
-
self.
|
|
42
|
+
self.plugins: list[PluginAbstract] = plugins
|
|
48
43
|
self._add_to_state: dict[str, Any] = {}
|
|
49
44
|
|
|
45
|
+
def load_plugins(self) -> None:
|
|
46
|
+
"""Load the plugins."""
|
|
47
|
+
for plugin in self.plugins:
|
|
48
|
+
plugin.set_application(application=self)
|
|
49
|
+
plugin.on_load()
|
|
50
|
+
|
|
51
|
+
async def startup_plugins(self) -> None:
|
|
52
|
+
"""Startup the plugins."""
|
|
53
|
+
for plugin in self.plugins:
|
|
54
|
+
await plugin.on_startup()
|
|
55
|
+
|
|
56
|
+
async def shutdown_plugins(self) -> None:
|
|
57
|
+
"""Shutdown the plugins."""
|
|
58
|
+
for plugin in self.plugins:
|
|
59
|
+
await plugin.on_shutdown()
|
|
60
|
+
|
|
50
61
|
def setup(self) -> None:
|
|
51
62
|
"""Initialize the application."""
|
|
52
63
|
# Initialize FastAPI application
|
|
@@ -61,10 +72,7 @@ class ApplicationAbstract(ABC):
|
|
|
61
72
|
self.configure()
|
|
62
73
|
self._apply_states_to_fastapi_app()
|
|
63
74
|
# Initialize PluginManager
|
|
64
|
-
self.
|
|
65
|
-
self.plugin_manager.load()
|
|
66
|
-
# Add the states to the FastAPI app
|
|
67
|
-
self._import_states_from_plugin_manager()
|
|
75
|
+
self.load_plugins()
|
|
68
76
|
|
|
69
77
|
def _apply_states_to_fastapi_app(self) -> None:
|
|
70
78
|
# Add manually added states to the FastAPI app
|
|
@@ -74,13 +82,6 @@ class ApplicationAbstract(ABC):
|
|
|
74
82
|
setattr(self._asgi_app.state, key, value)
|
|
75
83
|
self._add_to_state.clear()
|
|
76
84
|
|
|
77
|
-
def _import_states_from_plugin_manager(self) -> None:
|
|
78
|
-
"""Import the states from the plugins."""
|
|
79
|
-
for state in self.plugin_manager.states:
|
|
80
|
-
self.add_to_state(key=state.key, value=state.value)
|
|
81
|
-
self.plugin_manager.clear_states()
|
|
82
|
-
self._apply_states_to_fastapi_app()
|
|
83
|
-
|
|
84
85
|
def add_to_state(self, key: str, value: Any) -> None:
|
|
85
86
|
"""Add a value to the FastAPI app state."""
|
|
86
87
|
if key in self._add_to_state:
|
|
@@ -105,14 +106,13 @@ class ApplicationAbstract(ABC):
|
|
|
105
106
|
@asynccontextmanager
|
|
106
107
|
async def fastapi_lifespan(self, fastapi: FastAPI) -> AsyncGenerator[None, None]: # pylint: disable=unused-argument
|
|
107
108
|
"""FastAPI lifespan context manager."""
|
|
108
|
-
await self.
|
|
109
|
-
self._import_states_from_plugin_manager()
|
|
109
|
+
await self.startup_plugins()
|
|
110
110
|
await self.on_startup()
|
|
111
111
|
try:
|
|
112
112
|
yield
|
|
113
113
|
finally:
|
|
114
114
|
await self.on_shutdown()
|
|
115
|
-
await self.
|
|
115
|
+
await self.shutdown_plugins()
|
|
116
116
|
|
|
117
117
|
def get_config(self) -> RootConfig:
|
|
118
118
|
"""Get the configuration."""
|
|
@@ -122,10 +122,6 @@ class ApplicationAbstract(ABC):
|
|
|
122
122
|
"""Get the ASGI application."""
|
|
123
123
|
return self._asgi_app
|
|
124
124
|
|
|
125
|
-
def get_plugin_manager(self) -> PluginManager:
|
|
126
|
-
"""Get the plugin manager."""
|
|
127
|
-
return self.plugin_manager
|
|
128
|
-
|
|
129
125
|
def get_status_service(self) -> StatusService:
|
|
130
126
|
"""Get the status service."""
|
|
131
127
|
return self.status_service
|
|
@@ -4,10 +4,7 @@ from typing import Any, Generic, Self, TypeVar, get_args
|
|
|
4
4
|
|
|
5
5
|
from fastapi_factory_utilities.core.app.config import GenericConfigBuilder, RootConfig
|
|
6
6
|
from fastapi_factory_utilities.core.app.fastapi_builder import FastAPIBuilder
|
|
7
|
-
from fastapi_factory_utilities.core.
|
|
8
|
-
PluginManager,
|
|
9
|
-
)
|
|
10
|
-
from fastapi_factory_utilities.core.plugins import PluginsEnum
|
|
7
|
+
from fastapi_factory_utilities.core.plugins import PluginAbstract
|
|
11
8
|
from fastapi_factory_utilities.core.utils.log import LogModeEnum, setup_log
|
|
12
9
|
from fastapi_factory_utilities.core.utils.uvicorn import UvicornUtils
|
|
13
10
|
|
|
@@ -19,21 +16,16 @@ T = TypeVar("T", bound=ApplicationAbstract)
|
|
|
19
16
|
class ApplicationGenericBuilder(Generic[T]):
|
|
20
17
|
"""Application generic builder."""
|
|
21
18
|
|
|
22
|
-
def __init__(self,
|
|
19
|
+
def __init__(self, plugins: list[PluginAbstract] | None = None) -> None:
|
|
23
20
|
"""Instanciate the ApplicationGenericBuilder."""
|
|
24
21
|
self._uvicorn_utils: UvicornUtils | None = None
|
|
25
22
|
self._root_config: RootConfig | None = None
|
|
26
|
-
self.
|
|
23
|
+
self._plugins: list[PluginAbstract] = plugins or []
|
|
27
24
|
self._fastapi_builder: FastAPIBuilder | None = None
|
|
28
25
|
generic_args: tuple[Any, ...] = get_args(self.__orig_bases__[0]) # type: ignore
|
|
29
26
|
self._application_class: type[T] = generic_args[0]
|
|
30
|
-
self._plugins_activation_list: list[PluginsEnum]
|
|
31
|
-
if plugins_activation_list is None:
|
|
32
|
-
self._plugins_activation_list = self._application_class.DEFAULT_PLUGINS_ACTIVATED
|
|
33
|
-
else:
|
|
34
|
-
self._plugins_activation_list = plugins_activation_list
|
|
35
27
|
|
|
36
|
-
def add_plugin_to_activate(self, plugin:
|
|
28
|
+
def add_plugin_to_activate(self, plugin: PluginAbstract) -> Self:
|
|
37
29
|
"""Add a plugin to activate.
|
|
38
30
|
|
|
39
31
|
Args:
|
|
@@ -42,7 +34,7 @@ class ApplicationGenericBuilder(Generic[T]):
|
|
|
42
34
|
Returns:
|
|
43
35
|
Self: The builder.
|
|
44
36
|
"""
|
|
45
|
-
self.
|
|
37
|
+
self._plugins.append(plugin)
|
|
46
38
|
return self
|
|
47
39
|
|
|
48
40
|
def add_config(self, config: RootConfig) -> Self:
|
|
@@ -57,18 +49,6 @@ class ApplicationGenericBuilder(Generic[T]):
|
|
|
57
49
|
self._root_config = config
|
|
58
50
|
return self
|
|
59
51
|
|
|
60
|
-
def add_plugin_manager(self, plugin_manager: PluginManager) -> Self:
|
|
61
|
-
"""Add the plugin manager to the builder.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
plugin_manager (PluginManager): The plugin manager.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
Self: The builder.
|
|
68
|
-
"""
|
|
69
|
-
self._plugin_manager = plugin_manager
|
|
70
|
-
return self
|
|
71
|
-
|
|
72
52
|
def add_fastapi_builder(self, fastapi_builder: FastAPIBuilder) -> Self:
|
|
73
53
|
"""Add the FastAPI builder to the builder.
|
|
74
54
|
|
|
@@ -83,7 +63,7 @@ class ApplicationGenericBuilder(Generic[T]):
|
|
|
83
63
|
|
|
84
64
|
def _build_from_package_root_config(self) -> RootConfig:
|
|
85
65
|
"""Build the configuration from the package."""
|
|
86
|
-
return GenericConfigBuilder[self._application_class.CONFIG_CLASS](
|
|
66
|
+
return GenericConfigBuilder[self._application_class.CONFIG_CLASS]( # type: ignore
|
|
87
67
|
package_name=self._application_class.PACKAGE_NAME,
|
|
88
68
|
config_class=self._application_class.CONFIG_CLASS,
|
|
89
69
|
).build()
|
|
@@ -97,11 +77,6 @@ class ApplicationGenericBuilder(Generic[T]):
|
|
|
97
77
|
Returns:
|
|
98
78
|
T: The application.
|
|
99
79
|
"""
|
|
100
|
-
# Plugin manager
|
|
101
|
-
if self._plugin_manager is None:
|
|
102
|
-
self._plugin_manager = PluginManager()
|
|
103
|
-
for plugin in self._plugins_activation_list:
|
|
104
|
-
self._plugin_manager.activate(plugin=plugin)
|
|
105
80
|
# RootConfig
|
|
106
81
|
self._root_config = self._root_config or self._build_from_package_root_config()
|
|
107
82
|
# FastAPIBuilder
|
|
@@ -109,7 +84,7 @@ class ApplicationGenericBuilder(Generic[T]):
|
|
|
109
84
|
|
|
110
85
|
application: T = self._application_class(
|
|
111
86
|
root_config=self._root_config,
|
|
112
|
-
|
|
87
|
+
plugins=self._plugins,
|
|
113
88
|
fastapi_builder=self._fastapi_builder,
|
|
114
89
|
**kwargs,
|
|
115
90
|
)
|
|
@@ -125,6 +100,7 @@ class ApplicationGenericBuilder(Generic[T]):
|
|
|
125
100
|
"""Build the application and serve it with Uvicorn."""
|
|
126
101
|
uvicorn_utils: UvicornUtils = self._uvicorn_utils or self.build_as_uvicorn_utils()
|
|
127
102
|
|
|
103
|
+
assert self._root_config is not None
|
|
128
104
|
setup_log(mode=LogModeEnum.CONSOLE, logging_config=self._root_config.logging)
|
|
129
105
|
|
|
130
106
|
try:
|
|
@@ -64,7 +64,7 @@ class FastAPIBuilder:
|
|
|
64
64
|
title=self._root_config.application.service_name,
|
|
65
65
|
description="",
|
|
66
66
|
version=self._root_config.application.version,
|
|
67
|
-
lifespan=lifespan,
|
|
67
|
+
lifespan=lifespan,
|
|
68
68
|
)
|
|
69
69
|
|
|
70
70
|
fastapi.add_middleware(
|
|
@@ -77,7 +77,8 @@ class FastAPIBuilder:
|
|
|
77
77
|
|
|
78
78
|
for middleware_args in self._middleware_list:
|
|
79
79
|
fastapi.add_middleware(
|
|
80
|
-
middleware_class=middleware_args.middleware_class,
|
|
80
|
+
middleware_class=middleware_args.middleware_class, # type: ignore
|
|
81
|
+
**middleware_args.kwargs, # pyright: ignore
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
fastapi.include_router(router=self._base_router)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""FastAPI Factory Utilities exceptions."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from
|
|
5
|
-
from typing import Any
|
|
4
|
+
from typing import Any, cast
|
|
6
5
|
|
|
7
6
|
from opentelemetry.trace import Span, get_current_span
|
|
8
7
|
from opentelemetry.util.types import AttributeValue
|
|
@@ -14,39 +13,75 @@ _logger: BoundLogger = get_logger()
|
|
|
14
13
|
class FastAPIFactoryUtilitiesError(Exception):
|
|
15
14
|
"""Base exception for the FastAPI Factory Utilities."""
|
|
16
15
|
|
|
16
|
+
FILTERED_ATTRIBUTES: tuple[str, ...] = ()
|
|
17
|
+
DEFAULT_LOGGING_LEVEL: int = logging.ERROR
|
|
18
|
+
DEFAULT_MESSAGE: str | None = None
|
|
19
|
+
|
|
17
20
|
def __init__(
|
|
18
21
|
self,
|
|
19
|
-
*args:
|
|
20
|
-
|
|
21
|
-
level: int = logging.ERROR,
|
|
22
|
-
**kwargs: dict[str, Any],
|
|
22
|
+
*args: object,
|
|
23
|
+
**kwargs: Any,
|
|
23
24
|
) -> None:
|
|
24
|
-
"""
|
|
25
|
+
"""Instantiate the exception.
|
|
25
26
|
|
|
26
27
|
Args:
|
|
27
|
-
*args
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
**kwargs (dict[str, Any]): The keyword arguments
|
|
28
|
+
*args: The arguments.
|
|
29
|
+
**kwargs: The keyword arguments.
|
|
30
|
+
|
|
31
31
|
"""
|
|
32
|
+
# If Default Message is not set, try to extract it from docstring (first line)
|
|
33
|
+
default_message: str = self.DEFAULT_MESSAGE or "An error occurred"
|
|
34
|
+
if self.DEFAULT_MESSAGE is None and self.__doc__ is not None:
|
|
35
|
+
default_message = self.__doc__.split("\n", maxsplit=1)[0]
|
|
36
|
+
# Extract the message and the level from the kwargs if they are present
|
|
37
|
+
self.message: str | None = cast(str | None, kwargs.pop("message", None))
|
|
38
|
+
self.level: int = cast(int, kwargs.pop("level", self.DEFAULT_LOGGING_LEVEL))
|
|
39
|
+
|
|
40
|
+
# If the message is not present, try to extract it from the args
|
|
41
|
+
if self.message is None and len(args) > 0 and isinstance(args[0], str):
|
|
42
|
+
self.message = args[0]
|
|
43
|
+
elif self.message is None:
|
|
44
|
+
self.message = default_message
|
|
45
|
+
|
|
32
46
|
# Log the Exception
|
|
33
|
-
if message:
|
|
34
|
-
_logger.log(level=level, event=message)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
if self.message:
|
|
48
|
+
_logger.log(level=self.level, event=self.message)
|
|
49
|
+
|
|
50
|
+
# Set the kwargs as attributes of the exception
|
|
51
|
+
for key, value in kwargs.items():
|
|
52
|
+
if key in self.FILTERED_ATTRIBUTES:
|
|
53
|
+
continue
|
|
54
|
+
setattr(self, key, value)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Propagate the exception
|
|
58
|
+
span: Span = get_current_span()
|
|
59
|
+
# If not otel is setup, INVALID_SPAN is retrieved from get_current_span
|
|
60
|
+
# and it will respond False to the is_recording method
|
|
61
|
+
if span.is_recording():
|
|
62
|
+
span.record_exception(self)
|
|
63
|
+
for key, value in kwargs.items():
|
|
64
|
+
if key in self.FILTERED_ATTRIBUTES:
|
|
65
|
+
continue
|
|
66
|
+
attribute_value: AttributeValue
|
|
67
|
+
if not isinstance(value, (str, bool, int, float)):
|
|
68
|
+
attribute_value = str(value)
|
|
69
|
+
else:
|
|
70
|
+
attribute_value = value
|
|
71
|
+
span.set_attribute(key, attribute_value)
|
|
72
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
73
|
+
# Suppress any errors that occur while propagating the exception
|
|
74
|
+
pass
|
|
75
|
+
|
|
51
76
|
# Call the parent class
|
|
52
77
|
super().__init__(*args)
|
|
78
|
+
|
|
79
|
+
def __str__(self) -> str:
|
|
80
|
+
"""Return the string representation of the exception.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
str: The message if available, otherwise the default exception string.
|
|
84
|
+
"""
|
|
85
|
+
if self.message is not None:
|
|
86
|
+
return self.message
|
|
87
|
+
return super().__str__()
|
|
@@ -1,36 +1,7 @@
|
|
|
1
1
|
"""Package for plugins."""
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class PluginsEnum(StrEnum):
|
|
8
|
-
"""Enumeration for the plugins."""
|
|
9
|
-
|
|
10
|
-
OPENTELEMETRY_PLUGIN = auto()
|
|
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
|
|
32
|
-
|
|
3
|
+
from .abstracts import PluginAbstract
|
|
33
4
|
|
|
34
5
|
__all__: list[str] = [
|
|
35
|
-
"
|
|
6
|
+
"PluginAbstract",
|
|
36
7
|
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Abstracts for the plugins."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from fastapi_factory_utilities.core.protocols import ApplicationAbstractProtocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PluginAbstract(ABC):
|
|
11
|
+
"""Abstract class for the plugins."""
|
|
12
|
+
|
|
13
|
+
def __init__(self) -> None:
|
|
14
|
+
"""Initialize the plugin."""
|
|
15
|
+
self._application: ApplicationAbstractProtocol | None = None
|
|
16
|
+
|
|
17
|
+
def set_application(self, application: "ApplicationAbstractProtocol") -> Self:
|
|
18
|
+
"""Set the application."""
|
|
19
|
+
self._application = application
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def _add_to_state(self, key: str, value: Any) -> None:
|
|
23
|
+
"""Add to the state."""
|
|
24
|
+
assert self._application is not None
|
|
25
|
+
setattr(self._application.get_asgi_app().state, key, value)
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def on_load(self) -> None:
|
|
29
|
+
"""On load."""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def on_startup(self) -> None:
|
|
34
|
+
"""On startup."""
|
|
35
|
+
raise NotImplementedError
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def on_shutdown(self) -> None:
|
|
39
|
+
"""On shutdown."""
|
|
40
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Aiopika Plugin Module."""
|
|
2
|
+
|
|
3
|
+
from .configs import AiopikaConfig
|
|
4
|
+
from .depends import depends_aiopika_robust_connection
|
|
5
|
+
from .exceptions import AiopikaPluginBaseError, AiopikaPluginConfigError
|
|
6
|
+
from .exchange import Exchange
|
|
7
|
+
from .listener import AbstractListener
|
|
8
|
+
from .message import AbstractMessage, SenderModel
|
|
9
|
+
from .plugins import AiopikaPlugin
|
|
10
|
+
from .publisher import AbstractPublisher
|
|
11
|
+
from .queue import Queue
|
|
12
|
+
|
|
13
|
+
__all__: list[str] = [
|
|
14
|
+
"AbstractListener",
|
|
15
|
+
"AbstractMessage",
|
|
16
|
+
"AbstractPublisher",
|
|
17
|
+
"AiopikaConfig",
|
|
18
|
+
"AiopikaPlugin",
|
|
19
|
+
"AiopikaPluginBaseError",
|
|
20
|
+
"AiopikaPluginConfigError",
|
|
21
|
+
"Exchange",
|
|
22
|
+
"Queue",
|
|
23
|
+
"SenderModel",
|
|
24
|
+
"depends_aiopika_robust_connection",
|
|
25
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Provides the abstract class for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from aio_pika.abc import AbstractChannel, AbstractRobustConnection
|
|
7
|
+
|
|
8
|
+
from .exceptions import AiopikaPluginBaseError, AiopikaPluginConnectionNotProvidedError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractAiopikaResource(ABC):
|
|
12
|
+
"""Abstract class for the Aiopika resource."""
|
|
13
|
+
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
"""Initialize the Aiopika resource."""
|
|
16
|
+
self._robust_connection: AbstractRobustConnection | None = None
|
|
17
|
+
self._channel: AbstractChannel | None = None
|
|
18
|
+
|
|
19
|
+
def set_robust_connection(self, robust_connection: AbstractRobustConnection) -> Self:
|
|
20
|
+
"""Set the robust connection."""
|
|
21
|
+
self._robust_connection = robust_connection
|
|
22
|
+
return self
|
|
23
|
+
|
|
24
|
+
def set_channel(self, channel: AbstractChannel) -> Self:
|
|
25
|
+
"""Set the channel."""
|
|
26
|
+
self._channel = channel
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
async def _acquire_channel(self) -> AbstractChannel:
|
|
30
|
+
"""Acquire the channel."""
|
|
31
|
+
if self._robust_connection is None:
|
|
32
|
+
raise AiopikaPluginConnectionNotProvidedError(
|
|
33
|
+
message="Robust connection not provided.",
|
|
34
|
+
)
|
|
35
|
+
if self._channel is None:
|
|
36
|
+
try:
|
|
37
|
+
self._channel = await self._robust_connection.channel()
|
|
38
|
+
except Exception as exception:
|
|
39
|
+
raise AiopikaPluginBaseError(
|
|
40
|
+
message="Failed to acquire the channel.",
|
|
41
|
+
) from exception
|
|
42
|
+
return self._channel
|
|
43
|
+
|
|
44
|
+
async def setup(self) -> Self:
|
|
45
|
+
"""Setup the Aiopika resource."""
|
|
46
|
+
if self._channel is None:
|
|
47
|
+
await self._acquire_channel()
|
|
48
|
+
return self
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Provides the configuration for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, ClassVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, UrlConstraints
|
|
6
|
+
from pydantic_core import Url, ValidationError
|
|
7
|
+
|
|
8
|
+
from fastapi_factory_utilities.core.utils.importlib import get_path_file_in_package
|
|
9
|
+
from fastapi_factory_utilities.core.utils.yaml_reader import (
|
|
10
|
+
UnableToReadYamlFileError,
|
|
11
|
+
YamlFileReader,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .exceptions import AiopikaPluginConfigError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AiopikaConfig(BaseModel):
|
|
18
|
+
"""Provides the configuration model for the Aiopika plugin.
|
|
19
|
+
|
|
20
|
+
https://docs.aio-pika.com/#aio-pika-connect-robust-function-and-aio-pika-robustconnection-class-specific
|
|
21
|
+
|
|
22
|
+
Possible query parameters for the AMQP URL:
|
|
23
|
+
name (str url encoded) - A string that will be visible in the RabbitMQ management console
|
|
24
|
+
and in the server logs, convenient for diagnostics.
|
|
25
|
+
cafile (str) - Path to Certificate Authority file
|
|
26
|
+
capath (str) - Path to Certificate Authority directory
|
|
27
|
+
cadata (str url encoded) - URL encoded CA certificate content
|
|
28
|
+
keyfile (str) - Path to client ssl private key file
|
|
29
|
+
certfile (str) - Path to client ssl certificate file
|
|
30
|
+
no_verify_ssl - No verify server SSL certificates. 0 by default and means False other value means True.
|
|
31
|
+
heartbeat (int-like) - interval in seconds between AMQP heartbeat packets. 0 disables this feature.
|
|
32
|
+
reconnect_interval (float-like) - is the period in seconds, not more often than the attempts
|
|
33
|
+
to re-establish the connection will take place.
|
|
34
|
+
fail_fast (true/yes/y/enable/on/enabled/1 means True, otherwise False) - special behavior
|
|
35
|
+
for the start connection attempt, if it fails, all other attempts stops
|
|
36
|
+
and an exception will be thrown at the connection stage. Enabled by default, if you are sure you need
|
|
37
|
+
to disable this feature, be ensures for the passed URL is really working.
|
|
38
|
+
Otherwise, your program will go into endless reconnection attempts that can not be successed.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
model_config: ClassVar[ConfigDict] = ConfigDict(frozen=True, extra="forbid")
|
|
43
|
+
|
|
44
|
+
amqp_url: Annotated[Url, UrlConstraints(allowed_schemes=["amqp", "amqps"])] = Field(description="The AMQP URL.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_config_from_package(package_name: str) -> AiopikaConfig:
|
|
48
|
+
"""Build the configuration from the package.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
package_name (str): The package name.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
AiopikaConfig: The Aiopika configuration.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
AiopikaPluginConfigError: If the configuration cannot be read or created or the configuration is invalid.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
yaml_file_content: dict[str, Any] = YamlFileReader(
|
|
61
|
+
file_path=get_path_file_in_package(
|
|
62
|
+
filename="application.yaml",
|
|
63
|
+
package=package_name,
|
|
64
|
+
),
|
|
65
|
+
yaml_base_key="aiopika",
|
|
66
|
+
use_environment_injection=True,
|
|
67
|
+
).read()
|
|
68
|
+
except (FileNotFoundError, ImportError, UnableToReadYamlFileError) as exception:
|
|
69
|
+
raise AiopikaPluginConfigError(
|
|
70
|
+
message="Unable to read the application configuration file for the Aiopika plugin in the package.",
|
|
71
|
+
package_name=package_name,
|
|
72
|
+
) from exception
|
|
73
|
+
|
|
74
|
+
# Create the application configuration model
|
|
75
|
+
config: AiopikaConfig
|
|
76
|
+
try:
|
|
77
|
+
config = AiopikaConfig(**yaml_file_content)
|
|
78
|
+
except ValidationError as exception:
|
|
79
|
+
raise AiopikaPluginConfigError(
|
|
80
|
+
message="Unable to create the application configuration model for the Aiopika plugin in the package.",
|
|
81
|
+
package_name=package_name,
|
|
82
|
+
validation_errors=exception.errors(),
|
|
83
|
+
) from exception
|
|
84
|
+
|
|
85
|
+
return config
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Provides the dependencies for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import cast
|
|
4
|
+
|
|
5
|
+
from aio_pika.abc import AbstractRobustConnection
|
|
6
|
+
from fastapi import Request
|
|
7
|
+
|
|
8
|
+
from .exceptions import AiopikaPluginBaseError
|
|
9
|
+
|
|
10
|
+
DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY: str = "aiopika_robust_connection"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def depends_aiopika_robust_connection(request: Request) -> AbstractRobustConnection:
|
|
14
|
+
"""Get the Aiopika robust connection."""
|
|
15
|
+
robust_connection: AbstractRobustConnection | None = cast(
|
|
16
|
+
AbstractRobustConnection | None, getattr(request.app.state, DEPENDS_AIOPIKA_ROBUST_CONNECTION_KEY, None)
|
|
17
|
+
)
|
|
18
|
+
if robust_connection is None:
|
|
19
|
+
raise AiopikaPluginBaseError("Aiopika robust connection not found in the application state.")
|
|
20
|
+
return robust_connection
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Provides the exceptions for the Aiopika plugin."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from fastapi_factory_utilities.core.exceptions import FastAPIFactoryUtilitiesError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AiopikaPluginBaseError(FastAPIFactoryUtilitiesError):
|
|
9
|
+
"""Base class for all exceptions raised by the Aiopika plugin."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str, **kwargs: Any) -> None:
|
|
12
|
+
"""Initialize the Aiopika plugin base exception."""
|
|
13
|
+
super().__init__(message, **kwargs)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AiopikaPluginConfigError(AiopikaPluginBaseError):
|
|
17
|
+
"""Exception for the Aiopika plugin configuration."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AiopikaPluginConnectionNotProvidedError(AiopikaPluginBaseError):
|
|
21
|
+
"""Exception for the Aiopika plugin connection not provided."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AiopikaPluginExchangeNotDeclaredError(AiopikaPluginBaseError):
|
|
25
|
+
"""Exception for the Aiopika plugin exchange not declared."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AiopikaPluginQueueNotDeclaredError(AiopikaPluginBaseError):
|
|
29
|
+
"""Exception for the Aiopika plugin queue not declared."""
|