[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 <programmistov.programmist@gmail.com>
This commit is contained in:
Nikita Vakula 2023-05-26 02:58:39 +02:00 committed by GitHub
parent b94952b3b7
commit 3db7169959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 307 additions and 21 deletions

View File

@ -420,4 +420,6 @@ public class CodegenConstants {
"setting this to true. You can do that by:<ul>" +
"<li>defining the propertyName as an enum with only one value in the schemas that are in your discriminator map</li>" +
"<li>setting additionalProperties: false in your schemas</li></ul>";
public static final String FASTAPI_IMPLEMENTATION_PACKAGE = "fastapiImplementationPackage";
}

View File

@ -65,6 +65,7 @@ public class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
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 class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
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 class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
* 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 class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
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 class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
.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 class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
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;

View File

@ -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}}

View File

@ -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}}

View File

@ -0,0 +1 @@
{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}}

View File

@ -0,0 +1 @@
{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}}: {{>param_type}}

View File

@ -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

View File

@ -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 @@ from openapi_server.security_api import get_token_petstore_auth, get_token_api_k
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)

View File

@ -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:
""""""
...

View File

@ -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 @@ from openapi_server.security_api import get_token_api_key
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 &lt; 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 &lt;&#x3D; 5 or &gt; 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)

View File

@ -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 &lt; 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 &lt;&#x3D; 5 or &gt; 10. Other values will generate exceptions"""
...
def place_order(
self,
order: Order,
) -> Order:
""""""
...

View File

@ -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 @@ from openapi_server.security_api import get_token_api_key
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)

View File

@ -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."""
...