From 84406e46c8d946d86049d21e68cfe5eb20dd99d5 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 18 Oct 2021 18:07:18 +0200 Subject: [PATCH 1/4] Add register_middleware hook --- fps/app.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ fps/hooks.py | 15 +++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/fps/app.py b/fps/app.py index 8ad2a0b..d7d4708 100644 --- a/fps/app.py +++ b/fps/app.py @@ -271,6 +271,49 @@ def _load_routers(app: FastAPI) -> None: logger.info("No plugin API router to load") +def _load_middlewares(app: FastAPI) -> None: + + pm = _get_pluggin_manager(HookType.MIDDLEWARE) + + middleware_impls = pm.hook.middleware.get_hookimpls() + if middleware_impls: + grouped_middlewares = _grouped_hookimpls_results(pm.hook.middleware) + pkg_names = {get_pkg_name(p, strip_fps=False) for p in grouped_middlewares} + logger.info(f"Loading middlewares from plugin package(s) {pkg_names}") + + for p, middlewares in grouped_middlewares.items(): + p_name = Config.plugin_name(p) + plugin_config = Config.from_name(p_name) + + disabled = ( + plugin_config + and not plugin_config.enabled + or p_name in Config(FPSConfig).disabled_plugins + or ( + Config(FPSConfig).enabled_plugins + and p_name not in Config(FPSConfig).enabled_plugins + ) + ) + if not middlewares or disabled: + disabled_msg = " (disabled)" if disabled else "" + logger.info( + f"No middleware registered for plugin '{p_name}'{disabled_msg}" + ) + continue + + for plugin_middleware, plugin_kwargs in middlewares: + app.add_middleware( + plugin_middleware, + **plugin_kwargs, + ) + + logger.info( + f"{len(middlewares)} middleware(s) added from plugin '{p_name}'" + ) + else: + logger.info("No plugin middleware to load") + + def create_app(): logging.getLogger("fps") @@ -283,6 +326,7 @@ def create_app(): _load_routers(app) _load_exceptions_handlers(app) + _load_middlewares(app) Config.check_not_used_sections() diff --git a/fps/hooks.py b/fps/hooks.py index 835f64b..f57dafe 100644 --- a/fps/hooks.py +++ b/fps/hooks.py @@ -12,6 +12,7 @@ class HookType(Enum): ROUTER = "fps_router" CONFIG = "fps_config" EXCEPTION = "fps_exception" + MIDDLEWARE = "fps_middleware" @pluggy.HookspecMarker(HookType.ROUTER.value) @@ -75,3 +76,17 @@ def plugin_name_callback() -> str: return pluggy.HookimplMarker(HookType.CONFIG.value)( function=plugin_name_callback, specname="plugin_name" ) + + +@pluggy.HookspecMarker(HookType.MIDDLEWARE.value) +def middleware() -> Tuple[type, Dict[str, Any]]: + pass + + +def register_middleware(m: type, **kwargs: Dict[str, Any]): + def middleware_callback() -> Tuple[type, Dict[str, Any]]: + return m, kwargs + + return pluggy.HookimplMarker(HookType.MIDDLEWARE.value)( + function=middleware_callback, specname="middleware" + ) From 63fcc2024c50f7d6bd25f0bef156fc7650a02d4f Mon Sep 17 00:00:00 2001 From: David Brochart Date: Tue, 19 Oct 2021 10:07:04 +0200 Subject: [PATCH 2/4] Handle middleware conflicts --- fps/app.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/fps/app.py b/fps/app.py index d7d4708..0edfb40 100644 --- a/fps/app.py +++ b/fps/app.py @@ -275,9 +275,10 @@ def _load_middlewares(app: FastAPI) -> None: pm = _get_pluggin_manager(HookType.MIDDLEWARE) - middleware_impls = pm.hook.middleware.get_hookimpls() - if middleware_impls: - grouped_middlewares = _grouped_hookimpls_results(pm.hook.middleware) + grouped_middlewares = _grouped_hookimpls_results(pm.hook.middleware) + + if grouped_middlewares: + pkg_names = {get_pkg_name(p, strip_fps=False) for p in grouped_middlewares} logger.info(f"Loading middlewares from plugin package(s) {pkg_names}") @@ -302,6 +303,13 @@ def _load_middlewares(app: FastAPI) -> None: continue for plugin_middleware, plugin_kwargs in middlewares: + if plugin_middleware in [ + middleware.cls for middleware in app.user_middleware + ]: + logger.error( + f"Redefinition of middleware '{plugin_middleware}' is not allowed." + ) + exit(1) app.add_middleware( plugin_middleware, **plugin_kwargs, From 4774b504b9a1b880b7a201b6339155634f8def98 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Wed, 20 Oct 2021 09:55:23 +0200 Subject: [PATCH 3/4] Add --fps.middlewares option, remove middleware conflict handling --- fps/app.py | 8 +------- fps/config.py | 5 ++++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/fps/app.py b/fps/app.py index 0edfb40..8f0f707 100644 --- a/fps/app.py +++ b/fps/app.py @@ -294,6 +294,7 @@ def _load_middlewares(app: FastAPI) -> None: Config(FPSConfig).enabled_plugins and p_name not in Config(FPSConfig).enabled_plugins ) + or p_name not in Config(FPSConfig).middlewares ) if not middlewares or disabled: disabled_msg = " (disabled)" if disabled else "" @@ -303,13 +304,6 @@ def _load_middlewares(app: FastAPI) -> None: continue for plugin_middleware, plugin_kwargs in middlewares: - if plugin_middleware in [ - middleware.cls for middleware in app.user_middleware - ]: - logger.error( - f"Redefinition of middleware '{plugin_middleware}' is not allowed." - ) - exit(1) app.add_middleware( plugin_middleware, **plugin_kwargs, diff --git a/fps/config.py b/fps/config.py index 6b54485..e9745b7 100644 --- a/fps/config.py +++ b/fps/config.py @@ -35,7 +35,10 @@ class FPSConfig(BaseModel): enabled_plugins: List[str] = [] disabled_plugins: List[str] = [] - @validator("enabled_plugins", "disabled_plugins") + # plugin middlewares + middlewares: List[str] = [] + + @validator("enabled_plugins", "disabled_plugins", "middlewares") def plugins_format(cls, plugins): warnings = [p for p in plugins if p.startswith("[") or p.endswith("]")] if warnings: From abb18d881fee42cfdcf25323525915fd9978c4b1 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Thu, 21 Oct 2021 09:59:46 +0200 Subject: [PATCH 4/4] Pass middleware kwargs through configuration --- fps/app.py | 35 ++++++++++++++++++++++++------ fps/config.py | 11 +++++++--- fps/hooks.py | 8 +++---- plugins/uvicorn/fps_uvicorn/cli.py | 3 ++- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/fps/app.py b/fps/app.py index 8f0f707..cf203db 100644 --- a/fps/app.py +++ b/fps/app.py @@ -282,6 +282,7 @@ def _load_middlewares(app: FastAPI) -> None: pkg_names = {get_pkg_name(p, strip_fps=False) for p in grouped_middlewares} logger.info(f"Loading middlewares from plugin package(s) {pkg_names}") + middleware_dict = {} for p, middlewares in grouped_middlewares.items(): p_name = Config.plugin_name(p) plugin_config = Config.from_name(p_name) @@ -294,7 +295,6 @@ def _load_middlewares(app: FastAPI) -> None: Config(FPSConfig).enabled_plugins and p_name not in Config(FPSConfig).enabled_plugins ) - or p_name not in Config(FPSConfig).middlewares ) if not middlewares or disabled: disabled_msg = " (disabled)" if disabled else "" @@ -303,15 +303,36 @@ def _load_middlewares(app: FastAPI) -> None: ) continue - for plugin_middleware, plugin_kwargs in middlewares: - app.add_middleware( - plugin_middleware, - **plugin_kwargs, + logger.info(f"Registered middleware(s) for plugin '{p_name}':") + for middleware in middlewares: + logger.info( + f"Middleware: {middleware.__module__}.{middleware.__qualname__}" ) - logger.info( - f"{len(middlewares)} middleware(s) added from plugin '{p_name}'" + middleware_dict.update( + { + f"{middleware.__module__}.{middleware.__qualname__}": middleware + for middleware in middlewares + } ) + + middleware_cnt = 0 + for middleware in Config(FPSConfig).middlewares: + middleware_class_path = middleware.class_path + if middleware_class_path not in middleware_dict: + logger.warning(f"Unknown middleware {middleware_class_path}") + continue + + logger.info(f"Adding middleware {middleware_class_path}") + middleware_class = middleware_dict[middleware_class_path] + app.add_middleware( + middleware_class, + **middleware.kwargs, + ) + middleware_cnt += 1 + + logger.info(f"{middleware_cnt} middleware(s) added") + else: logger.info("No plugin middleware to load") diff --git a/fps/config.py b/fps/config.py index e9745b7..f89cba4 100644 --- a/fps/config.py +++ b/fps/config.py @@ -2,7 +2,7 @@ import os from collections import OrderedDict from types import ModuleType -from typing import Dict, List, Tuple, Type +from typing import Any, Dict, List, Tuple, Type import toml from pydantic import BaseModel, create_model, validator @@ -25,6 +25,11 @@ def create_default_plugin_model(plugin_name: str): return create_model(f"{plugin_name}Model", __base__=PluginModel) +class MiddlewareModel(BaseModel): + class_path: str + kwargs: Dict[str, Any] = {} + + class FPSConfig(BaseModel): # fastapi title: str = "FPS" @@ -36,9 +41,9 @@ class FPSConfig(BaseModel): disabled_plugins: List[str] = [] # plugin middlewares - middlewares: List[str] = [] + middlewares: List[MiddlewareModel] = [] - @validator("enabled_plugins", "disabled_plugins", "middlewares") + @validator("enabled_plugins", "disabled_plugins") def plugins_format(cls, plugins): warnings = [p for p in plugins if p.startswith("[") or p.endswith("]")] if warnings: diff --git a/fps/hooks.py b/fps/hooks.py index f57dafe..5a22f13 100644 --- a/fps/hooks.py +++ b/fps/hooks.py @@ -79,13 +79,13 @@ def plugin_name_callback() -> str: @pluggy.HookspecMarker(HookType.MIDDLEWARE.value) -def middleware() -> Tuple[type, Dict[str, Any]]: +def middleware() -> type: pass -def register_middleware(m: type, **kwargs: Dict[str, Any]): - def middleware_callback() -> Tuple[type, Dict[str, Any]]: - return m, kwargs +def register_middleware(m: type): + def middleware_callback() -> type: + return m return pluggy.HookimplMarker(HookType.MIDDLEWARE.value)( function=middleware_callback, specname="middleware" diff --git a/plugins/uvicorn/fps_uvicorn/cli.py b/plugins/uvicorn/fps_uvicorn/cli.py index 5c7a54a..fa09f75 100644 --- a/plugins/uvicorn/fps_uvicorn/cli.py +++ b/plugins/uvicorn/fps_uvicorn/cli.py @@ -2,6 +2,7 @@ import os import threading import webbrowser +from pathlib import Path from typing import Any, Dict, List import toml @@ -86,7 +87,7 @@ def store_extra_options(options: Dict[str, Any]): f_name = "fps_cli_args.toml" with open(f_name, "w") as f: toml.dump(opts, f) - os.environ["FPS_CLI_CONFIG_FILE"] = f_name + os.environ["FPS_CLI_CONFIG_FILE"] = str(Path(f_name).resolve()) @app.command(