Skip to content
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>`_
- One Codex, Inc. `@onecodex <https://github.com/onecodex>`_
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Changelog
0.41.0 (unreleased)
*******************

Features:

- Support multiple APIs in a single application via ``Api.__init__`` param
``config_prefix``.

Other changes:

- Official Python 3.11 support (:pr:`416`).
Expand Down
13 changes: 13 additions & 0 deletions docs/openapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,19 @@ interactively. This feature is accessible through Flask app parameters.

Default: ``openapi.json``

`Note`: To support multiple APIs in the single application a ``config_prefix``
parameter needs to be passed to :class:`Api <Api>` constructor. In that case
Flask app parameters need to be prefixed with a custom prefix.

.. code-block:: python

api1 = Api(config_prefix="V1_")


class Config:
V1_OPENAPI_VERSION = "3.0.2" # Instead of OPENAPI_VERSION
V1_OPENAPI_URL_PREFIX = "/v1/" # Instead of OPENAPI_URL_PREFIX

`ReDoc`_, `Swagger UI`_ and `RapiDoc` interfaces are available to present the
API.

Expand Down
16 changes: 12 additions & 4 deletions flask_smorest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .blueprint import Blueprint # noqa
from .pagination import Page # noqa
from .error_handler import ErrorHandlerMixin
from .utils import normalize_config_prefix

__version__ = "0.40.0"

Expand All @@ -17,6 +18,11 @@ class Api(APISpecMixin, ErrorHandlerMixin):

:param Flask app: Flask application
:param spec_kwargs: kwargs to pass to internal APISpec instance
:param str config_prefix: Should be used if the user is planning to use
multiple `Api`'s in a single app. If it is not empty then
all application parameters will be prefixed with it. For example:
if ``config_prefix`` is ``V1_`` then ``V1_API_TITLE`` is going to
be used instead of ``API_TITLE``.

The ``spec_kwargs`` dictionary is passed as kwargs to the internal APISpec
instance. **flask-smorest** adds a few parameters to the original
Expand All @@ -39,9 +45,10 @@ class Api(APISpecMixin, ErrorHandlerMixin):
parameter `API_SPEC_OPTIONS`.
"""

def __init__(self, app=None, *, spec_kwargs=None):
def __init__(self, app=None, *, spec_kwargs=None, config_prefix=""):
self._app = app
self._spec_kwargs = spec_kwargs or {}
self.config_prefix = normalize_config_prefix(config_prefix)
self.spec = None
# Use lists to enforce order
self._fields = []
Expand All @@ -59,8 +66,8 @@ def init_app(self, app, *, spec_kwargs=None):

# Register flask-smorest in app extensions
app.extensions = getattr(app, "extensions", {})
ext = app.extensions.setdefault("flask-smorest", {})
ext["ext_obj"] = self
ext = app.extensions.setdefault("flask-smorest", {"apis": {}})
ext["apis"][self.config_prefix] = self

# Initialize spec
self._init_spec(**{**self._spec_kwargs, **(spec_kwargs or {})})
Expand All @@ -85,7 +92,8 @@ def register_blueprint(self, blp, *, parameters=None, **options):
Must be called after app is initialized.
"""
blp_name = options.get("name", blp.name)

if hasattr(blp, "config_prefix"):
blp.config_prefix = self.config_prefix
self._app.register_blueprint(blp, **options)

# Register views in API documentation for this resource
Expand Down
2 changes: 1 addition & 1 deletion flask_smorest/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ class Blueprint(
DOCSTRING_INFO_DELIMITER = "---"

def __init__(self, *args, **kwargs):

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

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

Expand Down
21 changes: 11 additions & 10 deletions flask_smorest/etag.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from flask import request, current_app

from .exceptions import PreconditionRequired, PreconditionFailed, NotModified
from .utils import deepupdate, resolve_schema_instance, get_appcontext
from .utils import deepupdate, resolve_schema_instance, get_appcontext, get_config_value


IF_NONE_MATCH_HEADER = {
Expand All @@ -35,11 +35,6 @@
}


def _is_etag_enabled():
"""Return True if ETag feature enabled application-wise"""
return not current_app.config.get("ETAG_DISABLED", False)


def _get_etag_ctx():
"""Get ETag section of AppContext"""
return get_appcontext().setdefault("etag", {})
Expand Down Expand Up @@ -73,7 +68,7 @@ def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):

etag_enabled = _is_etag_enabled()
etag_enabled = self._is_etag_enabled()

if etag_enabled:
# Check etag precondition
Expand Down Expand Up @@ -142,14 +137,20 @@ def check_etag(self, etag_data, etag_schema=None):
"""
if request.method not in self.METHODS_NEEDING_CHECK_ETAG:
warnings.warn(f"ETag cannot be checked on {request.method} request.")
if _is_etag_enabled():
if self._is_etag_enabled():
if etag_schema is not None:
etag_data = resolve_schema_instance(etag_schema).dump(etag_data)
new_etag = self._generate_etag(etag_data)
_get_etag_ctx()["etag_checked"] = True
if new_etag not in request.if_match:
raise PreconditionFailed

def _is_etag_enabled(self, app=None):
"""Return True if ETag feature enabled api-wise"""
return not get_config_value(
app=app or current_app, ctx=self, key="ETAG_DISABLED", default=False
)

def _verify_check_etag(self):
"""Verify check_etag was called in resource code

Expand Down Expand Up @@ -192,7 +193,7 @@ def set_etag(self, etag_data, etag_schema=None):
"""
if request.method not in self.METHODS_ALLOWING_SET_ETAG:
warnings.warn(f"ETag cannot be set on {request.method} request.")
if _is_etag_enabled():
if self._is_etag_enabled():
if etag_schema is not None:
etag_data = resolve_schema_instance(etag_schema).dump(etag_data)
new_etag = self._generate_etag(etag_data)
Expand Down Expand Up @@ -223,7 +224,7 @@ def _set_etag_in_response(self, response):
response.set_etag(new_etag)

def _prepare_etag_doc(self, doc, doc_info, *, app, spec, method, **kwargs):
if doc_info.get("etag", False) and not app.config.get("ETAG_DISABLED", False):
if doc_info.get("etag", False) and self._is_etag_enabled(app):
responses = {}
method_u = method.upper()
if method_u in self.METHODS_CHECKING_NOT_MODIFIED:
Expand Down
Loading