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

How to Use Schema with Path Parameter? #219

Closed
Anti-Distinctlyminty opened this issue Jan 27, 2021 · 6 comments
Closed

How to Use Schema with Path Parameter? #219

Anti-Distinctlyminty opened this issue Jan 27, 2021 · 6 comments

Comments

@Anti-Distinctlyminty
Copy link

In the example below, I am trying to figure out how to use the ClientSchema to correctly document and validate ClientResource.get. Is this possible?

ma = flask_marshmallow.Marshmallow()

class ClientSchema(ma.SQLAlchemyAutoSchema):
    class Meta:
        model = Client
    clientId  = ma.auto_field(description='The ID of the client', 
                                            required=True,
                                            validate=validate.Range(min=1))
                                            )

@bp.route('/<int:clientId>')
class ClientResource(MethodView):

    def get(self, *args, **kwargs):
        pass
@Anti-Distinctlyminty Anti-Distinctlyminty changed the title How to Use Path Parameter in blueprint.route? How to Use Schema with Path Parameter? Jan 27, 2021
@lafrech
Copy link
Member

lafrech commented Jan 27, 2021

Can you try this?

@bp.route('/<int:clientId>')
@blp.arguments(ClientSchema, location='path')
class ClientResource(MethodView):

    def get(self, *args, **kwargs):
        pass

@Anti-Distinctlyminty
Copy link
Author

That gives me the description in the OpenAPI interface, but also results in TypeError: object() takes no parameters when I try to use it.

Stack Trace
ERROR : 2021-01-27 11:04:51 : werkzeug : Error on request:
Traceback (most recent call last):
  File "/home/lukesapi/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 323, in run_wsgi
    execute(self.server.app)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/werkzeug/serving.py", line 312, in execute
    application_iter = app(environ, start_response)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 2464, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 2450, in wsgi_app
    response = self.handle_exception(e)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 1867, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 2447, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 1952, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask_cors/extension.py", line 161, in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 1821, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
    raise value
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 1950, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask/app.py", line 1936, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/webargs/core.py", line 445, in wrapper
    return func(*args, **kwargs)
  File "/home/lukesapi/.local/lib/python3.6/site-packages/flask_smorest/arguments.py", line 77, in wrapper
    return func(*f_args, **f_kwargs)
TypeError: object() takes no parameters

If I move the arguments decorator on the get function, it does seem to work, however unless the get signature is

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

I get similar type errors complaining about umbers of arguments.
I also get both *args and **kwargs populated with a clientId.

@lafrech
Copy link
Member

lafrech commented Jan 27, 2021

Yes, sorry, it should be on the function, not the class.

The issue with this method is that both Flask and marshmallow/webargs send the parameter to the function. I don't know how to inhibit this in Flask to let the schema do it alone.

I never use a schema to validate and document path parameters.

I rely on werkzeug path parameter converters for the validation, and if I define a custom path parameter converter, I register it in flask-smorest to get automatic documentation as explained in https://flask-smorest.readthedocs.io/en/latest/openapi.html#register-custom-path-parameter-converters.

To add extra documentation, one can then pass parameters parameter to route.
https://flask-smorest.readthedocs.io/en/latest/openapi.html#document-path-parameters

In your case, I'd make clientId a read-only field so that it is not used on PUT and I'd rely on flask/werkzeug to pass the id on GET/PUT/DELETE.

You lose the validate=Range(min=1) in the process but it is not that bad. The query will fail and you'll return a 404 just like when using a wrong ID. And the id>=1 won't be documented but you don't have to inform users about your ID bounds if your app creates the IDs.

Note that werkzeug's int converter has a signed parameter to allow negative values and it False by default and you can even specify min if you want, and it will be documented by flask-smorest.

TL;DR

This should get you there. Or close.

@bp.route('/<int(min=1):clientId>', parameters=["clientId": {'description': 'The ID of the client'})
class ClientResource(MethodView):

    def get(self, *args, **kwargs):
        pass

@Anti-Distinctlyminty
Copy link
Author

Anti-Distinctlyminty commented Jan 27, 2021

I never use a schema to validate and document path parameters.

This is actually my main motivation. I'm trying to keep things as centralized as possible by not having documentation scattered around and not having to check arguments at the head of every method (the example I gave above is very simplified).

But as long as using

@bp.route('/<int:clientId>')
class ClientResource(MethodView):
    @bp.arguments(ClientSchema, location='path', as_kwargs=True)
    def get(self, **kwargs):
        pass

isn't the wrong way to do things, and will not cause me any issues, then this seems relatively neat..

@Anti-Distinctlyminty
Copy link
Author

I rely on werkzeug path parameter converters for the validation, and if I define a custom path parameter converter, I register it in flask-smorest to get automatic documentation as explained in https://flask-smorest.readthedocs.io/en/latest/openapi.html#register-custom-path-parameter-converters.

I did think that the parameter converters could be employed to solve this issue, but I'm not familiar with werkzeug so really don't know how to do this.

@lafrech
Copy link
Member

lafrech commented Jan 27, 2021

I did think that the parameter converters could be employed to solve this issue, but I'm not familiar with werkzeug so really don't know how to do this.

My example above should do what you want.

I understand the will to centralize.

I'd choose my way other yours because sending clientId twice to the view func kinda sucks while my method only leaves a bit of documentation in the call to route which is not that bad.

Yours should work too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants