Merge 0288dbcb9d02b00d4e21de28bda37c8a509b6410 into 2fb26c362ea6557c90353606ccdc3c446d6a8f35

This commit is contained in:
Aditya Mayukh Som 2025-05-12 10:35:09 +09:00 committed by GitHub
commit 40331a3deb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 163 additions and 69 deletions

View File

@ -24,6 +24,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|<dl><dt>**false**</dt><dd>No changes to the enum's are made, this is the default option.</dd><dt>**true**</dt><dd>With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.</dd></dl>|false|
|fastapiImplementationPackage|python package name for the implementation code (convention: snake_case).| |impl|
|isLibrary|whether to generate minimal python code to be published as a separate library or not| |false|
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.</dd></dl>|true|
|packageName|python package name (convention: snake_case).| |openapi_server|
|packageVersion|python package version.| |1.0.0|

View File

@ -63,7 +63,9 @@ public class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
final Logger LOGGER = LoggerFactory.getLogger(PythonFastAPIServerCodegen.class);
private String implPackage;
protected String sourceFolder;
protected boolean isLibrary = false;
private static final String BASE_CLASS_SUFFIX = "base";
private static final String SERVER_PORT = "serverPort";
@ -73,8 +75,7 @@ public class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
private static final String DEFAULT_SOURCE_FOLDER = "src";
private static final String DEFAULT_IMPL_FOLDER = "impl";
private static final String DEFAULT_PACKAGE_VERSION = "1.0.0";
private String implPackage;
private static final String IS_LIBRARY = "isLibrary";
@Override
public CodegenType getTag() {
@ -86,6 +87,18 @@ public class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
return "Generates a Python FastAPI server (beta). Models are defined with the pydantic library";
}
public void setIsLibrary(final boolean isLibrary) {
this.isLibrary = isLibrary;
}
public void setImplPackage(final String implPackage) {
this.implPackage = implPackage;
}
public void setSourceFolder(final String sourceFolder) {
this.sourceFolder = sourceFolder;
}
public PythonFastAPIServerCodegen() {
super();
@ -128,36 +141,41 @@ public class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
implPackage = DEFAULT_IMPL_FOLDER;
apiTestTemplateFiles().put("api_test.mustache", ".py");
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "python package name (convention: snake_case).")
.defaultValue(DEFAULT_PACKAGE_NAME));
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_VERSION, "python package version.")
.defaultValue(DEFAULT_PACKAGE_VERSION));
cliOptions.add(new CliOption(SERVER_PORT, "TCP port to listen to in app.run")
.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(implPackage));
// Adds the following options in the codegen CLI
addOption(CodegenConstants.PACKAGE_NAME,
"python package name (convention: snake_case).",
DEFAULT_PACKAGE_NAME);
addOption(CodegenConstants.PACKAGE_VERSION,
"python package version.",
DEFAULT_PACKAGE_VERSION);
addOption(SERVER_PORT,
"TCP port to listen to in app.run",
String.valueOf(DEFAULT_SERVER_PORT));
addOption(CodegenConstants.SOURCE_FOLDER,
"directory for generated python source code",
DEFAULT_SOURCE_FOLDER);
addOption(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE,
"python package name for the implementation code (convention: snake_case).",
implPackage);
addSwitch(IS_LIBRARY,
"whether to generate minimal python code to be published as a separate library or not",
isLibrary);
}
@Override
public void processOpts() {
super.processOpts();
if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME));
}
if (additionalProperties.containsKey(CodegenConstants.SOURCE_FOLDER)) {
this.sourceFolder = ((String) additionalProperties.get(CodegenConstants.SOURCE_FOLDER));
}
if (additionalProperties.containsKey(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE)) {
this.implPackage = ((String) additionalProperties.get(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE));
// Prefix templating value with the package name
additionalProperties.put(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE,
this.packageName + "." + this.implPackage);
}
// converts additional property values into corresponding type and passes them to setter
convertPropertyToStringAndWriteBack(CodegenConstants.PACKAGE_NAME, this::setPackageName);
convertPropertyToStringAndWriteBack(CodegenConstants.SOURCE_FOLDER, this::setSourceFolder);
convertPropertyToStringAndWriteBack(CodegenConstants.FASTAPI_IMPLEMENTATION_PACKAGE, this::setImplPackage);
convertPropertyToBooleanAndWriteBack(IS_LIBRARY, this::setIsLibrary);
modelPackage = packageName + "." + modelPackage;
apiPackage = packageName + "." + apiPackage;
@ -166,8 +184,12 @@ public class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
supportingFiles.add(new SupportingFile("openapi.mustache", "", "openapi.yaml"));
supportingFiles.add(new SupportingFile("main.mustache", String.join(File.separator, new String[]{sourceFolder, packageName.replace('.', File.separatorChar)}), "main.py"));
supportingFiles.add(new SupportingFile("docker-compose.mustache", "", "docker-compose.yaml"));
supportingFiles.add(new SupportingFile("Dockerfile.mustache", "", "Dockerfile"));
if (!this.isLibrary) {
supportingFiles.add(new SupportingFile("docker-compose.mustache", "", "docker-compose.yaml"));
supportingFiles.add(new SupportingFile("Dockerfile.mustache", "", "Dockerfile"));
}
supportingFiles.add(new SupportingFile("requirements.mustache", "", "requirements.txt"));
supportingFiles.add(new SupportingFile("security_api.mustache", String.join(File.separator, new String[]{sourceFolder, packageName.replace('.', File.separatorChar)}), "security_api.py"));
supportingFiles.add(new SupportingFile("extra_models.mustache", StringUtils.substringAfter(modelFileFolder(), outputFolder), "extra_models.py"));
@ -178,12 +200,15 @@ public class PythonFastAPIServerCodegen extends AbstractPythonCodegen {
namespacePackagePath.append(File.separator).append(tmp);
supportingFiles.add(new SupportingFile("__init__.mustache", namespacePackagePath.toString(), "__init__.py"));
}
supportingFiles.add(new SupportingFile("__init__.mustache", StringUtils.substringAfter(modelFileFolder(), outputFolder), "__init__.py"));
supportingFiles.add(new SupportingFile("__init__.mustache", StringUtils.substringAfter(apiFileFolder(), outputFolder), "__init__.py"));
supportingFiles.add(new SupportingFile("__init__.mustache", StringUtils.substringAfter(apiImplFileFolder(), outputFolder), "__init__.py"));
if (!this.isLibrary) {
supportingFiles.add(new SupportingFile("__init__.mustache", StringUtils.substringAfter(apiImplFileFolder(), outputFolder), "__init__.py"));
}
supportingFiles.add(new SupportingFile("conftest.mustache", testPackage.replace('.', File.separatorChar), "conftest.py"));
supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore"));
supportingFiles.add(new SupportingFile("pyproject_toml.mustache", "", "pyproject.toml"));
supportingFiles.add(new SupportingFile("setup_cfg.mustache", "", "setup.cfg"));

