python-restx / flask-restx

Fork of Flask-RESTPlus: Fully featured framework for fast, easy and documented API development with Flask
https://flask-restx.readthedocs.io/en/latest/
Other
2.16k stars 335 forks source link

Swagger - response type binary #497

Open 0xbart opened 1 year ago

0xbart commented 1 year ago

How do i define a function to responds with a binary file instead of json? This because typescript auto generated api documentation and does not use 'type: blob' on this api call.

Code

import pathlib
from flask_restx import Namespace, Resource

api = Namespace("Auth", path="/api/v1/")

@api.route("/download/")
class Download(Resource):
    def get(self):
        f_io = pathlib.Path("/tmp/file")
        # file object
        return send_file(f_io, mimetype="image/png")

Expected Behavior

Swagger example: https://swagger.io/docs/specification/describing-responses/#response-that-returns-a-file (3.0) or https://swagger.io/docs/specification/2-0/describing-responses/ (2.0)

Actual Behavior

Swagger example:

"get": {
    "responses": {
        "200": {
            "description": "Success"
        }
    },
    "summary": "Download file",
    "operationId": "download",
    "tags": [
        "download"
    ]
}
0xbart commented 1 year ago

Nasty hack to bypass this issue is by intercepting the SwaggerView:

First the Custom API class:

from flask_restx import Api

class CustomApi(Api):
    def __init__(self):
        super().__init__(
            **{
                "version": config.API_VERSION,
                "title": config.API_TITLE,
                "doc": config.API_DOC_URL,
                "description": config.API_DESCRIPTION,
                "authorizations": authorization,
                "security": ["x-api-key", "bearer"],
            }
        )

    def _register_specs(self, app_or_blueprint):
        if self._add_specs:
            endpoint = str("specs")
            self._register_view(
                app_or_blueprint,
                CustomSwaggerView,  # <---- link this to the new custom class (see below)
                self.default_namespace,
                f"{config.API_DOC_URL}/swagger.json",
                endpoint=endpoint,
                resource_class_args=(self,),
            )
            self.endpoints.add(endpoint)

    @property
    def specs_url(self):
        # overwrite custom swagger json url
        # please do not add '/' after API_DOC_URL, to prevent //swagger.json url style
        return f"{self.base_url[:-1]}{config.API_DOC_URL}swagger.json"

Furthermore, the Custom Swagger class:

from flask_restx.api import SwaggerView

class CustomSwaggerView(SwaggerView):
    """
    Use custom swagger view to support different type of response, instead of the default application/json.
    This is required because the generated Typescript code by parsing the Swagger json is using json for all requests.
    Some API calls require type blob instead of json, such as: QRcode PNG setup.
    """

    def get(self):
        schema = self.api.__schema__

        # Check if schema contains any errors. If errors, do not try to manipulate
        # the dict and return created schema instead.
        if "error" in schema:
            return schema, 500

        for ns in self.api.namespaces:
            for resource, urls, route_doc, kwargs in ns.resources:
                # check if resource has value 'custom_swagger' set.
                # custom swagger could be set via the following attribute (passing to the Resource class):
                #
                #   custom_swagger = {
                #         "get": {
                #             "responses": {"200": {"description": "Success", "schema": {"type": "file"}}},
                #             "produces": ["image/png"],
                #         }
                #     }

                custom_swagger = getattr(resource, "custom_swagger", None)

                if custom_swagger:
                    # if custom swagger, loop through all associated URLs and update dict
                    for url in self.api.ns_urls(ns, urls):
                        for method, method_values in custom_swagger.items():
                            if method.lower() not in schema["paths"][url]:
                                continue

                            for key, value in method_values.items():
                                if key == 'responses':
                                    schema["paths"][url][method.lower()][key].update(value)
                                else:
                                    schema["paths"][url][method.lower()][key] = value

        return schema, 200

Now lets create an example API call which returns an PNG instead of json:


from flask_restx import Resource

class ExampleResource(Resource):
    custom_swagger = {
        "get": {
            "responses": {"200": {"description": "Success", "schema": {"type": "file"}}},
            "produces": ["image/png"],
        }
    }

    @api.route('/')
    def get(self):
        # implement me
        pass