Currently I have foo.com/bar routing to a request handler Main. I also want foo.com/bar/id to route to that request handler (where "id" is an id of an object).
Here's what I tried but it's failing:
application = webapp.WSGIApplication(
[('/bar', MainHandler),
(r'/bar/(.*)', MainHandler)],
debug=True)
The error I get is:
TypeError: get() takes exactly 1 argument (2 given)
You need to change the signature of your MainHandler.get method, like so:
class MainHandler(webapp.RequestHandler):
def get(self, bar_id=None):
if bar_id is None:
# Handle /bar requests
else:
# Handle /bar/whatever requests
Related
I'm using the swagger editor (OpenApi 2) for creating flask apis in python. When you define a model in swagger and use it as a schema for the body of a request, swagger validates the body before handing the control to you in the X_controller.py files.
I want to add some code before that validation happens (for printing logs for debugging purposes). Swagger just prints to stdout errors like the following and they are not useful when you have a lot of fields (I need the key that isn't valid).
https://host/path validation error: False is not of type 'string'
10.255.0.2 - - [20/May/2020:20:20:20 +0000] "POST /path HTTP/1.1" 400 116 "-" "GuzzleHttp/7"
I know tecnically you can remove the validations in swagger and do them manually in your code but I want to keep using this feature, when it works it's awesome.
Any ideas on how to do this or any alternative to be able to log the requests are welcome.
After some time studying the matter this is what I learnt.
First let's take a look at how a python-flask server made with Swagger Editor works.
Swagger Editor generates the server stub through Swagger Codegen using the definition written in Swagger Editor. This server stub returned by codegen uses the framework Connexion on top of flask to handle all the HTTP requests and responses, including the validation against the swagger definition (swagger.yaml).
Connexion is a framework that makes it easy to develop python-flask servers because it has a lot of functionality you'd have to make yourself already built in, like parameter validation. All we need to do is replace (in this case modify) these connexion validators.
There are three validators:
ParameterValidator
RequestBodyValidator
ResponseValidator
They get mapped to flask by default but we can replace them easily in the __main__.py file as we'll see.
Our goal is to replace the default logs and default error response to some custom ones. I'm using a custom Error model and a function called error_response() for preparing error responses, and Loguru for logging the errors (not mandatory, you can keep the original one).
To make the changes needed, looking at the connexion validators code, we can see that most of it can be reused, we only need to modify:
RequestBodyValidator: __call__() and validate_schema()
ParameterValidator: __call__()
So we only need to create two new classes that extend the original ones, and copy and modify those functions.
Be careful when copying and pasting. This code is based on connexion==1.1.15. If your are on a different version you should base your classes on it.
In a new file custom_validators.py we need:
import json
import functools
from flask import Flask
from loguru import logger
from requests import Response
from jsonschema import ValidationError
from connexion.utils import all_json, is_null
from connexion.exceptions import ExtraParameterProblem
from swagger_server.models import Error
from connexion.decorators.validation import ParameterValidator, RequestBodyValidator
app = Flask(__name__)
def error_response(response: Error) -> Response:
return app.response_class(
response=json.dumps(response.to_dict(), default=str),
status=response.status,
mimetype='application/json')
class CustomParameterValidator(ParameterValidator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __call__(self, function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""
#functools.wraps(function)
def wrapper(request):
if self.strict_validation:
query_errors = self.validate_query_parameter_list(request)
formdata_errors = self.validate_formdata_parameter_list(request)
if formdata_errors or query_errors:
raise ExtraParameterProblem(formdata_errors, query_errors)
for param in self.parameters.get('query', []):
error = self.validate_query_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
for param in self.parameters.get('path', []):
error = self.validate_path_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
for param in self.parameters.get('header', []):
error = self.validate_header_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
for param in self.parameters.get('formData', []):
error = self.validate_formdata_parameter(param, request)
if error:
response = error_response(Error(status=400, description=f'Error: {error}'))
return self.api.get_response(response)
return function(request)
return wrapper
class CustomRequestBodyValidator(RequestBodyValidator):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __call__(self, function):
"""
:type function: types.FunctionType
:rtype: types.FunctionType
"""
#functools.wraps(function)
def wrapper(request):
if all_json(self.consumes):
data = request.json
if data is None and len(request.body) > 0 and not self.is_null_value_valid:
# the body has contents that were not parsed as JSON
return error_response(Error(
status=415,
description="Invalid Content-type ({content_type}), JSON data was expected".format(content_type=request.headers.get("Content-Type", ""))
))
error = self.validate_schema(data, request.url)
if error and not self.has_default:
return error
response = function(request)
return response
return wrapper
def validate_schema(self, data, url):
if self.is_null_value_valid and is_null(data):
return None
try:
self.validator.validate(data)
except ValidationError as exception:
description = f'Validation error. Attribute "{exception.validator_value}" return this error: "{exception.message}"'
logger.error(description)
return error_response(Error(
status=400,
description=description
))
return None
Once we have our validators, we have to map them to the flask app (__main__.py) using validator_map:
validator_map = {
'parameter': CustomParameterValidator,
'body': CustomRequestBodyValidator,
'response': ResponseValidator,
}
app = connexion.App(__name__, specification_dir='./swagger/', validator_map=validator_map)
app.app.json_encoder = encoder.JSONEncoder
app.add_api(Path('swagger.yaml'), arguments={'title': 'MyApp'})
If you also need to replace the validator I didn't use in this example, just create a custom child class of ResponseValidator and replace it on the validator_map dictionary in __main__.py.
Connexion docs:
https://connexion.readthedocs.io/en/latest/request.html
Forgive me for repeating an answer first posted at https://stackoverflow.com/a/73051652/1630244
Have you tried the Connexion before_request feature? Here's an example that logs the headers and content before Connexion validates the body:
import connexion
import logging
from flask import request
logger = logging.getLogger(__name__)
conn_app = connexion.FlaskApp(__name__)
#conn_app.app.before_request
def before_request():
for h in request.headers:
logger.debug('header %s', h)
logger.debug('data %s', request.get_data())
I just started to code in Flask swagger backend. When I tried to extract multiple paths and the argument parameters, an error occurs, says:
TypeError: get() missing 1 required positional argument: 'q'
But actually, my "get()" function has the positional argument 'q'. I may ask why this error occurs? That could be weird since I use
request.arg.get('q')
it works good and can returns the expected query argument.
The expected API paths is:
GET 127.0.0.1:8888/book/1?q=question20
and my code is here:
from flask import Flask, request
from flask_restplus import Resource, Api
app = Flask(__name__)
api = Api(app)
#api.route("/<string:resource>/<int:rid>")
#api.doc(params={'q': 'Your query here'})
class API(Resource):
def get(self, resource, rid, q):
return {"resource": resource,
"id": rid,
"query": q
}, 200
if __name__ == "__main__":
app.run(host='127.0.0.1', port=8888, debug=True)
I would appreciate if someone can help me solve this problem.
You can use api.parser() to document and capture query parameters
parser = api.parser()
parser.add_argument('q', type=str, help='Resource name', location='args')
#api.route("/<string:resource>/<int:rid>")
#api.doc(parser=parser)
class API(Resource):
def get(self, resource, rid):
args = parser.parse_args()
q = args['q']
return {"resource": resource,
"id": rid,
"q":q,
}, 200
This method is marked as deprecated https://flask-restplus.readthedocs.io/en/stable/parsing.html
I have following code:
application = tornado.web.Application([
(r"/get(.*)", GetHandler),
])
class GetHandler(tornado.web.RequestHandler):
def get(self, key):
response = {'key': key}
self.write(response)
when I go to localhost:port/get?key=python I receive empty key value {'key': ''}. What is wrong here?
(.*) in regex matches everything. So this — (r"/get(.*)", GetHandler) — will match anything followed by /get, example:
/get
/getsomething
/get/something
/get.asldfkj%5E&(*&fkasljf
Let's say a request comes in at localhost:port/get/something, then the value of key argument in GetHandler.get(self, key) will be /something (yes, including the slash because .* matches everything).
But if a request comes in at localhost:port/get?key=python, the value of key argument in GETHandler.get(self, key) will be an empty string. It happens because the part containing ?key=python is called a Query String. It's not part of the url path. Tornado (or almost every other web framework) doesn't pass this to the view as an argument.
There are two ways you can change your code:
If you want to access your view like this - localhost:port/get?key=python, you'll need to make changes to your url config and to your view:
application = tornado.web.Application([
(r"/get", GetHandler),
])
class GetHandler(tornado.web.RequestHandler):
def get(self):
key = self.get_argument('key', None)
response = {'key': key}
self.write(response)
If you don't want to change your app url config and your view, you'll need to make the request like this - localhost:port/get/python.
But still you'll need to make a small change to your url config. Add a slash - / - between get and (.*), because otherwise the value of key would be /python instead of python.
application = tornado.web.Application([
(r"/get/(.*)", GetHandler), # note the slash
])
I hope that you will figure what you did wrong by yourself - that's a task for you.
Your working code:
import tornado.ioloop
import tornado.web
class GetHandler(tornado.web.RequestHandler):
def get(self):
response = self.get_arguments("key")
if len(response) == 0:
# Handle me
self.set_status(400)
return self.finish("Invalid key")
self.write({"key":self.get_argument("key")})
def make_app():
return tornado.web.Application([
(r"/", GetHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
I have a problem making the simple (non-json) arguments work with POST. Just taking the simple example from their tutorials, I can't make a unit test where the task is passing as an argument. However task is never passed in. Its none.
class TodoList(Resource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
self.reqparse.add_argument('task', type = str)
super(TodoList, self).__init__()
def post(self):
args = self.reqparse.parse_args()
#args['task'] is None, but why?
return TODOS[args['task']], 201
Unit test:
def test_task(self):
rv = self.app.post('todos', data='task=test')
self.check_content_type(rv.headers)
resp = json.loads(rv.data)
eq_(rv.status_code, 201)
What am I missing please?
When you use 'task=test' test_client do not set application/x-www-form-urlencoded content type, because you put string to input stream. So flask can't detect form and read data from form and reqparse will return None for any values for this case.
To fix it you must set up content type or use dict {'task': 'test'} or tuple.
Also for request testing better to use client = self.app.test_client() instead app = self.app.test_client(), if you use FlaskTesting.TestCase class, then just call self.client.post.
I adapted this sample code in order to get webapp2 sessions to work on Google App Engine.
What do I need to do to be able to return webapp2.Response objects from a handler that's inheriting from a BaseHandler that overrides the dispatch method?
Here's a demonstration of the kind of handler I want to write:
import webapp2
import logging
from webapp2_extras import sessions
class BaseHandler(webapp2.RequestHandler):
def dispatch(self):
# Get a session store for this request.
self.session_store = sessions.get_store(request=self.request)
try:
# Dispatch the request.
webapp2.RequestHandler.dispatch(self)
finally:
# Save all sessions.
self.session_store.save_sessions(self.response)
class HomeHandler(BaseHandler):
def get(self):
logging.debug('In homehandler')
response = webapp2.Response()
response.write('Foo')
return response
config = {}
config['webapp2_extras.sessions'] = {
'secret_key': 'some-secret-key',
}
app = webapp2.WSGIApplication([
('/test', HomeHandler),
], debug=True, config=config)
This code is obviously not working, since BaseHandler always calls dispatch with self. I've looked through the code of webapp2.RequestHandler, but it seriously eludes me how to modify my BaseHandler (or perhaps set a custom dispatcher) such that I can simply return response objects from inheriting handlers.
Curiously, the shortcut of assigning self.response = copy.deepcopy(response) does not work either.
You're mixing the two responses in one method. Use either
return webapp2.Response('Foo')
or
self.response.write('Foo')
...not both.
I took a look at webapp2.RequestHandler and noticed that returned values are just passed up the stack.
A solution which works for me is to use the returned Response when one is returned from the handler, or self.response when nothing is returned.
class BaseHandler(webapp2.RequestHandler):
def dispatch(self):
# Get a session store for this request.
self.session_store = sessions.get_store(request=self.request)
response = None
try:
# Dispatch the request.
response = webapp2.RequestHandler.dispatch(self)
return response
finally:
# Save all sessions.
if response is None:
response = self.response
self.session_store.save_sessions(response)
While I was playing I noticed that my session stored as a secure cookie was not getting updated when exceptions were raised in the handler.