From 3db7169959aba4b8a35c216df396b491a430fc4a Mon Sep 17 00:00:00 2001 From: Nikita Vakula <52108696+krjakbrjak@users.noreply.github.com> Date: Fri, 26 May 2023 02:58:39 +0200 Subject: [PATCH] [python-fastapi] Added a base class for the actual implementation (#14470) It is very difficult to "merge" the changes, made by code generation, and the changes, made by developers. It would be very useful to separate the generated code and the code written by developers. In addition this would remove the necessity to track the generated code. Pyhton (since 3.6) has a hook, __init_subclasses__, that could be used to solve exactly this problem. The classes from *_base.py should be implemented in an ns package that is specified by the additional parameter ("-p fastapiImplementationPackage=example_name"). Signed-off-by: Nikita Vakula --- .../codegen/CodegenConstants.java | 2 + .../languages/PythonFastAPIServerCodegen.java | 13 +++ .../resources/python-fastapi/api.mustache | 11 ++- .../python-fastapi/base_api.mustache | 31 +++++++ .../python-fastapi/impl_argument.mustache | 1 + .../impl_argument_definition.mustache | 1 + .../python-fastapi/.openapi-generator/FILES | 3 + .../src/openapi_server/apis/pet_api.py | 25 ++++-- .../src/openapi_server/apis/pet_api_base.py | 81 +++++++++++++++++++ .../src/openapi_server/apis/store_api.py | 17 +++- .../src/openapi_server/apis/store_api_base.py | 42 ++++++++++ .../src/openapi_server/apis/user_api.py | 25 ++++-- .../src/openapi_server/apis/user_api_base.py | 76 +++++++++++++++++ 13 files changed, 307 insertions(+), 21 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/python-fastapi/base_api.mustache create mode 100644 modules/openapi-generator/src/main/resources/python-fastapi/impl_argument.mustache create mode 100644 modules/openapi-generator/src/main/resources/python-fastapi/impl_argument_definition.mustache create mode 100644 samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py create mode 100644 samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py create mode 100644 samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java index 841adbf3b6d8..43278b02e50e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConstants.java @@ -420,4 +420,6 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case, "setting this to true. You can do that by:"; + + public static final String FASTAPI_IMPLEMENTATION_PACKAGE = "fastapiImplementationPackage"; } 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 76c13906f070..b441fd3de15b 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 @@ -65,6 +65,7 @@ public void serialize(Boolean value, JsonGenerator gen, SerializerProvider seria protected String sourceFolder; + private static final String BASE_CLASS_SUFFIX = "base"; private static final String SERVER_PORT = "serverPort"; private static final String NAME = "python-fastapi"; private static final int DEFAULT_SERVER_PORT = 8080; @@ -72,6 +73,8 @@ public void serialize(Boolean value, JsonGenerator gen, SerializerProvider seria private static final String DEFAULT_SOURCE_FOLDER = "src"; private static final String DEFAULT_PACKAGE_VERSION = "1.0.0"; + private String implPackage; + @Override public CodegenType getTag() { return CodegenType.SERVER; @@ -99,8 +102,10 @@ public PythonFastAPIServerCodegen() { * are available in models, apis, and supporting files */ additionalProperties.put("serverPort", DEFAULT_SERVER_PORT); + additionalProperties.put("baseSuffix", BASE_CLASS_SUFFIX); additionalProperties.put(CodegenConstants.SOURCE_FOLDER, DEFAULT_SOURCE_FOLDER); additionalProperties.put(CodegenConstants.PACKAGE_NAME, DEFAULT_PACKAGE_NAME); + additionalProperties.put(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE, DEFAULT_PACKAGE_NAME.concat(".impl")); languageSpecificPrimitives.add("List"); languageSpecificPrimitives.add("Dict"); @@ -110,10 +115,12 @@ public PythonFastAPIServerCodegen() { outputFolder = "generated-code" + File.separator + NAME; modelTemplateFiles.put("model.mustache", ".py"); apiTemplateFiles.put("api.mustache", ".py"); + apiTemplateFiles.put("base_api.mustache", "_".concat(BASE_CLASS_SUFFIX).concat(".py")); embeddedTemplateDir = templateDir = NAME; apiPackage = "apis"; modelPackage = "models"; testPackage = "tests"; + implPackage = DEFAULT_PACKAGE_NAME.concat(".impl"); apiTestTemplateFiles().put("api_test.mustache", ".py"); cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "python package name (convention: snake_case).") @@ -124,6 +131,8 @@ public PythonFastAPIServerCodegen() { .defaultValue(String.valueOf(DEFAULT_SERVER_PORT))); cliOptions.add(new CliOption(CodegenConstants.SOURCE_FOLDER, "directory for generated python source code") .defaultValue(DEFAULT_SOURCE_FOLDER)); + cliOptions.add(new CliOption(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE, "python package name for the implementation code (convention: snake_case).") + .defaultValue(DEFAULT_PACKAGE_NAME.concat(".impl"))); } @@ -139,6 +148,10 @@ public void processOpts() { this.sourceFolder = ((String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER)); } + if (additionalProperties.containsKey(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE)) { + this.implPackage = ((String) additionalProperties.get(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE)); + } + modelPackage = packageName + "." + modelPackage; apiPackage = packageName + "." + apiPackage; diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache index 3adf3a2de340..400685f0abcb 100644 --- a/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache +++ b/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache @@ -1,6 +1,11 @@ # coding: utf-8 from typing import Dict, List # noqa: F401 +import importlib +import pkgutil + +from {{apiPackage}}.{{classFilename}}_{{baseSuffix}} import Base{{classname}} +import {{fastapiImplementationPackage}} from fastapi import ( # noqa: F401 APIRouter, @@ -24,6 +29,10 @@ from {{modelPackage}}.extra_models import TokenModel # noqa: F401 router = APIRouter() +ns_pkg = {{fastapiImplementationPackage}} +for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."): + importlib.import_module(name) + {{#operations}} {{#operation}} @@ -56,7 +65,7 @@ async def {{operationId}}( {{/hasAuthMethods}} ) -> {{returnType}}{{^returnType}}None{{/returnType}}: {{#notes}}"""{{.}}""" - ...{{/notes}}{{^notes}}...{{/notes}} + return Base{{classname}}.subclasses[0]().{{operationId}}({{#allParams}}{{>impl_argument}}{{^-last}}, {{/-last}}{{/allParams}}){{/notes}}{{^notes}}...{{/notes}} {{^-last}} diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/base_api.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/base_api.mustache new file mode 100644 index 000000000000..2e80168a328c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/python-fastapi/base_api.mustache @@ -0,0 +1,31 @@ +# coding: utf-8 + +from typing import ClassVar, Dict, List, Tuple # noqa: F401 + +{{#imports}} +{{import}} +{{/imports}} +{{#securityImports.0}}from {{packageName}}.security_api import {{#securityImports}}get_token_{{.}}{{^-last}}, {{/-last}}{{/securityImports}}{{/securityImports.0}} + +class Base{{classname}}: + subclasses: ClassVar[Tuple] = () + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + Base{{classname}}.subclasses = Base{{classname}}.subclasses + (cls,) +{{#operations}} +{{#operation}} + def {{operationId}}( + self, + {{#allParams}} + {{>impl_argument_definition}}, + {{/allParams}} + ) -> {{returnType}}{{^returnType}}None{{/returnType}}: + {{#notes}}"""{{.}}""" + ...{{/notes}}{{^notes}}...{{/notes}} +{{^-last}} + + +{{/-last}} +{{/operation}} +{{/operations}} diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/impl_argument.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/impl_argument.mustache new file mode 100644 index 000000000000..e4da6199be79 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/python-fastapi/impl_argument.mustache @@ -0,0 +1 @@ +{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/impl_argument_definition.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/impl_argument_definition.mustache new file mode 100644 index 000000000000..9bc74d63bf86 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/python-fastapi/impl_argument_definition.mustache @@ -0,0 +1 @@ +{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}}: {{>param_type}} \ No newline at end of file diff --git a/samples/server/petstore/python-fastapi/.openapi-generator/FILES b/samples/server/petstore/python-fastapi/.openapi-generator/FILES index 1d9f0c531099..db9bf70021f5 100644 --- a/samples/server/petstore/python-fastapi/.openapi-generator/FILES +++ b/samples/server/petstore/python-fastapi/.openapi-generator/FILES @@ -9,8 +9,11 @@ requirements.txt setup.cfg src/openapi_server/apis/__init__.py src/openapi_server/apis/pet_api.py +src/openapi_server/apis/pet_api_base.py src/openapi_server/apis/store_api.py +src/openapi_server/apis/store_api_base.py src/openapi_server/apis/user_api.py +src/openapi_server/apis/user_api_base.py src/openapi_server/main.py src/openapi_server/models/__init__.py src/openapi_server/models/api_response.py 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 b5fdb300c7f1..e07db9a7cc95 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 @@ -1,6 +1,11 @@ # coding: utf-8 from typing import Dict, List # noqa: F401 +import importlib +import pkgutil + +from openapi_server.apis.pet_api_base import BasePetApi +import openapi_server.impl from fastapi import ( # noqa: F401 APIRouter, @@ -23,6 +28,10 @@ router = APIRouter() +ns_pkg = openapi_server.impl +for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."): + importlib.import_module(name) + @router.post( "/pet", @@ -41,7 +50,7 @@ async def add_pet( ), ) -> Pet: """""" - ... + return BasePetApi.subclasses[0]().add_pet(pet) @router.delete( @@ -61,7 +70,7 @@ async def delete_pet( ), ) -> None: """""" - ... + return BasePetApi.subclasses[0]().delete_pet(petId, api_key) @router.get( @@ -81,7 +90,7 @@ async def find_pets_by_status( ), ) -> List[Pet]: """Multiple status values can be provided with comma separated strings""" - ... + return BasePetApi.subclasses[0]().find_pets_by_status(status) @router.get( @@ -101,7 +110,7 @@ async def find_pets_by_tags( ), ) -> List[Pet]: """Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.""" - ... + return BasePetApi.subclasses[0]().find_pets_by_tags(tags) @router.get( @@ -122,7 +131,7 @@ async def get_pet_by_id( ), ) -> Pet: """Returns a single pet""" - ... + return BasePetApi.subclasses[0]().get_pet_by_id(petId) @router.put( @@ -144,7 +153,7 @@ async def update_pet( ), ) -> Pet: """""" - ... + return BasePetApi.subclasses[0]().update_pet(pet) @router.post( @@ -165,7 +174,7 @@ async def update_pet_with_form( ), ) -> None: """""" - ... + return BasePetApi.subclasses[0]().update_pet_with_form(petId, name, status) @router.post( @@ -186,4 +195,4 @@ async def upload_file( ), ) -> ApiResponse: """""" - ... + return BasePetApi.subclasses[0]().upload_file(petId, additional_metadata, file) 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 new file mode 100644 index 000000000000..e8dfbc4f5c79 --- /dev/null +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api_base.py @@ -0,0 +1,81 @@ +# coding: utf-8 + +from typing import ClassVar, Dict, List, Tuple # noqa: F401 + +from openapi_server.models.api_response import ApiResponse +from openapi_server.models.pet import Pet +from openapi_server.security_api import get_token_petstore_auth, get_token_api_key + +class BasePetApi: + subclasses: ClassVar[Tuple] = () + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + BasePetApi.subclasses = BasePetApi.subclasses + (cls,) + def add_pet( + self, + pet: Pet, + ) -> Pet: + """""" + ... + + + def delete_pet( + self, + petId: int, + api_key: str, + ) -> None: + """""" + ... + + + def find_pets_by_status( + self, + status: List[str], + ) -> List[Pet]: + """Multiple status values can be provided with comma separated strings""" + ... + + + def find_pets_by_tags( + self, + tags: List[str], + ) -> List[Pet]: + """Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.""" + ... + + + def get_pet_by_id( + self, + petId: int, + ) -> Pet: + """Returns a single pet""" + ... + + + def update_pet( + self, + pet: Pet, + ) -> Pet: + """""" + ... + + + def update_pet_with_form( + self, + petId: int, + name: str, + status: str, + ) -> None: + """""" + ... + + + def upload_file( + self, + petId: int, + additional_metadata: str, + file: str, + ) -> ApiResponse: + """""" + ... 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 63315cfe5471..585a671bbaf4 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 @@ -1,6 +1,11 @@ # coding: utf-8 from typing import Dict, List # noqa: F401 +import importlib +import pkgutil + +from openapi_server.apis.store_api_base import BaseStoreApi +import openapi_server.impl from fastapi import ( # noqa: F401 APIRouter, @@ -22,6 +27,10 @@ router = APIRouter() +ns_pkg = openapi_server.impl +for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."): + importlib.import_module(name) + @router.delete( "/store/order/{orderId}", @@ -37,7 +46,7 @@ async def delete_order( orderId: str = Path(None, 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""" - ... + return BaseStoreApi.subclasses[0]().delete_order(orderId) @router.get( @@ -55,7 +64,7 @@ async def get_inventory( ), ) -> Dict[str, int]: """Returns a map of status codes to quantities""" - ... + return BaseStoreApi.subclasses[0]().get_inventory() @router.get( @@ -73,7 +82,7 @@ async def get_order_by_id( orderId: int = Path(None, description="ID of pet that needs to be fetched", ge=1, le=5), ) -> Order: """For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions""" - ... + return BaseStoreApi.subclasses[0]().get_order_by_id(orderId) @router.post( @@ -90,4 +99,4 @@ async def place_order( order: Order = Body(None, description="order placed for purchasing the pet"), ) -> Order: """""" - ... + return BaseStoreApi.subclasses[0]().place_order(order) 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 new file mode 100644 index 000000000000..f4436a6f07ea --- /dev/null +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api_base.py @@ -0,0 +1,42 @@ +# coding: utf-8 + +from typing import ClassVar, Dict, List, Tuple # noqa: F401 + +from openapi_server.models.order import Order +from openapi_server.security_api import get_token_api_key + +class BaseStoreApi: + subclasses: ClassVar[Tuple] = () + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + BaseStoreApi.subclasses = BaseStoreApi.subclasses + (cls,) + def delete_order( + self, + orderId: str, + ) -> None: + """For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors""" + ... + + + def get_inventory( + self, + ) -> Dict[str, int]: + """Returns a map of status codes to quantities""" + ... + + + def get_order_by_id( + self, + orderId: int, + ) -> Order: + """For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions""" + ... + + + def place_order( + self, + order: Order, + ) -> Order: + """""" + ... 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 b1073aa1f740..321732cc5e19 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 @@ -1,6 +1,11 @@ # coding: utf-8 from typing import Dict, List # noqa: F401 +import importlib +import pkgutil + +from openapi_server.apis.user_api_base import BaseUserApi +import openapi_server.impl from fastapi import ( # noqa: F401 APIRouter, @@ -22,6 +27,10 @@ router = APIRouter() +ns_pkg = openapi_server.impl +for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."): + importlib.import_module(name) + @router.post( "/user", @@ -39,7 +48,7 @@ async def create_user( ), ) -> None: """This can only be done by the logged in user.""" - ... + return BaseUserApi.subclasses[0]().create_user(user) @router.post( @@ -58,7 +67,7 @@ async def create_users_with_array_input( ), ) -> None: """""" - ... + return BaseUserApi.subclasses[0]().create_users_with_array_input(user) @router.post( @@ -77,7 +86,7 @@ async def create_users_with_list_input( ), ) -> None: """""" - ... + return BaseUserApi.subclasses[0]().create_users_with_list_input(user) @router.delete( @@ -97,7 +106,7 @@ async def delete_user( ), ) -> None: """This can only be done by the logged in user.""" - ... + return BaseUserApi.subclasses[0]().delete_user(username) @router.get( @@ -115,7 +124,7 @@ async def get_user_by_name( username: str = Path(None, description="The name that needs to be fetched. Use user1 for testing."), ) -> User: """""" - ... + return BaseUserApi.subclasses[0]().get_user_by_name(username) @router.get( @@ -133,7 +142,7 @@ async def login_user( password: str = Query(None, description="The password for login in clear text"), ) -> str: """""" - ... + return BaseUserApi.subclasses[0]().login_user(username, password) @router.get( @@ -151,7 +160,7 @@ async def logout_user( ), ) -> None: """""" - ... + return BaseUserApi.subclasses[0]().logout_user() @router.put( @@ -172,4 +181,4 @@ async def update_user( ), ) -> None: """This can only be done by the logged in user.""" - ... + return BaseUserApi.subclasses[0]().update_user(username, user) 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 new file mode 100644 index 000000000000..ee3314e7fa6b --- /dev/null +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api_base.py @@ -0,0 +1,76 @@ +# coding: utf-8 + +from typing import ClassVar, Dict, List, Tuple # noqa: F401 + +from openapi_server.models.user import User +from openapi_server.security_api import get_token_api_key + +class BaseUserApi: + subclasses: ClassVar[Tuple] = () + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + BaseUserApi.subclasses = BaseUserApi.subclasses + (cls,) + def create_user( + self, + user: User, + ) -> None: + """This can only be done by the logged in user.""" + ... + + + def create_users_with_array_input( + self, + user: List[User], + ) -> None: + """""" + ... + + + def create_users_with_list_input( + self, + user: List[User], + ) -> None: + """""" + ... + + + def delete_user( + self, + username: str, + ) -> None: + """This can only be done by the logged in user.""" + ... + + + def get_user_by_name( + self, + username: str, + ) -> User: + """""" + ... + + + def login_user( + self, + username: str, + password: str, + ) -> str: + """""" + ... + + + def logout_user( + self, + ) -> None: + """""" + ... + + + def update_user( + self, + username: str, + user: User, + ) -> None: + """This can only be done by the logged in user.""" + ...