noirbizarre / flask-restplus

Fully featured framework for fast, easy and documented API development with Flask
http://flask-restplus.readthedocs.org
Other
2.73k stars 507 forks source link

Flask-RESTPlus does not generate Swagger UI correctly #724

Open deepaerial opened 5 years ago

deepaerial commented 5 years ago

Swagger UI not generating interactive docs for resource

Code

Example code for images endpoint

import io
from flask import request, abort, send_file
from flask_restplus import Namespace, Resource, reqparse
from celery.result import AsyncResult

from scrapy_api.models import ImageTask, Image
from scrapy_api import errors

__all__ = ['image_namespace']

image_namespace = Namespace('/image/', description='Images endpoint')

put_arguments = reqparse.RequestParser()
put_arguments.add_argument('url', type=str, help='URL of web page.')

post_arguments = reqparse.RequestParser()
post_arguments.add_argument('task_id', type=str, help='Task id.')

get_arguments = reqparse.RequestParser()
get_arguments.add_argument('task_id', type=str, help='Task id.')
get_arguments.add_argument('img_id', type=int, help='Image id.')

@image_namespace.errorhandler(ImageTask.DoesNotExist)
def image_task_does_not_exist(error):
    return errors.make_error_response(errors.TaskDoesNotExist())

@image_namespace.route('/')
class ImageAPI(Resource):
    '''
    Images endpoint
    '''
    def get_image(self, task_id, id):
        task = ImageTask.objects(task_id=task_id, images__id=id).first()
        image = next((img for img in task.images if img.id == id), None)
        return image

    @image_namespace.expect(put_arguments)
    def put(self):
        '''
        Submit task to get images from page by url.
        '''
        json_data = image_namespace.payload
        image = ImagePUTInput.load(json_data).data
        image.save()
        return ImagePUTOutput.dump(image)

    @image_namespace.expect(post_arguments)
    def post(self):
        '''
        Get status of submitted task.
        '''
        json_data = image_namespace.payload
        task_id = json_data['task_id']
        image = ImageTask.objects.get(task_id=task_id)
        celery_task = AsyncResult(image.task_id)
        image.task_status = celery_task.state
        image.save()
        return ImagePOSTOutput.dump(image)

    @image_namespace.expect(get_arguments)
    @image_namespace.produces(['image/jpeg'])
    def get(self):
        '''
        Get image from task result.
        '''
        task_id = request.args.get('task_id')
        img_id = int(request.args.get('img_id'))
        image = self.get_image(task_id, img_id)
        if not image:
            abort(404)
        return send_file(io.BytesIO(image.img),
                         mimetype='image/jpeg',
                         as_attachment=True,
                         attachment_filename='{}-{}-{}.jpg'.format(
                             'Image', task_id, id))

Repro Steps (if applicable)

  1. Add namespace to api
  2. Open Swagger UI in browser

Expected Behavior

Generates Swagger documentation with interactive UI

Actual Behavior

Endpoints appear in Swagger UI but detailed information about them is not displayed.

Error Messages/Stack Trace

Some errors related to React are appearing in browser console:


TypeError: e.parentNode is null
DOMLazyTree.js:67
    React 128
        replaceChildWithTree
        dangerouslyReplaceNodeWithMarkup
        _replaceNodeWithMarkup
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        updateChildren
        _reconcilerUpdateChildren
        _updateChildren
        updateChildren
        _updateDOMChildren
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        updateChildren
        _reconcilerUpdateChildren
        _updateChildren
        updateChildren
        _updateDOMChildren
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        updateChildren
        _reconcilerUpdateChildren
        _updateChildren
        updateChildren
        _updateDOMChildren
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        updateChildren
        _reconcilerUpdateChildren
        _updateChildren
        updateChildren
        _updateDOMChildren
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        updateChildren
        _reconcilerUpdateChildren
        _updateChildren
        updateChildren
        _updateDOMChildren
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        updateChildren
        _reconcilerUpdateChildren
        _updateChildren
        updateChildren
        _updateDOMChildren
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        updateChildren
        _reconcilerUpdateChildren
        _updateChildren
        updateChildren
        _updateDOMChildren
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
        receiveComponent
        _updateRenderedComponent
        _performComponentUpdate
        updateComponent
        receiveComponent
