Skip to content

Commit

Permalink
[python-fastapi] Added a base class for the actual implementation (#1…
Browse files Browse the repository at this point in the history
…4470)

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 <[email protected]>
  • Loading branch information
krjakbrjak committed May 26, 2023
1 parent b94952b commit 3db7169
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -420,4 +420,6 @@ public static enum ENUM_PROPERTY_NAMING_TYPE {camelCase, PascalCase, snake_case,
"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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,16 @@ 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;
private static final String DEFAULT_PACKAGE_NAME = "openapi_server";
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;
Expand Down Expand Up @@ -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");
Expand All @@ -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).")
Expand All @@ -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")));

}

Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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}}
Expand Down Expand Up @@ -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}}


Expand Down
Original file line number Diff line number Diff line change
@@ -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}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#isPathParam}}{{baseName}}{{/isPathParam}}{{^isPathParam}}{{paramName}}{{/isPathParam}}: {{>param_type}}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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",
Expand All @@ -41,7 +50,7 @@ async def add_pet(
),
) -> Pet:
""""""
...
return BasePetApi.subclasses[0]().add_pet(pet)


@router.delete(
Expand All @@ -61,7 +70,7 @@ async def delete_pet(
),
) -> None:
""""""
...
return BasePetApi.subclasses[0]().delete_pet(petId, api_key)


@router.get(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -144,7 +153,7 @@ async def update_pet(
),
) -> Pet:
""""""
...
return BasePetApi.subclasses[0]().update_pet(pet)


@router.post(
Expand All @@ -165,7 +174,7 @@ async def update_pet_with_form(
),
) -> None:
""""""
...
return BasePetApi.subclasses[0]().update_pet_with_form(petId, name, status)


@router.post(
Expand All @@ -186,4 +195,4 @@ async def upload_file(
),
) -> ApiResponse:
""""""
...
return BasePetApi.subclasses[0]().upload_file(petId, additional_metadata, file)
Original file line number Diff line number Diff line change
@@ -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:
""""""
...
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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}",
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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)
Loading

0 comments on commit 3db7169

Please sign in to comment.