View File

@ -5,7 +5,10 @@ import importlib
import pkgutil
from {{apiPackage}}.{{classFilename}}_{{baseSuffix}} import Base{{classname}}
{{^isLibrary}}{{#fastapiImplementationPackage}}
import {{fastapiImplementationPackage}}
{{/fastapiImplementationPackage}}{{/isLibrary}}
from fastapi import ( # noqa: F401
APIRouter,
@ -30,10 +33,13 @@ from {{modelPackage}}.extra_models import TokenModel # noqa: F401
router = APIRouter()
{{^isLibrary}}
{{#fastapiImplementationPackage}}
ns_pkg = {{fastapiImplementationPackage}}
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
importlib.import_module(name)
{{/fastapiImplementationPackage}}
{{/isLibrary}}
{{#operations}}
{{#operation}}

View File

@ -1,20 +1,25 @@
# coding: utf-8
from typing import ClassVar, Dict, List, Tuple # noqa: F401
{{#isLibrary}}
from abc import ABC, abstractmethod
{{/isLibrary}}
{{#imports}}
{{import}}
{{/imports}}
{{#securityImports.0}}from {{packageName}}.security_api import {{#securityImports}}get_token_{{.}}{{^-last}}, {{/-last}}{{/securityImports}}{{/securityImports.0}}
class Base{{classname}}:
class Base{{classname}}{{#isLibrary}}(ABC){{/isLibrary}}:
subclasses: ClassVar[Tuple] = ()
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Base{{classname}}.subclasses = Base{{classname}}.subclasses + (cls,)
{{#operations}}
{{#operation}}
{{#operation}}{{#isLibrary}}
@abstractmethod{{/isLibrary}}
async def {{operationId}}(
self,
{{#allParams}}
@ -25,7 +30,7 @@ class Base{{classname}}:
...{{/notes}}{{^notes}}...{{/notes}}
{{^-last}}
{{/-last}}
{{/operation}}
{{/operations}}

View File

@ -1,6 +1,7 @@
# coding: utf-8
from typing import List
{{#isLibrary}}from abc import ABC, abstractmethod{{/isLibrary}}
from fastapi import Depends, Security # noqa: F401
from fastapi.openapi.models import OAuthFlowImplicit, OAuthFlows # noqa: F401
@ -56,10 +57,22 @@ oauth2_implicit = OAuth2(
)
)
{{/isImplicit}}
{{#isLibrary}}
{{>security_base_cls_init}}
@abstractmethod
def get_token(self, security_scopes: SecurityScopes, token: str) -> TokenModel:
...
@abstractmethod
def validate_scope(self, required_scopes: SecurityScopes, token_scopes: List[str]) -> bool:
...
{{/isLibrary}}
def get_token_{{name}}(
security_scopes: SecurityScopes, token: str = Depends(oauth2_{{#isPassword}}password{{/isPassword}}{{#isCode}}code{{/isCode}}{{#isImplicit}}implicit{{/isImplicit}})
security_scopes: SecurityScopes,
token: str = Depends(oauth2_{{#isPassword}}password{{/isPassword}}{{#isCode}}code{{/isCode}}{{#isImplicit}}implicit{{/isImplicit}})
) -> TokenModel:
"""
Validate and decode token.
@ -69,9 +82,8 @@ def get_token_{{name}}(
:return: Decoded token information or None if token is invalid
:rtype: TokenModel | None
"""
...
{{^isLibrary}}...{{/isLibrary}}
{{#isLibrary}}return Base{{name}}.subclasses[0]().get_token(security_scopes, token){{/isLibrary}}
def validate_scope_{{name}}(
required_scopes: SecurityScopes, token_scopes: List[str]
@ -86,12 +98,24 @@ def validate_scope_{{name}}(
:return: True if access to called API is allowed
:rtype: bool
"""
return False
{{^isLibrary}}return False{{/isLibrary}}
{{#isLibrary}}return Base{{name}}.subclasses[0]().validate_scope(required_scopes, token_scopes){{/isLibrary}}
{{/isOAuth}}
{{#isApiKey}}
{{#isLibrary}}
{{>security_base_cls_init}}
@abstractmethod
def get_token(
self,{{#isKeyInHeader}}
token_api_key_header: str,{{/isKeyInHeader}}{{#isKeyInCookie}}
token_api_key_cookie: str,{{/isKeyInCookie}}{{#isKeyInQuery}}
token_api_key_query: str,{{/isKeyInQuery}}
) -> TokenModel:
...
{{/isLibrary}}
def get_token_{{name}}(
{{#isKeyInHeader}}token_api_key_header: str = Security(
APIKeyHeader(name="{{keyParamName}}", auto_error=False)
@ -113,18 +137,25 @@ def get_token_{{name}}(
:return: Information attached to provided api_key or None if api_key is invalid or does not allow access to called API
:rtype: TokenModel | None
"""
...
{{^isLibrary}}...{{/isLibrary}}
{{#isLibrary}}return Base{{name}}.subclasses[0]().get_token(
{{#isKeyInHeader}}token_api_key_header,{{/isKeyInHeader}}{{#isKeyInCookie}}
token_api_key_cookie,{{/isKeyInCookie}}{{#isKeyInQuery}}
token_api_key_query,{{/isKeyInQuery}}
){{/isLibrary}}
{{/isApiKey}}
{{#isBasicBasic}}
basic_auth = HTTPBasic()
{{#isLibrary}}
{{>security_base_cls_init}}
def get_token_{{name}}(
credentials: HTTPBasicCredentials = Depends(basic_auth)
) -> TokenModel:
@abstractmethod
def get_token(self, credentials: HTTPBasicCredentials) -> TokenModel:
...
{{/isLibrary}}
def get_token_{{name}}(credentials: HTTPBasicCredentials = Depends(basic_auth)) -> TokenModel:
"""
Check and retrieve authentication information from basic auth.
@ -132,15 +163,20 @@ def get_token_{{name}}(
:type credentials: HTTPBasicCredentials
:rtype: TokenModel | None
"""
...
{{^isLibrary}}...{{/isLibrary}}
{{#isLibrary}}return Base{{name}}.subclasses[0]().get_token(credentials){{/isLibrary}}
{{/isBasicBasic}}
{{#isBasicBearer}}
bearer_auth = HTTPBearer()
{{#isLibrary}}
{{>security_base_cls_init}}
@abstractmethod
def get_token(self, credentials: HTTPAuthorizationCredentials) -> TokenModel:
...
{{/isLibrary}}
def get_token_{{name}}(credentials: HTTPAuthorizationCredentials = Depends(bearer_auth)) -> TokenModel:
"""
Check and retrieve authentication information from custom bearer token.
@ -150,8 +186,7 @@ def get_token_{{name}}(credentials: HTTPAuthorizationCredentials = Depends(beare
:return: Decoded token information or None if token is invalid
:rtype: TokenModel | None
"""
...
{{^isLibrary}}...{{/isLibrary}}
{{#isLibrary}}return Base{{name}}.subclasses[0]().get_token(credentials){{/isLibrary}}
{{/isBasicBearer}}
{{/authMethods}}

View File

@ -0,0 +1,6 @@
class Base{{name}}(ABC):
subclasses: ClassVar[Tuple] = ()
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Base{{name}}.subclasses = Base{{name}}.subclasses + (cls,)

View File

@ -5,8 +5,11 @@ import importlib
import pkgutil
from openapi_server.apis.fake_api_base import BaseFakeApi
import openapi_server.impl
from fastapi import ( # noqa: F401
APIRouter,
Body,
@ -34,7 +37,6 @@ ns_pkg = openapi_server.impl
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
importlib.import_module(name)
@router.get(
"/fake/query_param_default",
responses={

View File

@ -13,6 +13,8 @@ class BaseFakeApi:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
BaseFakeApi.subclasses = BaseFakeApi.subclasses + (cls,)
async def fake_query_param_default(
self,
has_default: Annotated[Optional[StrictStr], Field(description="has default value")],
@ -20,3 +22,4 @@ class BaseFakeApi:
) -> None:
""""""
...

View File

@ -5,8 +5,11 @@ import importlib
import pkgutil
from openapi_server.apis.pet_api_base import BasePetApi
import openapi_server.impl
from fastapi import ( # noqa: F401
APIRouter,
Body,
@ -36,7 +39,6 @@ ns_pkg = openapi_server.impl
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
importlib.import_module(name)
@router.post(
"/pet",
responses={

View File

@ -15,6 +15,8 @@ class BasePetApi:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
BasePetApi.subclasses = BasePetApi.subclasses + (cls,)
async def add_pet(
self,
pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")],
@ -82,3 +84,4 @@ class BasePetApi:
) -> ApiResponse:
""""""
...

View File

@ -5,8 +5,11 @@ import importlib
import pkgutil
from openapi_server.apis.store_api_base import BaseStoreApi
import openapi_server.impl
from fastapi import ( # noqa: F401
APIRouter,
Body,
@ -35,7 +38,6 @@ 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}",
responses={

View File

@ -14,6 +14,8 @@ class BaseStoreApi:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
BaseStoreApi.subclasses = BaseStoreApi.subclasses + (cls,)
async def delete_order(
self,
orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")],
@ -43,3 +45,4 @@ class BaseStoreApi:
) -> Order:
""""""
...

View File

@ -5,8 +5,11 @@ import importlib
import pkgutil
from openapi_server.apis.user_api_base import BaseUserApi
import openapi_server.impl
from fastapi import ( # noqa: F401
APIRouter,
Body,
@ -35,7 +38,6 @@ ns_pkg = openapi_server.impl
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
importlib.import_module(name)
@router.post(
"/user",
responses={

View File

@ -14,6 +14,8 @@ class BaseUserApi:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
BaseUserApi.subclasses = BaseUserApi.subclasses + (cls,)
async def create_user(
self,
user: Annotated[User, Field(description="Created user object")],
@ -77,3 +79,4 @@ class BaseUserApi:
) -> None:
"""This can only be done by the logged in user."""
...

View File

@ -2,6 +2,7 @@
from typing import List
from fastapi import Depends, Security # noqa: F401
from fastapi.openapi.models import OAuthFlowImplicit, OAuthFlows # noqa: F401
from fastapi.security import ( # noqa: F401
@ -29,10 +30,9 @@ oauth2_implicit = OAuth2(
)
)
)
def get_token_petstore_auth(
security_scopes: SecurityScopes, token: str = Depends(oauth2_implicit)
security_scopes: SecurityScopes,
token: str = Depends(oauth2_implicit)
) -> TokenModel:
"""
Validate and decode token.
@ -42,9 +42,8 @@ def get_token_petstore_auth(
:return: Decoded token information or None if token is invalid
:rtype: TokenModel | None
"""
...
def validate_scope_petstore_auth(
required_scopes: SecurityScopes, token_scopes: List[str]
@ -59,10 +58,8 @@ def validate_scope_petstore_auth(
:return: True if access to called API is allowed
:rtype: bool
"""
return False
def get_token_api_key(
token_api_key_header: str = Security(
APIKeyHeader(name="api_key", auto_error=False)
@ -78,6 +75,5 @@ def get_token_api_key(
:return: Information attached to provided api_key or None if api_key is invalid or does not allow access to called API
:rtype: TokenModel | None
"""
...