Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin system for Blueprints and Api #436

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ Contributors (chronological)
- Choudhury Noor `@Cnoor0171 <https://github.com/Cnoor0171>`_
- Dmitry Erlikh `@derlikh-smart <https://github.com/derlikh-smart>`_
- 0x78f1935 `@0x78f1935 <https://github.com/0x78f1935>`_
- Cory Laughlin `@Aesonus <https://github.com/Aesonus>`_
3 changes: 3 additions & 0 deletions flask_smorest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ def register_blueprint(self, blp, *, parameters=None, **options):

self._app.register_blueprint(blp, **options)

for bp_plugin in getattr(blp, "_smore_plugins", []):
bp_plugin.visit_api(self)

# Register views in API documentation for this resource
blp.register_views_in_doc(
self,
Expand Down
4 changes: 4 additions & 0 deletions flask_smorest/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ def __init__(self, *args, **kwargs):

self.description = kwargs.pop("description", "")

# This is where smore plugins are stored
self._smore_plugins = kwargs.pop("smore_plugins", [])

super().__init__(*args, **kwargs)

# _docs stores information used at init time to produce documentation.
Expand All @@ -97,6 +100,7 @@ def __init__(self, *args, **kwargs):
self._prepare_response_doc,
self._prepare_pagination_doc,
self._prepare_etag_doc,
*[plugin.register_method_docs for plugin in self._smore_plugins],
]

def add_url_rule(
Expand Down
Empty file.
27 changes: 27 additions & 0 deletions flask_smorest/plugin/abc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import abc


class Plugin(abc.ABC):
"""Abstract base class to structure smore plugins"""

@abc.abstractmethod
def register_method_docs(self, doc, doc_info, *, api, spec, **kwargs):
"""
Call when the views are registered in doc

:param dict doc: The current operation doc
:param dict doc_info: Doc info stored by decorators
:param Api api: The Api() instance
:param APISpec spec: The APISpec() instance
"""

@abc.abstractmethod
def visit_api(self, api, **kwargs):
"""
Visit the api

This should be used to register toplevel objects on the spec

:param api: The APISpec() instance
"""
pass
33 changes: 33 additions & 0 deletions flask_smorest/plugin/built_in.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from flask_smorest import Api

from ..utils import deepupdate
from . import abc


class APIKeySecurityPlugin(abc.Plugin):
def __init__(self, schema_name, parameter_name, in_="header") -> None:
self._schema_name = schema_name
self._parameter_name = parameter_name
self._in = in_

def security(self, keys):
def decorator(func):
func._apidoc = deepupdate(
getattr(func, "_apidoc", {}), {"security": [{key: [] for key in keys}]}
)
return func

return decorator

def register_method_docs(self, doc, doc_info, *, api, spec, **kwargs):
# No need to attempt to add "security" to doc if it is not in doc_info
if "security" in doc_info:
doc = deepupdate(doc, {"security": doc_info["security"]})
return doc

def visit_api(self, api: Api, **kwargs) -> None:
"""Visits the api and registers security objects"""
api.spec.components.security_scheme(
self._schema_name,
{"type": "apiKey", "in": self._in, "name": self._parameter_name},
)
39 changes: 39 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest

from flask_smorest import Api
from flask_smorest.blueprint import Blueprint
from flask_smorest.plugin.built_in import APIKeySecurityPlugin


class TestSecurityPlugin:
@pytest.fixture
def security_plugin(self):
return APIKeySecurityPlugin("testApiKey", "X-API-Key")

@pytest.mark.parametrize("openapi_version", ("2.0", "3.0.2"))
def test_spec_contains_security_requirement(
self, app, security_plugin, openapi_version
):
app.config["OPENAPI_VERSION"] = openapi_version
api = Api(app)

blp = Blueprint(
"test", __name__, url_prefix="/test", smore_plugins=[security_plugin]
)

@blp.route("/")
@security_plugin.security(["testApiKey"])
def func():
"""Dummy view func"""

api.register_blueprint(blp)

spec = api.spec.to_dict()
assert spec["paths"]["/test/"]["get"]["security"] == [{"testApiKey": []}]
if openapi_version == "3.0.2":
security_schemes = spec["components"]["securitySchemes"]
else: # Version 2.0
security_schemes = spec["securityDefinitions"]
assert security_schemes == {
"testApiKey": {"type": "apiKey", "in": "header", "name": "X-API-Key"}
}