From 1fde85ff68c77a0003fc4685bf2c9ae609ac5b57 Mon Sep 17 00:00:00 2001 From: Boyan Kostadinov Date: Fri, 28 Apr 2023 20:35:42 +0200 Subject: [PATCH 1/2] feat: Add operation_id_callback --- flask_openapi3/api_blueprint.py | 12 ++++++++---- flask_openapi3/openapi.py | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/flask_openapi3/api_blueprint.py b/flask_openapi3/api_blueprint.py index b33a2ae1..30ce6bff 100644 --- a/flask_openapi3/api_blueprint.py +++ b/flask_openapi3/api_blueprint.py @@ -26,6 +26,7 @@ def __init__( abp_security: Optional[List[Dict[str, List[str]]]] = None, abp_responses: Optional[Dict[str, Type[BaseModel]]] = None, doc_ui: bool = True, + operation_id_callback: Callable = get_operation_id_for_path, **kwargs: Any ) -> None: """ @@ -39,6 +40,9 @@ def __init__( abp_security: APIBlueprint security for every api abp_responses: APIBlueprint response model doc_ui: add openapi document UI(swagger and redoc). Defaults to True. + operation_id_callback: Callback function for custom operation_id generation. + Receives function_name (str), request_path (str) and request_method (str) parameters. + Defaults to `get_operation_id_for_path` from utils kwargs: Flask Blueprint kwargs """ super(APIBlueprint, self).__init__(name, import_name, **kwargs) @@ -52,6 +56,7 @@ def __init__( self.abp_security = abp_security or [] self.abp_responses = abp_responses or {} self.doc_ui = doc_ui + self.operation_id_callback: Callable = operation_id_callback def _do_decorator( self, @@ -112,10 +117,9 @@ def _do_decorator( if deprecated: operation.deprecated = True # Unique string used to identify the operation. - if operation_id: - operation.operationId = operation_id - else: - operation.operationId = get_operation_id_for_path(name=func.__name__, path=rule, method=method) + operation.operationId = operation_id or self.operation_id_callback( + name=func.__name__, path=rule, method=method + ) # store tags tags = tags + self.abp_tags if tags else self.abp_tags parse_and_store_tags(tags, self.tags, self.tag_names, operation) diff --git a/flask_openapi3/openapi.py b/flask_openapi3/openapi.py index 96f0a468..8c945bde 100644 --- a/flask_openapi3/openapi.py +++ b/flask_openapi3/openapi.py @@ -41,6 +41,7 @@ def __init__( redoc_url: str = "/redoc", rapidoc_url: str = "/rapidoc", servers: Optional[List[Server]] = None, + operation_id_callback: Callable = get_operation_id_for_path, **kwargs: Any ) -> None: """ @@ -65,6 +66,9 @@ def __init__( redoc_url: The Redoc UI documentation. Defaults to `/redoc`. rapidoc_url: The RapiDoc UI documentation. Defaults to `/rapidoc`. servers: An array of Server Objects, which provide connectivity information to a target server. + operation_id_callback: Callback function for custom operation_id generation. + Receives function_name (str), request_path (str) and request_method (str) parameters. + Defaults to `get_operation_id_for_path` from utils kwargs: Flask kwargs """ super(OpenAPI, self).__init__(import_name, **kwargs) @@ -94,6 +98,7 @@ def __init__( self.init_doc() self.doc_expansion = doc_expansion self.severs = servers + self.operation_id_callback: Callable = operation_id_callback def init_doc(self) -> None: """ @@ -254,10 +259,9 @@ def _do_decorator( if deprecated: operation.deprecated = True # Unique string used to identify the operation. - if operation_id: - operation.operationId = operation_id - else: - operation.operationId = get_operation_id_for_path(name=func.__name__, path=rule, method=method) + operation.operationId = operation_id or self.operation_id_callback( + name=func.__name__, path=rule, method=method + ) # store tags parse_and_store_tags(tags, self.tags, self.tag_names, operation) # parse parameters From 22207ef77474b6e8f56a3cd60c7602a0b8aaf6fc Mon Sep 17 00:00:00 2001 From: Boyan Kostadinov Date: Fri, 5 May 2023 19:48:59 +0200 Subject: [PATCH 2/2] Add tests for operation_id_callback --- flask_openapi3/api_blueprint.py | 2 +- flask_openapi3/openapi.py | 2 +- tests/test_api_blueprint.py | 2 ++ tests/test_restapi.py | 16 ++++++++++++++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/flask_openapi3/api_blueprint.py b/flask_openapi3/api_blueprint.py index 30ce6bff..9af991af 100644 --- a/flask_openapi3/api_blueprint.py +++ b/flask_openapi3/api_blueprint.py @@ -41,7 +41,7 @@ def __init__( abp_responses: APIBlueprint response model doc_ui: add openapi document UI(swagger and redoc). Defaults to True. operation_id_callback: Callback function for custom operation_id generation. - Receives function_name (str), request_path (str) and request_method (str) parameters. + Receives name (str), path (str) and method (str) parameters. Defaults to `get_operation_id_for_path` from utils kwargs: Flask Blueprint kwargs """ diff --git a/flask_openapi3/openapi.py b/flask_openapi3/openapi.py index 8c945bde..9cfee310 100644 --- a/flask_openapi3/openapi.py +++ b/flask_openapi3/openapi.py @@ -67,7 +67,7 @@ def __init__( rapidoc_url: The RapiDoc UI documentation. Defaults to `/rapidoc`. servers: An array of Server Objects, which provide connectivity information to a target server. operation_id_callback: Callback function for custom operation_id generation. - Receives function_name (str), request_path (str) and request_method (str) parameters. + Receives name (str), path (str) and method (str) parameters. Defaults to `get_operation_id_for_path` from utils kwargs: Flask kwargs """ diff --git a/tests/test_api_blueprint.py b/tests/test_api_blueprint.py index 140e8b62..fddacc73 100644 --- a/tests/test_api_blueprint.py +++ b/tests/test_api_blueprint.py @@ -86,6 +86,8 @@ def test_openapi(client): resp = client.get("/openapi/openapi.json") assert resp.status_code == 200 assert resp.json == app.api_doc + assert resp.json["paths"]["/api/book/{bid}"]["put"]["operationId"] == "update" + assert resp.json["paths"]["/api/book/{bid}"]["delete"]["operationId"] == "delete_book_book__int_bid__delete" def test_post(client): diff --git a/tests/test_restapi.py b/tests/test_restapi.py index 046cf3b1..6196fd02 100644 --- a/tests/test_restapi.py +++ b/tests/test_restapi.py @@ -21,8 +21,18 @@ class NotFoundResponse(BaseModel): code: int = Field(-1, description="Status Code") message: str = Field("Resource not found!", description="Exception Information") +def get_operation_id_for_path_callback(*, name: str, path: str, method: str) -> str: + return name -app = OpenAPI(__name__, info=info, security_schemes=security_schemes, responses={"404": NotFoundResponse}) + +app = OpenAPI( + __name__, + info=info, + security_schemes=security_schemes, + responses={"404": NotFoundResponse}, + operation_id_callback=get_operation_id_for_path_callback, + +) app.config["TESTING"] = True security = [{"jwt": []}] book_tag = Tag(name='book', description='Book') @@ -121,7 +131,7 @@ def update_book1(path: BookPath, body: BookBody): return {"code": 0, "message": "ok"} -@app.delete('/book/', tags=[book_tag]) +@app.delete('/book/', tags=[book_tag], operation_id="delete") def delete_book(path: BookPath): assert path.bid == 1 return {"code": 0, "message": "ok"} @@ -132,6 +142,8 @@ def test_openapi(client): print(resp.json) assert resp.status_code == 200 assert resp.json == app.api_doc + assert resp.json["paths"]["/book"]["get"]["operationId"] == "get_books" + assert resp.json["paths"]["/book/{bid}"]["delete"]["operationId"] == "delete" def test_get(client):