[英]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.