TypeError: l is null
OperationContainer.jsx:64:26
TypeError: e.parentNode is null
DOMLazyTree.js:67

Environment

Additional Context

screenrecording

I've checked code on previous version (0.12.1) and it works perfectly fine.

j5awry commented 5 years ago

could you describe how you installed flask-restplus?

Asking because we don't have a version 0.13.3 anywhere. Last release release on PyPi was 0.13.0. Current master is on 0.13.1-dev

Just wondering if it's a typo or you're on someone else's fork?

deepaerial commented 5 years ago

@j5awry I'm sorry. I made a typo. It's 0.13.0. I've updated issue.

deepaerial commented 5 years ago

For simpler bug reproduction we can use this code:

from flask_restplus import Namespace, Resource

ping_namespace = Namespace(
    '/ping/', description='Endpoint for checking service availability.')

@ping_namespace.route('')
class PingAPI(Resource):
    '''
    Ping endpoint.
    '''

    def get(self):
        return {'message': 'OK'}

This should make Swagger UI with GET section inside ping namespace. But in current version (0.13.0) it's not displayed.

j5awry commented 5 years ago

I'll need to know more about your environment. My guess is there are missing assets from Swagger. I set up my environment using the task in flask-restplus' codeline to get all the Swagger base assets:

https://github.com/noirbizarre/flask-restplus/blob/master/tasks.py#L180

def assets(ctx):
    '''Fetch web assets'''
    header(assets.__doc__)
    with ctx.cd(ROOT):
        ctx.run('npm install')
        ctx.run('mkdir -p flask_restplus/static')
        ctx.run('cp node_modules/swagger-ui-dist/{swagger-ui*.{css,js}{,.map},favicon*.png,oauth2-redirect.html} flask_restplus/static')
        # Until next release we need to install droid sans separately
        ctx.run('cp node_modules/typeface-droid-sans/index.css flask_restplus/static/droid-sans.css')
        ctx.run('cp -R node_modules/typeface-droid-sans/files flask_restplus/static/')

With that in mind, and knowing my local running environment (from the flask-restplus codeline from Git, after running inv assets), I did the following, and was unable to reproduce

  1. create fresh virtualenv of python 3.6.6
  2. pip install flask-restplus
  3. run our todo.py demo: https://github.com/noirbizarre/flask-restplus/blob/master/examples/todo.py

pip freeze contents:

aniso8601==8.0.0
attrs==19.2.0
Click==7.0
Flask==1.1.1
flask-restplus==0.13.0
invoke==1.3.0
itsdangerous==1.1.0
Jinja2==2.10.1
jsonschema==3.0.2
MarkupSafe==1.1.1
pyrsistent==0.15.4
pytz==2019.2
six==1.12.0
Werkzeug==0.16.0

(invoke was added so i could quickly run the demo without trying)

Considering it's React errors, I'd assume it's missing some Swagger assets somewhere.

deepaerial commented 5 years ago

Did you tested this on 0.13.0 version?

I've installed Flask-RESTPlus using pipenv and it automatically fetched latest version:


$ pipenv install flask-restplus
deepaerial commented 5 years ago

Here is the part of code where I add namespaces:

def register_blueprints(app):
    '''
    Register blueprints for app.
    '''
    blueprint = Blueprint('api', __name__, url_prefix='/api')
    api = Api(blueprint,
              title=app.config.app_name,
              version=app.config.app_version,
              description=app.config.app_descr)

    # Error handlers
    @blueprint.app_errorhandler(404)
    def path_not_found(error):
        return errors.make_error_response(errors.Error404())

    @api.errorhandler
    def default_error_handler(error):
        return errors.make_error_response(error)

    # Adding namespaces
    api.add_namespace(ping_namespace, '/ping')
    api.add_namespace(document_namespace, '/document')
    api.add_namespace(image_namespace, '/image')

    app.register_blueprint(blueprint)

