123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962 |
- # -*- coding: utf-8 -*-
- from __future__ import unicode_literals
- import difflib
- import inspect
- from itertools import chain
- import logging
- import operator
- import re
- import six
- import sys
- import warnings
- from collections import OrderedDict
- from functools import wraps, partial
- from types import MethodType
- from flask import url_for, request, current_app
- from flask import make_response as original_flask_make_response
- try:
- from flask.helpers import _endpoint_from_view_func
- except ImportError:
- from flask.scaffold import _endpoint_from_view_func
- from flask.signals import got_request_exception
- from jsonschema import RefResolver
- from werkzeug.utils import cached_property
- from werkzeug.datastructures import Headers
- from werkzeug.exceptions import (
- HTTPException,
- MethodNotAllowed,
- NotFound,
- NotAcceptable,
- InternalServerError,
- )
- from werkzeug import __version__ as werkzeug_version
- if werkzeug_version.split('.')[0] >= '2':
- from werkzeug.wrappers import Response as BaseResponse
- else:
- from werkzeug.wrappers import BaseResponse
- from . import apidoc
- from .mask import ParseError, MaskError
- from .namespace import Namespace
- from .postman import PostmanCollectionV1
- from .resource import Resource
- from .swagger import Swagger
- from .utils import default_id, camel_to_dash, unpack
- from .representations import output_json
- from ._http import HTTPStatus
- RE_RULES = re.compile("(<.*>)")
- # List headers that should never be handled by Flask-RESTX
- HEADERS_BLACKLIST = ("Content-Length",)
- DEFAULT_REPRESENTATIONS = [("application/json", output_json)]
- log = logging.getLogger(__name__)
- class Api(object):
- """
- The main entry point for the application.
- You need to initialize it with a Flask Application: ::
- >>> app = Flask(__name__)
- >>> api = Api(app)
- Alternatively, you can use :meth:`init_app` to set the Flask application
- after it has been constructed.
- The endpoint parameter prefix all views and resources:
- - The API root/documentation will be ``{endpoint}.root``
- - A resource registered as 'resource' will be available as ``{endpoint}.resource``
- :param flask.Flask|flask.Blueprint app: the Flask application object or a Blueprint
- :param str version: The API version (used in Swagger documentation)
- :param str title: The API title (used in Swagger documentation)
- :param str description: The API description (used in Swagger documentation)
- :param str terms_url: The API terms page URL (used in Swagger documentation)
- :param str contact: A contact email for the API (used in Swagger documentation)
- :param str license: The license associated to the API (used in Swagger documentation)
- :param str license_url: The license page URL (used in Swagger documentation)
- :param str endpoint: The API base endpoint (default to 'api).
- :param str default: The default namespace base name (default to 'default')
- :param str default_label: The default namespace label (used in Swagger documentation)
- :param str default_mediatype: The default media type to return
- :param bool validate: Whether or not the API should perform input payload validation.
- :param bool ordered: Whether or not preserve order models and marshalling.
- :param str doc: The documentation path. If set to a false value, documentation is disabled.
- (Default to '/')
- :param list decorators: Decorators to attach to every resource
- :param bool catch_all_404s: Use :meth:`handle_error`
- to handle 404 errors throughout your app
- :param dict authorizations: A Swagger Authorizations declaration as dictionary
- :param bool serve_challenge_on_401: Serve basic authentication challenge with 401
- responses (default 'False')
- :param FormatChecker format_checker: A jsonschema.FormatChecker object that is hooked into
- the Model validator. A default or a custom FormatChecker can be provided (e.g., with custom
- checkers), otherwise the default action is to not enforce any format validation.
- :param url_scheme: If set to a string (e.g. http, https), then the specs_url and base_url will explicitly use this
- scheme regardless of how the application is deployed. This is necessary for some deployments behind a reverse
- proxy.
- """
- def __init__(
- self,
- app=None,
- version="1.0",
- title=None,
- description=None,
- terms_url=None,
- license=None,
- license_url=None,
- contact=None,
- contact_url=None,
- contact_email=None,
- authorizations=None,
- security=None,
- doc="/",
- default_id=default_id,
- default="default",
- default_label="Default namespace",
- validate=None,
- tags=None,
- prefix="",
- ordered=False,
- default_mediatype="application/json",
- decorators=None,
- catch_all_404s=False,
- serve_challenge_on_401=False,
- format_checker=None,
- url_scheme=None,
- **kwargs
- ):
- self.version = version
- self.title = title or "API"
- self.description = description
- self.terms_url = terms_url
- self.contact = contact
- self.contact_email = contact_email
- self.contact_url = contact_url
- self.license = license
- self.license_url = license_url
- self.authorizations = authorizations
- self.security = security
- self.default_id = default_id
- self.ordered = ordered
- self._validate = validate
- self._doc = doc
- self._doc_view = None
- self._default_error_handler = None
- self.tags = tags or []
- self.error_handlers = OrderedDict({
- ParseError: mask_parse_error_handler,
- MaskError: mask_error_handler,
- })
- self._schema = None
- self.models = {}
- self._refresolver = None
- self.format_checker = format_checker
- self.namespaces = []
- self.ns_paths = dict()
- self.representations = OrderedDict(DEFAULT_REPRESENTATIONS)
- self.urls = {}
- self.prefix = prefix
- self.default_mediatype = default_mediatype
- self.decorators = decorators if decorators else []
- self.catch_all_404s = catch_all_404s
- self.serve_challenge_on_401 = serve_challenge_on_401
- self.blueprint_setup = None
- self.endpoints = set()
- self.resources = []
- self.app = None
- self.blueprint = None
- # must come after self.app initialisation to prevent __getattr__ recursion
- # in self._configure_namespace_logger
- self.default_namespace = self.namespace(
- default,
- default_label,
- endpoint="{0}-declaration".format(default),
- validate=validate,
- api=self,
- path="/",
- )
- self.url_scheme = url_scheme
- if app is not None:
- self.app = app
- self.init_app(app)
- # super(Api, self).__init__(app, **kwargs)
- def init_app(self, app, **kwargs):
- """
- Allow to lazy register the API on a Flask application::
- >>> app = Flask(__name__)
- >>> api = Api()
- >>> api.init_app(app)
- :param flask.Flask app: the Flask application object
- :param str title: The API title (used in Swagger documentation)
- :param str description: The API description (used in Swagger documentation)
- :param str terms_url: The API terms page URL (used in Swagger documentation)
- :param str contact: A contact email for the API (used in Swagger documentation)
- :param str license: The license associated to the API (used in Swagger documentation)
- :param str license_url: The license page URL (used in Swagger documentation)
- :param url_scheme: If set to a string (e.g. http, https), then the specs_url and base_url will explicitly use
- this scheme regardless of how the application is deployed. This is necessary for some deployments behind a
- reverse proxy.
- """
- self.app = app
- self.title = kwargs.get("title", self.title)
- self.description = kwargs.get("description", self.description)
- self.terms_url = kwargs.get("terms_url", self.terms_url)
- self.contact = kwargs.get("contact", self.contact)
- self.contact_url = kwargs.get("contact_url", self.contact_url)
- self.contact_email = kwargs.get("contact_email", self.contact_email)
- self.license = kwargs.get("license", self.license)
- self.license_url = kwargs.get("license_url", self.license_url)
- self.url_scheme = kwargs.get("url_scheme", self.url_scheme)
- self._add_specs = kwargs.get("add_specs", True)
- # If app is a blueprint, defer the initialization
- try:
- app.record(self._deferred_blueprint_init)
- # Flask.Blueprint has a 'record' attribute, Flask.Api does not
- except AttributeError:
- self._init_app(app)
- else:
- self.blueprint = app
- def _init_app(self, app):
- """
- Perform initialization actions with the given :class:`flask.Flask` object.
- :param flask.Flask app: The flask application object
- """
- self._register_specs(self.blueprint or app)
- self._register_doc(self.blueprint or app)
- app.handle_exception = partial(self.error_router, app.handle_exception)
- app.handle_user_exception = partial(
- self.error_router, app.handle_user_exception
- )
- if len(self.resources) > 0:
- for resource, namespace, urls, kwargs in self.resources:
- self._register_view(app, resource, namespace, *urls, **kwargs)
- for ns in self.namespaces:
- self._configure_namespace_logger(app, ns)
- self._register_apidoc(app)
- self._validate = (
- self._validate
- if self._validate is not None
- else app.config.get("RESTX_VALIDATE", False)
- )
- app.config.setdefault("RESTX_MASK_HEADER", "X-Fields")
- app.config.setdefault("RESTX_MASK_SWAGGER", True)
- app.config.setdefault("RESTX_INCLUDE_ALL_MODELS", False)
- # check for deprecated config variable names
- if "ERROR_404_HELP" in app.config:
- app.config['RESTX_ERROR_404_HELP'] = app.config['ERROR_404_HELP']
- warnings.warn(
- "'ERROR_404_HELP' config setting is deprecated and will be "
- "removed in the future. Use 'RESTX_ERROR_404_HELP' instead.",
- DeprecationWarning
- )
- def __getattr__(self, name):
- try:
- return getattr(self.default_namespace, name)
- except AttributeError:
- raise AttributeError("Api does not have {0} attribute".format(name))
- def _complete_url(self, url_part, registration_prefix):
- """
- This method is used to defer the construction of the final url in
- the case that the Api is created with a Blueprint.
- :param url_part: The part of the url the endpoint is registered with
- :param registration_prefix: The part of the url contributed by the
- blueprint. Generally speaking, BlueprintSetupState.url_prefix
- """
- parts = (registration_prefix, self.prefix, url_part)
- return "".join(part for part in parts if part)
- def _register_apidoc(self, app):
- conf = app.extensions.setdefault("restx", {})
- if not conf.get("apidoc_registered", False):
- app.register_blueprint(apidoc.apidoc)
- conf["apidoc_registered"] = True
- def _register_specs(self, app_or_blueprint):
- if self._add_specs:
- endpoint = str("specs")
- self._register_view(
- app_or_blueprint,
- SwaggerView,
- self.default_namespace,
- "/swagger.json",
- endpoint=endpoint,
- resource_class_args=(self,),
- )
- self.endpoints.add(endpoint)
- def _register_doc(self, app_or_blueprint):
- if self._add_specs and self._doc:
- # Register documentation before root if enabled
- app_or_blueprint.add_url_rule(self._doc, "doc", self.render_doc)
- app_or_blueprint.add_url_rule(self.prefix or "/", "root", self.render_root)
- def register_resource(self, namespace, resource, *urls, **kwargs):
- endpoint = kwargs.pop("endpoint", None)
- endpoint = str(endpoint or self.default_endpoint(resource, namespace))
- kwargs["endpoint"] = endpoint
- self.endpoints.add(endpoint)
- if self.app is not None:
- self._register_view(self.app, resource, namespace, *urls, **kwargs)
- else:
- self.resources.append((resource, namespace, urls, kwargs))
- return endpoint
- def _configure_namespace_logger(self, app, namespace):
- for handler in app.logger.handlers:
- namespace.logger.addHandler(handler)
- namespace.logger.setLevel(app.logger.level)
- def _register_view(self, app, resource, namespace, *urls, **kwargs):
- endpoint = kwargs.pop("endpoint", None) or camel_to_dash(resource.__name__)
- resource_class_args = kwargs.pop("resource_class_args", ())
- resource_class_kwargs = kwargs.pop("resource_class_kwargs", {})
- # NOTE: 'view_functions' is cleaned up from Blueprint class in Flask 1.0
- if endpoint in getattr(app, "view_functions", {}):
- previous_view_class = app.view_functions[endpoint].__dict__["view_class"]
- # if you override the endpoint with a different class, avoid the
- # collision by raising an exception
- if previous_view_class != resource:
- msg = "This endpoint (%s) is already set to the class %s."
- raise ValueError(msg % (endpoint, previous_view_class.__name__))
- resource.mediatypes = self.mediatypes_method() # Hacky
- resource.endpoint = endpoint
- resource_func = self.output(
- resource.as_view(
- endpoint, self, *resource_class_args, **resource_class_kwargs
- )
- )
- # Apply Namespace and Api decorators to a resource
- for decorator in chain(namespace.decorators, self.decorators):
- resource_func = decorator(resource_func)
- for url in urls:
- # If this Api has a blueprint
- if self.blueprint:
- # And this Api has been setup
- if self.blueprint_setup:
- # Set the rule to a string directly, as the blueprint is already
- # set up.
- self.blueprint_setup.add_url_rule(
- url, view_func=resource_func, **kwargs
- )
- continue
- else:
- # Set the rule to a function that expects the blueprint prefix
- # to construct the final url. Allows deferment of url finalization
- # in the case that the associated Blueprint has not yet been
- # registered to an application, so we can wait for the registration
- # prefix
- rule = partial(self._complete_url, url)
- else:
- # If we've got no Blueprint, just build a url with no prefix
- rule = self._complete_url(url, "")
- # Add the url to the application or blueprint
- app.add_url_rule(rule, view_func=resource_func, **kwargs)
- def output(self, resource):
- """
- Wraps a resource (as a flask view function),
- for cases where the resource does not directly return a response object
- :param resource: The resource as a flask view function
- """
- @wraps(resource)
- def wrapper(*args, **kwargs):
- resp = resource(*args, **kwargs)
- if isinstance(resp, BaseResponse):
- return resp
- data, code, headers = unpack(resp)
- return self.make_response(data, code, headers=headers)
- return wrapper
- def make_response(self, data, *args, **kwargs):
- """
- Looks up the representation transformer for the requested media
- type, invoking the transformer to create a response object. This
- defaults to default_mediatype if no transformer is found for the
- requested mediatype. If default_mediatype is None, a 406 Not
- Acceptable response will be sent as per RFC 2616 section 14.1
- :param data: Python object containing response data to be transformed
- """
- default_mediatype = (
- kwargs.pop("fallback_mediatype", None) or self.default_mediatype
- )
- mediatype = request.accept_mimetypes.best_match(
- self.representations, default=default_mediatype,
- )
- if mediatype is None:
- raise NotAcceptable()
- if mediatype in self.representations:
- resp = self.representations[mediatype](data, *args, **kwargs)
- resp.headers["Content-Type"] = mediatype
- return resp
- elif mediatype == "text/plain":
- resp = original_flask_make_response(str(data), *args, **kwargs)
- resp.headers["Content-Type"] = "text/plain"
- return resp
- else:
- raise InternalServerError()
- def documentation(self, func):
- """A decorator to specify a view function for the documentation"""
- self._doc_view = func
- return func
- def render_root(self):
- self.abort(HTTPStatus.NOT_FOUND)
- def render_doc(self):
- """Override this method to customize the documentation page"""
- if self._doc_view:
- return self._doc_view()
- elif not self._doc:
- self.abort(HTTPStatus.NOT_FOUND)
- return apidoc.ui_for(self)
- def default_endpoint(self, resource, namespace):
- """
- Provide a default endpoint for a resource on a given namespace.
- Endpoints are ensured not to collide.
- Override this method specify a custom algorithm for default endpoint.
- :param Resource resource: the resource for which we want an endpoint
- :param Namespace namespace: the namespace holding the resource
- :returns str: An endpoint name
- """
- endpoint = camel_to_dash(resource.__name__)
- if namespace is not self.default_namespace:
- endpoint = "{ns.name}_{endpoint}".format(ns=namespace, endpoint=endpoint)
- if endpoint in self.endpoints:
- suffix = 2
- while True:
- new_endpoint = "{base}_{suffix}".format(base=endpoint, suffix=suffix)
- if new_endpoint not in self.endpoints:
- endpoint = new_endpoint
- break
- suffix += 1
- return endpoint
- def get_ns_path(self, ns):
- return self.ns_paths.get(ns)
- def ns_urls(self, ns, urls):
- path = self.get_ns_path(ns) or ns.path
- return [path + url for url in urls]
- def add_namespace(self, ns, path=None):
- """
- This method registers resources from namespace for current instance of api.
- You can use argument path for definition custom prefix url for namespace.
- :param Namespace ns: the namespace
- :param path: registration prefix of namespace
- """
- if ns not in self.namespaces:
- self.namespaces.append(ns)
- if self not in ns.apis:
- ns.apis.append(self)
- # Associate ns with prefix-path
- if path is not None:
- self.ns_paths[ns] = path
- # Register resources
- for r in ns.resources:
- urls = self.ns_urls(ns, r.urls)
- self.register_resource(ns, r.resource, *urls, **r.kwargs)
- # Register models
- for name, definition in six.iteritems(ns.models):
- self.models[name] = definition
- if not self.blueprint and self.app is not None:
- self._configure_namespace_logger(self.app, ns)
- def namespace(self, *args, **kwargs):
- """
- A namespace factory.
- :returns Namespace: a new namespace instance
- """
- kwargs["ordered"] = kwargs.get("ordered", self.ordered)
- ns = Namespace(*args, **kwargs)
- self.add_namespace(ns)
- return ns
- def endpoint(self, name):
- if self.blueprint:
- return "{0}.{1}".format(self.blueprint.name, name)
- else:
- return name
- @property
- def specs_url(self):
- """
- The Swagger specifications relative url (ie. `swagger.json`). If
- the spec_url_scheme attribute is set, then the full url is provided instead
- (e.g. http://localhost/swaggger.json).
- :rtype: str
- """
- external = None if self.url_scheme is None else True
- return url_for(
- self.endpoint("specs"), _scheme=self.url_scheme, _external=external
- )
- @property
- def base_url(self):
- """
- The API base absolute url
- :rtype: str
- """
- return url_for(self.endpoint("root"), _scheme=self.url_scheme, _external=True)
- @property
- def base_path(self):
- """
- The API path
- :rtype: str
- """
- return url_for(self.endpoint("root"), _external=False)
- @cached_property
- def __schema__(self):
- """
- The Swagger specifications/schema for this API
- :returns dict: the schema as a serializable dict
- """
- if not self._schema:
- try:
- self._schema = Swagger(self).as_dict()
- except Exception:
- # Log the source exception for debugging purpose
- # and return an error message
- msg = "Unable to render schema"
- log.exception(msg) # This will provide a full traceback
- return {"error": msg}
- return self._schema
- @property
- def _own_and_child_error_handlers(self):
- rv = OrderedDict()
- rv.update(self.error_handlers)
- for ns in self.namespaces:
- for exception, handler in six.iteritems(ns.error_handlers):
- rv[exception] = handler
- return rv
- def errorhandler(self, exception):
- """A decorator to register an error handler for a given exception"""
- if inspect.isclass(exception) and issubclass(exception, Exception):
- # Register an error handler for a given exception
- def wrapper(func):
- self.error_handlers[exception] = func
- return func
- return wrapper
- else:
- # Register the default error handler
- self._default_error_handler = exception
- return exception
- def owns_endpoint(self, endpoint):
- """
- Tests if an endpoint name (not path) belongs to this Api.
- Takes into account the Blueprint name part of the endpoint name.
- :param str endpoint: The name of the endpoint being checked
- :return: bool
- """
- if self.blueprint:
- if endpoint.startswith(self.blueprint.name):
- endpoint = endpoint.split(self.blueprint.name + ".", 1)[-1]
- else:
- return False
- return endpoint in self.endpoints
- def _should_use_fr_error_handler(self):
- """
- Determine if error should be handled with FR or default Flask
- The goal is to return Flask error handlers for non-FR-related routes,
- and FR errors (with the correct media type) for FR endpoints. This
- method currently handles 404 and 405 errors.
- :return: bool
- """
- adapter = current_app.create_url_adapter(request)
- try:
- adapter.match()
- except MethodNotAllowed as e:
- # Check if the other HTTP methods at this url would hit the Api
- valid_route_method = e.valid_methods[0]
- rule, _ = adapter.match(method=valid_route_method, return_rule=True)
- return self.owns_endpoint(rule.endpoint)
- except NotFound:
- return self.catch_all_404s
- except Exception:
- # Werkzeug throws other kinds of exceptions, such as Redirect
- pass
- def _has_fr_route(self):
- """Encapsulating the rules for whether the request was to a Flask endpoint"""
- # 404's, 405's, which might not have a url_rule
- if self._should_use_fr_error_handler():
- return True
- # for all other errors, just check if FR dispatched the route
- if not request.url_rule:
- return False
- return self.owns_endpoint(request.url_rule.endpoint)
- def error_router(self, original_handler, e):
- """
- This function decides whether the error occurred in a flask-restx
- endpoint or not. If it happened in a flask-restx endpoint, our
- handler will be dispatched. If it happened in an unrelated view, the
- app's original error handler will be dispatched.
- In the event that the error occurred in a flask-restx endpoint but
- the local handler can't resolve the situation, the router will fall
- back onto the original_handler as last resort.
- :param function original_handler: the original Flask error handler for the app
- :param Exception e: the exception raised while handling the request
- """
- if self._has_fr_route():
- try:
- return self.handle_error(e)
- except Exception as f:
- return original_handler(f)
- return original_handler(e)
- def handle_error(self, e):
- """
- Error handler for the API transforms a raised exception into a Flask response,
- with the appropriate HTTP status code and body.
- :param Exception e: the raised Exception object
- """
- # When propagate_exceptions is set, do not return the exception to the
- # client if a handler is configured for the exception.
- if (
- not isinstance(e, HTTPException)
- and current_app.propagate_exceptions
- and not isinstance(e, tuple(self._own_and_child_error_handlers.keys()))
- ):
- exc_type, exc_value, tb = sys.exc_info()
- if exc_value is e:
- raise
- else:
- raise e
- include_message_in_response = current_app.config.get(
- "ERROR_INCLUDE_MESSAGE", True
- )
- default_data = {}
- headers = Headers()
- for typecheck, handler in six.iteritems(self._own_and_child_error_handlers):
- if isinstance(e, typecheck):
- result = handler(e)
- default_data, code, headers = unpack(
- result, HTTPStatus.INTERNAL_SERVER_ERROR
- )
- break
- else:
- # Flask docs say: "This signal is not sent for HTTPException or other exceptions that have error handlers
- # registered, unless the exception was raised from an error handler."
- got_request_exception.send(current_app._get_current_object(), exception=e)
- if isinstance(e, HTTPException):
- code = HTTPStatus(e.code)
- if include_message_in_response:
- default_data = {"message": getattr(e, "description", code.phrase)}
- headers = e.get_response().headers
- elif self._default_error_handler:
- result = self._default_error_handler(e)
- default_data, code, headers = unpack(
- result, HTTPStatus.INTERNAL_SERVER_ERROR
- )
- else:
- code = HTTPStatus.INTERNAL_SERVER_ERROR
- if include_message_in_response:
- default_data = {
- "message": code.phrase,
- }
- if include_message_in_response:
- default_data["message"] = default_data.get("message", str(e))
- data = getattr(e, "data", default_data)
- fallback_mediatype = None
- if code >= HTTPStatus.INTERNAL_SERVER_ERROR:
- exc_info = sys.exc_info()
- if exc_info[1] is None:
- exc_info = None
- current_app.log_exception(exc_info)
- elif (
- code == HTTPStatus.NOT_FOUND
- and current_app.config.get("RESTX_ERROR_404_HELP", True)
- and include_message_in_response
- ):
- data["message"] = self._help_on_404(data.get("message", None))
- elif code == HTTPStatus.NOT_ACCEPTABLE and self.default_mediatype is None:
- # if we are handling NotAcceptable (406), make sure that
- # make_response uses a representation we support as the
- # default mediatype (so that make_response doesn't throw
- # another NotAcceptable error).
- supported_mediatypes = list(self.representations.keys())
- fallback_mediatype = (
- supported_mediatypes[0] if supported_mediatypes else "text/plain"
- )
- # Remove blacklisted headers
- for header in HEADERS_BLACKLIST:
- headers.pop(header, None)
- resp = self.make_response(
- data, code, headers, fallback_mediatype=fallback_mediatype
- )
- if code == HTTPStatus.UNAUTHORIZED:
- resp = self.unauthorized(resp)
- return resp
- def _help_on_404(self, message=None):
- rules = dict(
- [
- (RE_RULES.sub("", rule.rule), rule.rule)
- for rule in current_app.url_map.iter_rules()
- ]
- )
- close_matches = difflib.get_close_matches(request.path, rules.keys())
- if close_matches:
- # If we already have a message, add punctuation and continue it.
- message = "".join(
- (
- (message.rstrip(".") + ". ") if message else "",
- "You have requested this URI [",
- request.path,
- "] but did you mean ",
- " or ".join((rules[match] for match in close_matches)),
- " ?",
- )
- )
- return message
- def as_postman(self, urlvars=False, swagger=False):
- """
- Serialize the API as Postman collection (v1)
- :param bool urlvars: whether to include or not placeholders for query strings
- :param bool swagger: whether to include or not the swagger.json specifications
- """
- return PostmanCollectionV1(self, swagger=swagger).as_dict(urlvars=urlvars)
- @property
- def payload(self):
- """Store the input payload in the current request context"""
- return request.get_json()
- @property
- def refresolver(self):
- if not self._refresolver:
- self._refresolver = RefResolver.from_schema(self.__schema__)
- return self._refresolver
- @staticmethod
- def _blueprint_setup_add_url_rule_patch(
- blueprint_setup, rule, endpoint=None, view_func=None, **options
- ):
- """
- Method used to patch BlueprintSetupState.add_url_rule for setup
- state instance corresponding to this Api instance. Exists primarily
- to enable _complete_url's function.
- :param blueprint_setup: The BlueprintSetupState instance (self)
- :param rule: A string or callable that takes a string and returns a
- string(_complete_url) that is the url rule for the endpoint
- being registered
- :param endpoint: See BlueprintSetupState.add_url_rule
- :param view_func: See BlueprintSetupState.add_url_rule
- :param **options: See BlueprintSetupState.add_url_rule
- """
- if callable(rule):
- rule = rule(blueprint_setup.url_prefix)
- elif blueprint_setup.url_prefix:
- rule = blueprint_setup.url_prefix + rule
- options.setdefault("subdomain", blueprint_setup.subdomain)
- if endpoint is None:
- endpoint = _endpoint_from_view_func(view_func)
- defaults = blueprint_setup.url_defaults
- if "defaults" in options:
- defaults = dict(defaults, **options.pop("defaults"))
- blueprint_setup.app.add_url_rule(
- rule,
- "%s.%s" % (blueprint_setup.blueprint.name, endpoint),
- view_func,
- defaults=defaults,
- **options
- )
- def _deferred_blueprint_init(self, setup_state):
- """
- Synchronize prefix between blueprint/api and registration options, then
- perform initialization with setup_state.app :class:`flask.Flask` object.
- When a :class:`flask_restx.Api` object is initialized with a blueprint,
- this method is recorded on the blueprint to be run when the blueprint is later
- registered to a :class:`flask.Flask` object. This method also monkeypatches
- BlueprintSetupState.add_url_rule with _blueprint_setup_add_url_rule_patch.
- :param setup_state: The setup state object passed to deferred functions
- during blueprint registration
- :type setup_state: flask.blueprints.BlueprintSetupState
- """
- self.blueprint_setup = setup_state
- if setup_state.add_url_rule.__name__ != "_blueprint_setup_add_url_rule_patch":
- setup_state._original_add_url_rule = setup_state.add_url_rule
- setup_state.add_url_rule = MethodType(
- Api._blueprint_setup_add_url_rule_patch, setup_state
- )
- if not setup_state.first_registration:
- raise ValueError("flask-restx blueprints can only be registered once.")
- self._init_app(setup_state.app)
- def mediatypes_method(self):
- """Return a method that returns a list of mediatypes"""
- return lambda resource_cls: self.mediatypes() + [self.default_mediatype]
- def mediatypes(self):
- """Returns a list of requested mediatypes sent in the Accept header"""
- return [
- h
- for h, q in sorted(
- request.accept_mimetypes, key=operator.itemgetter(1), reverse=True
- )
- ]
- def representation(self, mediatype):
- """
- Allows additional representation transformers to be declared for the
- api. Transformers are functions that must be decorated with this
- method, passing the mediatype the transformer represents. Three
- arguments are passed to the transformer:
- * The data to be represented in the response body
- * The http status code
- * A dictionary of headers
- The transformer should convert the data appropriately for the mediatype
- and return a Flask response object.
- Ex::
- @api.representation('application/xml')
- def xml(data, code, headers):
- resp = make_response(convert_data_to_xml(data), code)
- resp.headers.extend(headers)
- return resp
- """
- def wrapper(func):
- self.representations[mediatype] = func
- return func
- return wrapper
- def unauthorized(self, response):
- """Given a response, change it to ask for credentials"""
- if self.serve_challenge_on_401:
- realm = current_app.config.get("HTTP_BASIC_AUTH_REALM", "flask-restx")
- challenge = '{0} realm="{1}"'.format("Basic", realm)
- response.headers["WWW-Authenticate"] = challenge
- return response
- def url_for(self, resource, **values):
- """
- Generates a URL to the given resource.
- Works like :func:`flask.url_for`.
- """
- endpoint = resource.endpoint
- if self.blueprint:
- endpoint = "{0}.{1}".format(self.blueprint.name, endpoint)
- return url_for(endpoint, **values)
- class SwaggerView(Resource):
- """Render the Swagger specifications as JSON"""
- def get(self):
- schema = self.api.__schema__
- return (
- schema,
- HTTPStatus.INTERNAL_SERVER_ERROR if "error" in schema else HTTPStatus.OK,
- )
- def mediatypes(self):
- return ["application/json"]
- def mask_parse_error_handler(error):
- """When a mask can't be parsed"""
- return {"message": "Mask parse error: {0}".format(error)}, HTTPStatus.BAD_REQUEST
- def mask_error_handler(error):
- """When any error occurs on mask"""
- return {"message": "Mask error: {0}".format(error)}, HTTPStatus.BAD_REQUEST
|