Python flask pythonic params (#2374)

* Using connexion `pythonic_params` support while keeping OpenAPI spec file correct with reference to the original spec file.

* - Add `camelCase` query parameter which shows the incorrectnes of the conversion of the OpenAPI spec file in Python server implementation(s).

* Also use `pythonic_params=True` for the `python-aiohttp` implementation.

* - Updated Python related samples.

* The unit tests must provide the correct query parameters.

* - Updated Python related samples.
This commit is contained in:
Tom Ghyselinck
2019-03-24 03:36:26 +01:00
committed by William Cheng
parent 033ab8a6f5
commit f7943257c5
27 changed files with 190 additions and 69 deletions

View File

@@ -336,6 +336,7 @@ public class PythonAbstractConnexionServerCodegen extends DefaultCodegen impleme
if (!fixedPath.equals(pathname)) {
LOGGER.warn("Path '" + pathname + "' is not consistant with Python variable names. It will be replaced by '" + fixedPath + "'");
paths.remove(pathname);
path.addExtension("x-python-connexion-openapi-name", pathname);
paths.put(fixedPath, path);
}
}
@@ -364,6 +365,7 @@ public class PythonAbstractConnexionServerCodegen extends DefaultCodegen impleme
String pythonParameterName = this.toParamName(swaggerParameterName);
if (!swaggerParameterName.equals(pythonParameterName)) {
LOGGER.warn("Parameter name '" + swaggerParameterName + "' is not consistant with Python variable names. It will be replaced by '" + pythonParameterName + "'");
parameter.addExtension("x-python-connexion-openapi-name", swaggerParameterName);
parameter.setName(pythonParameterName);
}
if (swaggerParameterName.isEmpty()) {
@@ -474,6 +476,70 @@ public class PythonAbstractConnexionServerCodegen extends DefaultCodegen impleme
@Override
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
// XXX - Revert the original parameter (and path) names to make sure we have
// a consistent REST interface across other server/client languages:
//
// XXX - Reverts `x-python-connexion-openapi-name` back to the original (query/path) parameter name.
// We do not want to have our REST API itself being converted to pythonic params.
// This would be incompatible with other server implementations.
OpenAPI openAPI = (OpenAPI) objs.get("openAPI");
//OpenAPI openAPI = (OpenAPI) objs.remove("openAPI");
Map<String, PathItem> paths = openAPI.getPaths();
if (paths != null) {
List<String> pathnames = new ArrayList(paths.keySet());
for (String pythonPathname : pathnames) {
PathItem path = paths.get(pythonPathname);
// Fix path parameters back to original casing
Map<String, Object> pathExtensions = path.getExtensions();
if (pathExtensions != null) {
// Get and remove the (temporary) vendor extension
String openapiPathname = (String) pathExtensions.remove("x-python-connexion-openapi-name");
if (openapiPathname != null && openapiPathname != pythonPathname) {
LOGGER.info("Path '" + pythonPathname + "' is not consistant with the original OpenAPI definition. It will be replaced back by '" + openapiPathname + "'");
paths.remove(pythonPathname);
paths.put(openapiPathname, path);
}
}
Map<HttpMethod, Operation> operationMap = path.readOperationsMap();
if (operationMap != null) {
for (HttpMethod method : operationMap.keySet()) {
Operation operation = operationMap.get(method);
if (operation.getParameters() != null) {
for (Parameter parameter: operation.getParameters()) {
Map<String, Object> parameterExtensions = parameter.getExtensions();
if (parameterExtensions != null) {
// Get and remove the (temporary) vendor extension
String swaggerParameterName = (String) parameterExtensions.remove("x-python-connexion-openapi-name");
if (swaggerParameterName != null) {
String pythonParameterName = parameter.getName();
if (swaggerParameterName != pythonParameterName) {
LOGGER.info("Reverting name of parameter '" + pythonParameterName + "' of operation '" + operation.getOperationId() + "' back to '" + swaggerParameterName + "'");
parameter.setName(swaggerParameterName);
} else {
LOGGER.debug("Name of parameter '" + pythonParameterName + "' of operation '" + operation.getOperationId() + "' was unchanged.");
}
} else {
LOGGER.debug("x-python-connexion-openapi-name was not set on parameter '" + parameter.getName() + "' of operation '" + operation.getOperationId() + "'");
}
}
}
}
}
}
}
// Sort path names after variable name fix
List<String> recoveredPathnames = new ArrayList(paths.keySet());
Collections.sort(recoveredPathnames);
for (String pathname: recoveredPathnames) {
PathItem pathItem = paths.remove(pathname);
paths.put(pathname, pathItem);
}
}
//objs.put("openAPI", openAPI);
generateYAMLSpecFile(objs);
for (Map<String, Object> operations : getOperations(objs)) {

View File

@@ -8,5 +8,8 @@ def main():
}
specification_dir = os.path.join(os.path.dirname(__file__), 'openapi')
app = connexion.AioHttpApp(__name__, specification_dir=specification_dir, options=options)
app.add_api('openapi.yaml', arguments={'title': '{{appName}}'}, pass_context_arg_name='request')
app.add_api('openapi.yaml',
arguments={'title': '{{appName}}'},
pythonic_params=True,
pass_context_arg_name='request')
app.run(port={{serverPort}})

View File

@@ -11,7 +11,11 @@ def client(loop, aiohttp_client):
options = {
"swagger_ui": True
}
specification_dir = os.path.join(os.path.dirname(__file__), '..', '{{packageName}}', 'openapi')
app = connexion.AioHttpApp(__name__, specification_dir=specification_dir, options=options)
app.add_api('openapi.yaml', pass_context_arg_name='request')
specification_dir = os.path.join(os.path.dirname(__file__), '..',
'{{packageName}}',
'openapi')
app = connexion.AioHttpApp(__name__, specification_dir=specification_dir,
options=options)
app.add_api('openapi.yaml', pythonic_params=True,
pass_context_arg_name='request')
return loop.run_until_complete(aiohttp_client(app.app))

View File

@@ -29,7 +29,7 @@ async def test_{{operationId}}(client):
{{paramName}} = {{{example}}}
{{/bodyParam}}
{{#queryParams}}
{{#-first}}params = [{{/-first}}{{^-first}} {{/-first}}('{{paramName}}', {{{example}}}){{#hasMore}},{{/hasMore}}{{#-last}}]{{/-last}}
{{#-first}}params = [{{/-first}}{{^-first}} {{/-first}}('{{^vendorExtensions.x-python-connexion-openapi-name}}{{paramName}}{{/vendorExtensions.x-python-connexion-openapi-name}}{{#vendorExtensions.x-python-connexion-openapi-name}}{{vendorExtensions.x-python-connexion-openapi-name}}{{/vendorExtensions.x-python-connexion-openapi-name}}', {{{example}}}){{#hasMore}},{{/hasMore}}{{#-last}}]{{/-last}}
{{/queryParams}}
headers = { {{#vendorExtensions.x-prefered-produce}}
'Accept': '{{mediaType}}',{{/vendorExtensions.x-prefered-produce}}{{#vendorExtensions.x-prefered-consume}}

View File

@@ -12,5 +12,5 @@ class BaseTestCase(TestCase):
logging.getLogger('connexion.operation').setLevel('ERROR')
app = connexion.App(__name__, specification_dir='../openapi/')
app.app.json_encoder = JSONEncoder
app.add_api('openapi.yaml')
app.add_api('openapi.yaml', pythonic_params=True)
return app.app

View File

@@ -13,7 +13,9 @@ from {{packageName}} import encoder
def main():
app = connexion.App(__name__, specification_dir='./openapi/')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('openapi.yaml', arguments={'title': '{{appName}}'})
app.add_api('openapi.yaml',
arguments={'title': '{{appName}}'},
pythonic_params=True)
app.run(port={{serverPort}})

View File

@@ -27,7 +27,7 @@ class {{#operations}}Test{{classname}}(BaseTestCase):
{{paramName}} = {{{example}}}
{{/bodyParam}}
{{#queryParams}}
{{#-first}}query_string = [{{/-first}}{{^-first}} {{/-first}}('{{paramName}}', {{{example}}}){{#hasMore}},{{/hasMore}}{{#-last}}]{{/-last}}
{{#-first}}query_string = [{{/-first}}{{^-first}} {{/-first}}('{{^vendorExtensions.x-python-connexion-openapi-name}}{{paramName}}{{/vendorExtensions.x-python-connexion-openapi-name}}{{#vendorExtensions.x-python-connexion-openapi-name}}{{vendorExtensions.x-python-connexion-openapi-name}}{{/vendorExtensions.x-python-connexion-openapi-name}}', {{{example}}}){{#hasMore}},{{/hasMore}}{{#-last}}]{{/-last}}
{{/queryParams}}
headers = { {{#vendorExtensions.x-prefered-produce}}
'Accept': '{{mediaType}}',{{/vendorExtensions.x-prefered-produce}}{{#vendorExtensions.x-prefered-consume}}

View File

@@ -116,6 +116,13 @@ paths:
type: array
items:
type: string
- name: maxCount
in: query
description: Maximum number of items to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: successful operation

View File

@@ -8,7 +8,9 @@ from openapi_server import encoder
def main():
app = connexion.App(__name__, specification_dir='./openapi/')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('openapi.yaml', arguments={'title': 'OpenAPI Petstore'})
app.add_api('openapi.yaml',
arguments={'title': 'OpenAPI Petstore'},
pythonic_params=True)
app.run(port=8080)

View File

@@ -49,13 +49,15 @@ def find_pets_by_status(status): # noqa: E501
return 'do some magic!'
def find_pets_by_tags(tags): # noqa: E501
def find_pets_by_tags(tags, max_count=None): # noqa: E501
"""Finds Pets by tags
Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. # noqa: E501
:param tags: Tags to filter by
:type tags: List[str]
:param max_count: Maximum number of items to return
:type max_count: int
:rtype: List[Pet]
"""

View File

@@ -114,6 +114,15 @@ paths:
type: string
type: array
style: form
- description: Maximum number of items to return
explode: true
in: query
name: maxCount
required: false
schema:
format: int32
type: integer
style: form
responses:
200:
content:
@@ -138,7 +147,7 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}:
/pet/{petId}:
delete:
operationId: delete_pet
parameters:
@@ -152,7 +161,7 @@ paths:
- description: Pet id to delete
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -176,7 +185,7 @@ paths:
- description: ID of pet to return
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -208,7 +217,7 @@ paths:
- description: ID of pet that needs to be updated
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -238,14 +247,14 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}/uploadImage:
/pet/{petId}/uploadImage:
post:
operationId: upload_file
parameters:
- description: ID of pet to update
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -326,7 +335,7 @@ paths:
tags:
- store
x-openapi-router-controller: openapi_server.controllers.store_controller
/store/order/{order_id}:
/store/order/{orderId}:
delete:
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
operationId: delete_order
@@ -334,7 +343,7 @@ paths:
- description: ID of the order that needs to be deleted
explode: false
in: path
name: order_id
name: orderId
required: true
schema:
type: string
@@ -355,7 +364,7 @@ paths:
- description: ID of pet that needs to be fetched
explode: false
in: path
name: order_id
name: orderId
required: true
schema:
format: int64

View File

@@ -12,5 +12,5 @@ class BaseTestCase(TestCase):
logging.getLogger('connexion.operation').setLevel('ERROR')
app = connexion.App(__name__, specification_dir='../openapi/')
app.app.json_encoder = JSONEncoder
app.add_api('openapi.yaml')
app.add_api('openapi.yaml', pythonic_params=True)
return app.app

View File

@@ -89,7 +89,8 @@ class TestPetController(BaseTestCase):
Finds Pets by tags
"""
query_string = [('tags', 'tags_example')]
query_string = [('tags', 'tags_example'),
('maxCount', 56)]
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer special-key',

View File

@@ -8,7 +8,9 @@ from openapi_server import encoder
def main():
app = connexion.App(__name__, specification_dir='./openapi/')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('openapi.yaml', arguments={'title': 'OpenAPI Petstore'})
app.add_api('openapi.yaml',
arguments={'title': 'OpenAPI Petstore'},
pythonic_params=True)
app.run(port=8080)

View File

@@ -49,13 +49,15 @@ def find_pets_by_status(status): # noqa: E501
return 'do some magic!'
def find_pets_by_tags(tags): # noqa: E501
def find_pets_by_tags(tags, max_count=None): # noqa: E501
"""Finds Pets by tags
Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. # noqa: E501
:param tags: Tags to filter by
:type tags: List[str]
:param max_count: Maximum number of items to return
:type max_count: int
:rtype: List[Pet]
"""

View File

@@ -114,6 +114,15 @@ paths:
type: string
type: array
style: form
- description: Maximum number of items to return
explode: true
in: query
name: maxCount
required: false
schema:
format: int32
type: integer
style: form
responses:
200:
content:
@@ -138,7 +147,7 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}:
/pet/{petId}:
delete:
operationId: delete_pet
parameters:
@@ -152,7 +161,7 @@ paths:
- description: Pet id to delete
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -176,7 +185,7 @@ paths:
- description: ID of pet to return
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -208,7 +217,7 @@ paths:
- description: ID of pet that needs to be updated
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -238,14 +247,14 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}/uploadImage:
/pet/{petId}/uploadImage:
post:
operationId: upload_file
parameters:
- description: ID of pet to update
explode: false
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -326,7 +335,7 @@ paths:
tags:
- store
x-openapi-router-controller: openapi_server.controllers.store_controller
/store/order/{order_id}:
/store/order/{orderId}:
delete:
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
operationId: delete_order
@@ -334,7 +343,7 @@ paths:
- description: ID of the order that needs to be deleted
explode: false
in: path
name: order_id
name: orderId
required: true
schema:
type: string
@@ -355,7 +364,7 @@ paths:
- description: ID of pet that needs to be fetched
explode: false
in: path
name: order_id
name: orderId
required: true
schema:
format: int64

View File

@@ -12,5 +12,5 @@ class BaseTestCase(TestCase):
logging.getLogger('connexion.operation').setLevel('ERROR')
app = connexion.App(__name__, specification_dir='../openapi/')
app.app.json_encoder = JSONEncoder
app.add_api('openapi.yaml')
app.add_api('openapi.yaml', pythonic_params=True)
return app.app

View File

@@ -89,7 +89,8 @@ class TestPetController(BaseTestCase):
Finds Pets by tags
"""
query_string = [('tags', 'tags_example')]
query_string = [('tags', 'tags_example'),
('maxCount', 56)]
headers = {
'Accept': 'application/json',
'Authorization': 'Bearer special-key',

View File

@@ -8,5 +8,8 @@ def main():
}
specification_dir = os.path.join(os.path.dirname(__file__), 'openapi')
app = connexion.AioHttpApp(__name__, specification_dir=specification_dir, options=options)
app.add_api('openapi.yaml', arguments={'title': 'OpenAPI Petstore'}, pass_context_arg_name='request')
app.add_api('openapi.yaml',
arguments={'title': 'OpenAPI Petstore'},
pythonic_params=True,
pass_context_arg_name='request')
app.run(port=8080)

View File

@@ -161,7 +161,7 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}:
/pet/{petId}:
delete:
operationId: delete_pet
parameters:
@@ -171,7 +171,7 @@ paths:
type: string
- description: Pet id to delete
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -194,7 +194,7 @@ paths:
parameters:
- description: ID of pet to return
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -226,7 +226,7 @@ paths:
parameters:
- description: ID of pet that needs to be updated
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -256,13 +256,13 @@ paths:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
x-codegen-request-body-name: body
/pet/{pet_id}/uploadImage:
/pet/{petId}/uploadImage:
post:
operationId: upload_file
parameters:
- description: ID of pet to update
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -345,14 +345,14 @@ paths:
- store
x-codegen-request-body-name: body
x-openapi-router-controller: openapi_server.controllers.store_controller
/store/order/{order_id}:
/store/order/{orderId}:
delete:
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
operationId: delete_order
parameters:
- description: ID of the order that needs to be deleted
in: path
name: order_id
name: orderId
required: true
schema:
type: string
@@ -373,7 +373,7 @@ paths:
parameters:
- description: ID of pet that needs to be fetched
in: path
name: order_id
name: orderId
required: true
schema:
format: int64

View File

@@ -11,7 +11,11 @@ def client(loop, aiohttp_client):
options = {
"swagger_ui": True
}
specification_dir = os.path.join(os.path.dirname(__file__), '..', 'openapi_server', 'openapi')
app = connexion.AioHttpApp(__name__, specification_dir=specification_dir, options=options)
app.add_api('openapi.yaml', pass_context_arg_name='request')
specification_dir = os.path.join(os.path.dirname(__file__), '..',
'openapi_server',
'openapi')
app = connexion.AioHttpApp(__name__, specification_dir=specification_dir,
options=options)
app.add_api('openapi.yaml', pythonic_params=True,
pass_context_arg_name='request')
return loop.run_until_complete(aiohttp_client(app.app))

View File

@@ -8,7 +8,9 @@ from openapi_server import encoder
def main():
app = connexion.App(__name__, specification_dir='./openapi/')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('openapi.yaml', arguments={'title': 'OpenAPI Petstore'})
app.add_api('openapi.yaml',
arguments={'title': 'OpenAPI Petstore'},
pythonic_params=True)
app.run(port=8080)

View File

@@ -159,7 +159,7 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}:
/pet/{petId}:
delete:
operationId: delete_pet
parameters:
@@ -169,7 +169,7 @@ paths:
type: string
- description: Pet id to delete
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -192,7 +192,7 @@ paths:
parameters:
- description: ID of pet to return
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -224,7 +224,7 @@ paths:
parameters:
- description: ID of pet that needs to be updated
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -252,13 +252,13 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}/uploadImage:
/pet/{petId}/uploadImage:
post:
operationId: upload_file
parameters:
- description: ID of pet to update
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -338,14 +338,14 @@ paths:
- store
x-codegen-request-body-name: body
x-openapi-router-controller: openapi_server.controllers.store_controller
/store/order/{order_id}:
/store/order/{orderId}:
delete:
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
operationId: delete_order
parameters:
- description: ID of the order that needs to be deleted
in: path
name: order_id
name: orderId
required: true
schema:
type: string
@@ -366,7 +366,7 @@ paths:
parameters:
- description: ID of pet that needs to be fetched
in: path
name: order_id
name: orderId
required: true
schema:
format: int64

View File

@@ -12,5 +12,5 @@ class BaseTestCase(TestCase):
logging.getLogger('connexion.operation').setLevel('ERROR')
app = connexion.App(__name__, specification_dir='../openapi/')
app.app.json_encoder = JSONEncoder
app.add_api('openapi.yaml')
app.add_api('openapi.yaml', pythonic_params=True)
return app.app

View File

@@ -8,7 +8,9 @@ from openapi_server import encoder
def main():
app = connexion.App(__name__, specification_dir='./openapi/')
app.app.json_encoder = encoder.JSONEncoder
app.add_api('openapi.yaml', arguments={'title': 'OpenAPI Petstore'})
app.add_api('openapi.yaml',
arguments={'title': 'OpenAPI Petstore'},
pythonic_params=True)
app.run(port=8080)

View File

@@ -159,7 +159,7 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}:
/pet/{petId}:
delete:
operationId: delete_pet
parameters:
@@ -169,7 +169,7 @@ paths:
type: string
- description: Pet id to delete
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -192,7 +192,7 @@ paths:
parameters:
- description: ID of pet to return
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -224,7 +224,7 @@ paths:
parameters:
- description: ID of pet that needs to be updated
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -252,13 +252,13 @@ paths:
tags:
- pet
x-openapi-router-controller: openapi_server.controllers.pet_controller
/pet/{pet_id}/uploadImage:
/pet/{petId}/uploadImage:
post:
operationId: upload_file
parameters:
- description: ID of pet to update
in: path
name: pet_id
name: petId
required: true
schema:
format: int64
@@ -338,14 +338,14 @@ paths:
- store
x-codegen-request-body-name: body
x-openapi-router-controller: openapi_server.controllers.store_controller
/store/order/{order_id}:
/store/order/{orderId}:
delete:
description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
operationId: delete_order
parameters:
- description: ID of the order that needs to be deleted
in: path
name: order_id
name: orderId
required: true
schema:
type: string
@@ -366,7 +366,7 @@ paths:
parameters:
- description: ID of pet that needs to be fetched
in: path
name: order_id
name: orderId
required: true
schema:
format: int64

View File

@@ -12,5 +12,5 @@ class BaseTestCase(TestCase):
logging.getLogger('connexion.operation').setLevel('ERROR')
app = connexion.App(__name__, specification_dir='../openapi/')
app.app.json_encoder = JSONEncoder
app.add_api('openapi.yaml')
app.add_api('openapi.yaml', pythonic_params=True)
return app.app