Result of pip freeze:

amqp==2.5.1
aniso8601==8.0.0
appdirs==1.4.3
astroid==2.3.1
atomicwrites==1.3.0
attrs==19.2.0
beautifulsoup4==4.8.0
billiard==3.6.1.0
blinker==1.4
bs4==0.0.1
celery==4.3.0
certifi==2019.9.11
chardet==3.0.4
Click==7.0
cssselect==1.1.0
dynaconf==2.1.1
fake-useragent==0.1.11
fancycompleter==0.8
Flask==1.1.1
flask-mongoengine==0.9.5
flask-restplus==0.13.0
Flask-WTF==0.14.2
gunicorn==19.9.0
idna==2.8
importlib-metadata==0.23
isort==4.3.21
itsdangerous==1.1.0
Jinja2==2.10.1
jsonschema==3.0.2
kombu==4.6.5
lazy-object-proxy==1.4.2
lxml==4.4.1
MarkupSafe==1.1.1
mccabe==0.6.1
mongoengine==0.18.2
more-itertools==7.2.0
packaging==19.2
parse==1.12.1
pdbpp==0.10.0
pluggy==0.13.0
py==1.8.0
pyee==6.0.0
Pygments==2.4.2
pylint==2.4.2
pymongo==3.9.0
pyparsing==2.4.2
pyppeteer==0.0.25
pyquery==1.4.0
pyrsistent==0.15.4
pytest==5.2.0
python-box==3.4.5
python-dotenv==0.10.3
pytz==2019.2
PyYAML==5.1.2
redis==3.3.8
requests==2.22.0
requests-html==0.10.0
rope==0.14.0
six==1.12.0
soupsieve==1.9.4
toml==0.10.0
tqdm==4.36.1
typed-ast==1.4.0
urllib3==1.25.6
vine==1.3.0
w3lib==1.21.0
wcwidth==0.1.7
websockets==8.0.2
Werkzeug==0.16.0
wmctrl==0.3
wrapt==1.11.2
WTForms==2.2.1
yapf==0.28.0
zipp==0.6.0
j5awry commented 5 years ago

i tested on 0.13.0. As stated, if you're seeing errors in the browser related to React, then it's probably an issue with missing assets. You'll need to NPM install the following depenedencies to get the Swagger UI running

  "dependencies": {
    "swagger-ui-dist": "^3.4.0",
    "typeface-droid-sans": "0.0.40"
  }

These can be found in https://github.com/noirbizarre/flask-restplus/blob/master/package.json

deepaerial commented 5 years ago

@j5awry I've tried to copy and run example you've mentioned earlier and it works ok. But my code don't work with current version

SteadBytes commented 5 years ago

@deepaerial I have tried to reproduce this issue using your example code and I'm unable to do so. Could you please try creating a clean Python environment, reinstalling the minimum dependencies to get the example you provided working and try to reproduce?

deepaerial commented 5 years ago

@SteadBytes I've completely removed virtual environment and installed project again. Unfortuanatelly, nothing changed. Here is repo. Installed version from Pipfile.lock:

        "flask-restplus": {
            "hashes": [
                "sha256:a15d251923a8feb09a5d805c2f4d188555910a42c64d58f7dd281b8cac095f1b",
                "sha256:a66e442d0bca08f389fc3d07b4d808fc89961285d12fb8013f7cf15516fa9f5c"
            ],
            "index": "pypi",
            "version": "==0.13.0"
vgonisanz commented 4 years ago

I have the same problem using Flask-RESTPlus version: 0.12.1 and 0.13.0