Skip to content

[Python][FastAPI] Added support for library mode #21080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/generators/python-fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ public void serialize(Boolean value, JsonGenerator gen, SerializerProvider seria

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";
Expand All @@ -73,8 +75,7 @@ public void serialize(Boolean value, JsonGenerator gen, SerializerProvider seria
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() {
Expand All @@ -86,6 +87,18 @@ public String getHelp() {
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();

Expand Down Expand Up @@ -128,36 +141,41 @@ public PythonFastAPIServerCodegen() {
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;
Expand All @@ -166,8 +184,12 @@ public void processOpts() {
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"));
Expand All @@ -178,12 +200,15 @@ public void processOpts() {
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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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}}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}}
Expand All @@ -25,7 +30,7 @@ class Base{{classname}}:
...{{/notes}}{{^notes}}...{{/notes}}
{{^-last}}


{{/-last}}
{{/operation}}
{{/operations}}

Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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]
Expand All @@ -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)
Expand All @@ -113,34 +137,46 @@ 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.

:param credentials Credentials provided by Authorization header
: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.
Expand All @@ -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}}
Original file line number Diff line number Diff line change
@@ -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,)
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import pkgutil

from openapi_server.apis.fake_api_base import BaseFakeApi

import openapi_server.impl



from fastapi import ( # noqa: F401
APIRouter,
Body,
Expand Down Expand Up @@ -34,7 +37,6 @@
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
importlib.import_module(name)


@router.get(
"/fake/query_param_default",
responses={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@ 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")],
no_default: Annotated[Optional[StrictStr], Field(description="no default value")],
) -> None:
""""""
...

Loading