[英]Is it possible to run custom code before Swagger validations in a python/flask server stub?
我正在使用 swagger 编辑器 (OpenApi 2) 在 python 中创建 flask api。 当您在 swagger 中定义 model 并将其用作请求主体的模式时,swagger 在将控制文件交给您之前验证主体。
我想在验证发生之前添加一些代码(用于打印日志以进行调试)。 Swagger 只是打印到标准输出错误,如下所示,当您有很多字段时它们没有用(我需要无效的密钥)。
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"
我知道技术上您可以删除 swagger 中的验证并在您的代码中手动执行它们,但我想继续使用此功能,当它工作时它很棒。
欢迎任何关于如何执行此操作的想法或任何能够记录请求的替代方法。
经过一段时间的研究,这就是我所学到的。
首先让我们看看使用 Swagger Editor 制作的 python-flask 服务器是如何工作的。
Swagger Editor 使用 Swagger Editor 中编写的定义通过 Swagger Codegen 生成服务器存根。 codegen 返回的这个服务器存根使用 flask 之上的框架 Connexion 来处理所有 HTTP 请求和响应,包括针对 Z5DB89E7472F81A4EA6B7A73F7C6729 定义的验证。
Connexion 是一个让开发 python-flask 服务器变得容易的框架,因为它有很多你必须让自己内置的功能,比如参数验证。 我们需要做的就是替换(在这种情况下修改)这些连接验证器。
有三个验证器:
默认情况下,它们被映射到 flask,但我们可以在__main__.py
文件中轻松替换它们,正如我们将看到的。
我们的目标是将默认日志和默认错误响应替换为一些自定义日志。 我正在使用自定义Error
model 和名为error_response()
的 function 来准备错误响应,并使用 Loguru 来记录错误(不是强制性的,您可以保留原始错误)。
要进行所需的更改,查看连接验证器代码,我们可以看到大部分都可以重用,我们只需要修改:
__call__()
和validate_schema()
__call__()
所以我们只需要创建两个新的类来扩展原来的类,并复制和修改这些功能。
复制和粘贴时要小心。 此代码基于 connexion==1.1.15。 如果您使用的是不同的版本,您应该将您的课程基于它。
在新文件custom_validators.py
中,我们需要:
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
一旦我们有了我们的验证器,我们必须使用验证器映射 map 到 flask 应用程序( __main__.py
):
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'})
如果您还需要替换我在此示例中未使用的验证器,只需创建 ResponseValidator 的自定义子 class 并在__main__.py
中的 validator_map 字典中替换它。
连接文档: https://connexion.readthedocs.io/en/latest/request.html
请原谅我重复在https://stackoverflow.com/a/73051652/1630244上首次发布的答案
您是否尝试过 Connexion before_request
功能? 这是一个在 Connexion 验证正文之前记录标题和内容的示例:
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())
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.