From 929f8ecdd100d020d9cf2e9313b67be692d1cf61 Mon Sep 17 00:00:00 2001 From: Zach Hornback Date: Fri, 10 Oct 2025 16:58:21 -0500 Subject: [PATCH 1/2] [python-fastapi] Fix: Skip sorting of path operations (#22163) Make use of helpful code added in 243f501aef2e6d472606e6db59ef31ee3b1338f9 to skip sorting of path parameters. In FastAPI, order matters, see link for details: https://fastapi.tiangolo.com/tutorial/path-params/?h=path#order-matters Issue: https://github.com/OpenAPITools/openapi-generator/issues/22163 --- .../codegen/languages/PythonFastAPIServerCodegen.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java index 802cd4353485..d1e8b2e881be 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java @@ -89,6 +89,11 @@ public String getHelp() { public PythonFastAPIServerCodegen() { super(); + // Skip sorting of operations to preserve the order found in the OpenAPI spec file. See + // https://fastapi.tiangolo.com/tutorial/path-params/?h=path#order-matters for details on why order matters. + LOGGER.info("Skipping sorting of path operations, order matters, let the developer decide via their specification file."); + setSkipSortingOperations(true); + modifyFeatureSet(features -> features.includeSecurityFeatures( SecurityFeature.OAuth2_AuthorizationCode, SecurityFeature.OAuth2_Password From 890d8ac9cdfcc097fbd26bd5bd712ba02d7432ab Mon Sep 17 00:00:00 2001 From: Zach Hornback Date: Fri, 17 Oct 2025 10:39:12 -0500 Subject: [PATCH 2/2] Update samples after previous commit Reading comprehension is hard. I missed the part of step 3 where samples would be updated in response to the change I had previous submitted. Via this commit, update samples to match expectations. The order of various endpoint implementations is now changed in the sample, matchcing the order in the yaml files that created them. --- .../src/openapi_server/apis/pet_api.py | 66 +++++++-------- .../src/openapi_server/apis/pet_api_base.py | 24 +++--- .../src/openapi_server/apis/store_api.py | 58 ++++++------- .../src/openapi_server/apis/store_api_base.py | 22 ++--- .../src/openapi_server/apis/user_api.py | 84 +++++++++---------- .../src/openapi_server/apis/user_api_base.py | 32 +++---- 6 files changed, 143 insertions(+), 143 deletions(-) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py index a4aa3a6e71ec..416272bcd61d 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py @@ -37,17 +37,19 @@ importlib.import_module(name) -@router.post( +@router.put( "/pet", responses={ 200: {"model": Pet, "description": "successful operation"}, - 405: {"description": "Invalid input"}, + 400: {"description": "Invalid ID supplied"}, + 404: {"description": "Pet not found"}, + 405: {"description": "Validation exception"}, }, tags=["pet"], - summary="Add a new pet to the store", + summary="Update an existing pet", response_model_by_alias=True, ) -async def add_pet( +async def update_pet( pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(None, description="Pet object that needs to be added to the store"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] @@ -56,29 +58,29 @@ async def add_pet( """""" if not BasePetApi.subclasses: raise HTTPException(status_code=500, detail="Not implemented") - return await BasePetApi.subclasses[0]().add_pet(pet) + return await BasePetApi.subclasses[0]().update_pet(pet) -@router.delete( - "/pet/{petId}", +@router.post( + "/pet", responses={ - 400: {"description": "Invalid pet value"}, + 200: {"model": Pet, "description": "successful operation"}, + 405: {"description": "Invalid input"}, }, tags=["pet"], - summary="Deletes a pet", + summary="Add a new pet to the store", response_model_by_alias=True, ) -async def delete_pet( - petId: Annotated[StrictInt, Field(description="Pet id to delete")] = Path(..., description="Pet id to delete"), - api_key: Optional[StrictStr] = Header(None, description=""), +async def add_pet( + pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(None, description="Pet object that needs to be added to the store"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), -) -> None: +) -> Pet: """""" if not BasePetApi.subclasses: raise HTTPException(status_code=500, detail="Not implemented") - return await BasePetApi.subclasses[0]().delete_pet(petId, api_key) + return await BasePetApi.subclasses[0]().add_pet(pet) @router.get( @@ -148,43 +150,41 @@ async def get_pet_by_id( return await BasePetApi.subclasses[0]().get_pet_by_id(petId) -@router.put( - "/pet", +@router.post( + "/pet/{petId}", responses={ - 200: {"model": Pet, "description": "successful operation"}, - 400: {"description": "Invalid ID supplied"}, - 404: {"description": "Pet not found"}, - 405: {"description": "Validation exception"}, + 405: {"description": "Invalid input"}, }, tags=["pet"], - summary="Update an existing pet", + summary="Updates a pet in the store with form data", response_model_by_alias=True, ) -async def update_pet( - pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(None, description="Pet object that needs to be added to the store"), +async def update_pet_with_form( + petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")] = Path(..., description="ID of pet that needs to be updated"), + name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")] = Form(None, description="Updated name of the pet"), + status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")] = Form(None, description="Updated status of the pet"), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), -) -> Pet: +) -> None: """""" if not BasePetApi.subclasses: raise HTTPException(status_code=500, detail="Not implemented") - return await BasePetApi.subclasses[0]().update_pet(pet) + return await BasePetApi.subclasses[0]().update_pet_with_form(petId, name, status) -@router.post( +@router.delete( "/pet/{petId}", responses={ - 405: {"description": "Invalid input"}, + 400: {"description": "Invalid pet value"}, }, tags=["pet"], - summary="Updates a pet in the store with form data", + summary="Deletes a pet", response_model_by_alias=True, ) -async def update_pet_with_form( - petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")] = Path(..., description="ID of pet that needs to be updated"), - name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")] = Form(None, description="Updated name of the pet"), - status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")] = Form(None, description="Updated status of the pet"), +async def delete_pet( + petId: Annotated[StrictInt, Field(description="Pet id to delete")] = Path(..., description="Pet id to delete"), + api_key: Optional[StrictStr] = Header(None, description=""), token_petstore_auth: TokenModel = Security( get_token_petstore_auth, scopes=["write:pets", "read:pets"] ), @@ -192,7 +192,7 @@ async def update_pet_with_form( """""" if not BasePetApi.subclasses: raise HTTPException(status_code=500, detail="Not implemented") - return await BasePetApi.subclasses[0]().update_pet_with_form(petId, name, status) + return await BasePetApi.subclasses[0]().delete_pet(petId, api_key) @router.post( diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py index 608762c7a8db..0d1fb77e442d 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py @@ -15,7 +15,7 @@ class BasePetApi: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) BasePetApi.subclasses = BasePetApi.subclasses + (cls,) - async def add_pet( + async def update_pet( self, pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")], ) -> Pet: @@ -23,11 +23,10 @@ async def add_pet( ... - async def delete_pet( + async def add_pet( self, - petId: Annotated[StrictInt, Field(description="Pet id to delete")], - api_key: Optional[StrictStr], - ) -> None: + pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")], + ) -> Pet: """""" ... @@ -56,19 +55,20 @@ async def get_pet_by_id( ... - async def update_pet( + async def update_pet_with_form( self, - pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")], - ) -> Pet: + petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")], + name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")], + status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")], + ) -> None: """""" ... - async def update_pet_with_form( + async def delete_pet( self, - petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")], - name: Annotated[Optional[StrictStr], Field(description="Updated name of the pet")], - status: Annotated[Optional[StrictStr], Field(description="Updated status of the pet")], + petId: Annotated[StrictInt, Field(description="Pet id to delete")], + api_key: Optional[StrictStr], ) -> None: """""" ... diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py index 21d2aceb380d..3d2744c2e028 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py @@ -36,25 +36,6 @@ importlib.import_module(name) -@router.delete( - "/store/order/{orderId}", - responses={ - 400: {"description": "Invalid ID supplied"}, - 404: {"description": "Order not found"}, - }, - tags=["store"], - summary="Delete purchase order by ID", - response_model_by_alias=True, -) -async def delete_order( - orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")] = Path(..., description="ID of the order that needs to be deleted"), -) -> None: - """For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors""" - if not BaseStoreApi.subclasses: - raise HTTPException(status_code=500, detail="Not implemented") - return await BaseStoreApi.subclasses[0]().delete_order(orderId) - - @router.get( "/store/inventory", responses={ @@ -75,6 +56,25 @@ async def get_inventory( return await BaseStoreApi.subclasses[0]().get_inventory() +@router.post( + "/store/order", + responses={ + 200: {"model": Order, "description": "successful operation"}, + 400: {"description": "Invalid Order"}, + }, + tags=["store"], + summary="Place an order for a pet", + response_model_by_alias=True, +) +async def place_order( + order: Annotated[Order, Field(description="order placed for purchasing the pet")] = Body(None, description="order placed for purchasing the pet"), +) -> Order: + """""" + if not BaseStoreApi.subclasses: + raise HTTPException(status_code=500, detail="Not implemented") + return await BaseStoreApi.subclasses[0]().place_order(order) + + @router.get( "/store/order/{orderId}", responses={ @@ -95,20 +95,20 @@ async def get_order_by_id( return await BaseStoreApi.subclasses[0]().get_order_by_id(orderId) -@router.post( - "/store/order", +@router.delete( + "/store/order/{orderId}", responses={ - 200: {"model": Order, "description": "successful operation"}, - 400: {"description": "Invalid Order"}, + 400: {"description": "Invalid ID supplied"}, + 404: {"description": "Order not found"}, }, tags=["store"], - summary="Place an order for a pet", + summary="Delete purchase order by ID", response_model_by_alias=True, ) -async def place_order( - order: Annotated[Order, Field(description="order placed for purchasing the pet")] = Body(None, description="order placed for purchasing the pet"), -) -> Order: - """""" +async def delete_order( + orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")] = Path(..., description="ID of the order that needs to be deleted"), +) -> None: + """For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors""" if not BaseStoreApi.subclasses: raise HTTPException(status_code=500, detail="Not implemented") - return await BaseStoreApi.subclasses[0]().place_order(order) + return await BaseStoreApi.subclasses[0]().delete_order(orderId) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py index 20629628e50e..84d9b639c1d3 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py @@ -14,18 +14,18 @@ class BaseStoreApi: def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) BaseStoreApi.subclasses = BaseStoreApi.subclasses + (cls,) - async def delete_order( + async def get_inventory( self, - orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")], - ) -> None: - """For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors""" + ) -> Dict[str, int]: + """Returns a map of status codes to quantities""" ... - async def get_inventory( + async def place_order( self, - ) -> Dict[str, int]: - """Returns a map of status codes to quantities""" + order: Annotated[Order, Field(description="order placed for purchasing the pet")], + ) -> Order: + """""" ... @@ -37,9 +37,9 @@ async def get_order_by_id( ... - async def place_order( + async def delete_order( self, - order: Annotated[Order, Field(description="order placed for purchasing the pet")], - ) -> Order: - """""" + orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")], + ) -> None: + """For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors""" ... diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py index efad9b7d18f3..47fd026bbc6e 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py @@ -99,48 +99,6 @@ async def create_users_with_list_input( return await BaseUserApi.subclasses[0]().create_users_with_list_input(user) -@router.delete( - "/user/{username}", - responses={ - 400: {"description": "Invalid username supplied"}, - 404: {"description": "User not found"}, - }, - tags=["user"], - summary="Delete user", - response_model_by_alias=True, -) -async def delete_user( - username: Annotated[StrictStr, Field(description="The name that needs to be deleted")] = Path(..., description="The name that needs to be deleted"), - token_api_key: TokenModel = Security( - get_token_api_key - ), -) -> None: - """This can only be done by the logged in user.""" - if not BaseUserApi.subclasses: - raise HTTPException(status_code=500, detail="Not implemented") - return await BaseUserApi.subclasses[0]().delete_user(username) - - -@router.get( - "/user/{username}", - responses={ - 200: {"model": User, "description": "successful operation"}, - 400: {"description": "Invalid username supplied"}, - 404: {"description": "User not found"}, - }, - tags=["user"], - summary="Get user by user name", - response_model_by_alias=True, -) -async def get_user_by_name( - username: Annotated[StrictStr, Field(description="The name that needs to be fetched. Use user1 for testing.")] = Path(..., description="The name that needs to be fetched. Use user1 for testing."), -) -> User: - """""" - if not BaseUserApi.subclasses: - raise HTTPException(status_code=500, detail="Not implemented") - return await BaseUserApi.subclasses[0]().get_user_by_name(username) - - @router.get( "/user/login", responses={ @@ -181,6 +139,26 @@ async def logout_user( return await BaseUserApi.subclasses[0]().logout_user() +@router.get( + "/user/{username}", + responses={ + 200: {"model": User, "description": "successful operation"}, + 400: {"description": "Invalid username supplied"}, + 404: {"description": "User not found"}, + }, + tags=["user"], + summary="Get user by user name", + response_model_by_alias=True, +) +async def get_user_by_name( + username: Annotated[StrictStr, Field(description="The name that needs to be fetched. Use user1 for testing.")] = Path(..., description="The name that needs to be fetched. Use user1 for testing."), +) -> User: + """""" + if not BaseUserApi.subclasses: + raise HTTPException(status_code=500, detail="Not implemented") + return await BaseUserApi.subclasses[0]().get_user_by_name(username) + + @router.put( "/user/{username}", responses={ @@ -202,3 +180,25 @@ async def update_user( if not BaseUserApi.subclasses: raise HTTPException(status_code=500, detail="Not implemented") return await BaseUserApi.subclasses[0]().update_user(username, user) + + +@router.delete( + "/user/{username}", + responses={ + 400: {"description": "Invalid username supplied"}, + 404: {"description": "User not found"}, + }, + tags=["user"], + summary="Delete user", + response_model_by_alias=True, +) +async def delete_user( + username: Annotated[StrictStr, Field(description="The name that needs to be deleted")] = Path(..., description="The name that needs to be deleted"), + token_api_key: TokenModel = Security( + get_token_api_key + ), +) -> None: + """This can only be done by the logged in user.""" + if not BaseUserApi.subclasses: + raise HTTPException(status_code=500, detail="Not implemented") + return await BaseUserApi.subclasses[0]().delete_user(username) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py index fb86c924a58f..752960411104 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py @@ -38,22 +38,6 @@ async def create_users_with_list_input( ... - async def delete_user( - self, - username: Annotated[StrictStr, Field(description="The name that needs to be deleted")], - ) -> None: - """This can only be done by the logged in user.""" - ... - - - async def get_user_by_name( - self, - username: Annotated[StrictStr, Field(description="The name that needs to be fetched. Use user1 for testing.")], - ) -> User: - """""" - ... - - async def login_user( self, username: Annotated[str, Field(strict=True, description="The user name for login")], @@ -70,6 +54,14 @@ async def logout_user( ... + async def get_user_by_name( + self, + username: Annotated[StrictStr, Field(description="The name that needs to be fetched. Use user1 for testing.")], + ) -> User: + """""" + ... + + async def update_user( self, username: Annotated[StrictStr, Field(description="name that need to be deleted")], @@ -77,3 +69,11 @@ async def update_user( ) -> None: """This can only be done by the logged in user.""" ... + + + async def delete_user( + self, + username: Annotated[StrictStr, Field(description="The name that needs to be deleted")], + ) -> None: + """This can only be done by the logged in user.""" + ...