Skip to content

Commit

Permalink
feat(type-safe-api): faster code generation for python infrastructure (
Browse files Browse the repository at this point in the history
…#849)

Move python infrastructure projects to new codegen.

Note that I've duplicated the ejs templates for the async python infrastructure as it's a useful
starting point for when async support for python is added.

Note that this also fixes #794 by removing the `.lower()` of the full path to the mock json files.
  • Loading branch information
cogwirrel authored Oct 9, 2024
1 parent 25db3d3 commit 96946c2
Show file tree
Hide file tree
Showing 30 changed files with 506 additions and 818 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -291,13 +291,59 @@ const toJavaType = (property: parseOpenapi.Model): string => {
}
};

const toPythonPrimitive = (property: parseOpenapi.Model): string => {
if (property.type === "string" && property.format === "date") {
return "date";
} else if (property.type === "string" && property.format === "date-time") {
return "datetime"
} else if (property.type === "any") {
return "object";
} else if (property.type === "binary") {
return "bytearray";
} else if (property.type === "number") {
if ((property as any).openapiType === "integer") {
return "int";
}

switch(property.format) {
case "int32":
case "int64":
return "int";
case "float":
case "double":
default:
return "float";
}
} else if (property.type === "boolean") {
return "bool";
} else if (property.type === "string") {
return "str";
}
return property.type;
};

const toPythonType = (property: parseOpenapi.Model): string => {
switch (property.export) {
case "generic":
case "reference":
return toPythonPrimitive(property);
case "array":
return `List[${property.link ? toPythonType(property.link) : property.type}]`;
case "dictionary":
return `Dict[str, ${property.link ? toPythonType(property.link) : property.type}]`;
default:
return property.type;
}
};

/**
* Mutates the given model to add language specific types and names
*/
const mutateModelWithAdditionalTypes = (model: parseOpenapi.Model) => {
(model as any).typescriptName = model.name;
(model as any).typescriptType = toTypeScriptType(model);
(model as any).javaType = toJavaType(model);
(model as any).pythonType = toPythonType(model);
(model as any).isPrimitive = PRIMITIVE_TYPES.has(model.type);

// Trim any surrounding quotes from name
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
###TSAPI_WRITE_FILE###
{
"id": "init",
"dir": "<%= metadata.srcDir || 'src' %>",
"name": "__init__",
"ext": ".py",
"overwrite": true
}
###/TSAPI_WRITE_FILE####
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from dataclasses import fields
###TSAPI_WRITE_FILE###
{
"id": "api",
"dir": "<%= metadata.srcDir || 'src' %>",
"name": "api",
"ext": ".py",
"overwrite": true
}
###/TSAPI_WRITE_FILE###from dataclasses import fields
from aws_pdk.type_safe_api import TypeSafeRestApi, TypeSafeApiIntegration
from {{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-runtime-module-name}}{{/apis.0}}{{/apiInfo}}.api.operation_config import OperationLookup, OperationConfig
from <%- metadata.runtimeModuleName %>.api.operation_config import OperationLookup, OperationConfig
from os import path
from pathlib import Path

SPEC_PATH = path.join(str(Path(__file__).absolute().parent), "{{#apiInfo}}{{#apis.0}}{{vendorExtensions.x-relative-spec-path}}{{/apis.0}}{{/apiInfo}}")
SPEC_PATH = path.join(str(Path(__file__).absolute().parent), "<%- metadata.relativeSpecPath %>")

class Api(TypeSafeRestApi):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
###TSAPI_WRITE_FILE###
{
"dir": "<%- metadata.srcDir || 'src' %>",
"name": "functions",
"ext": ".py",
"overwrite": true
}
###/TSAPI_WRITE_FILE###from aws_cdk import Duration
from aws_cdk.aws_lambda import (
Function, Runtime, Tracing, Code
)
from aws_pdk.type_safe_api import SnapStartFunction
from os import path
from pathlib import Path

<%_ allOperations.forEach((operation) => { _%>
<%_ if (operation.vendorExtensions && operation.vendorExtensions['x-handler']) { _%>
<%_ const language = operation.vendorExtensions['x-handler'].language; _%>
<%_ const isTypeScript = language === 'typescript'; _%>
<%_ const isJava = language === 'java'; _%>
<%_ const isPython = language === 'python'; _%>
class <%- operation.operationIdPascalCase %>Function(<% if (isJava) { %>SnapStart<% } %>Function):
"""
Lambda function construct which points to the <%- language %> implementation of <%- operation.operationIdPascalCase %>
"""
def __init__(self, scope, id, **kwargs):
super().__init__(scope, id,
<%_ if (isTypeScript) { _%>
runtime=Runtime.<%- metadata['x-handlers-node-lambda-runtime-version'] %>,
<%_ } else if (isPython) { _%>
runtime=Runtime.<%- metadata['x-handlers-python-lambda-runtime-version'] %>,
<%_ } else if (isJava) { _%>
runtime=Runtime.<%- metadata['x-handlers-java-lambda-runtime-version'] %>,
<%_ } _%>
<%_ if (isTypeScript) { _%>
handler="index.handler",
<%_ } else if (isPython) { _%>
handler="<%- metadata['x-handlers-python-module'] %>.<%- operation.operationIdSnakeCase %>.handler",
<%_ } else if (isJava) { _%>
handler="<%- metadata['x-handlers-java-package'] %>.<%- operation.operationIdPascalCase %>Handler",
<%_ } _%>
code=Code.from_asset(path.join(str(Path(__file__).absolute().parent), "..",
<%_ if (isTypeScript) { _%>
"<%- metadata['x-handlers-typescript-asset-path'] %>",
"<%- operation.operationIdKebabCase %>",
<%_ } else if (isPython) { _%>
"<%- metadata['x-handlers-python-asset-path'] %>",
<%_ } else if (isJava) { _%>
"<%- metadata['x-handlers-java-asset-path'] %>",
<%_ } _%>
)),
tracing=Tracing.ACTIVE,
timeout=Duration.seconds(30),
**kwargs,
)
<%_ } _%>
<%_ }); _%>

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
###TSAPI_WRITE_FILE###
{
"id": "mock-integrations",
"dir": "<%= metadata.srcDir || 'src' %>",
"name": "mock_integrations",
"ext": ".py",
"overwrite": true
}
###/TSAPI_WRITE_FILE###import json
from aws_pdk.type_safe_api import Integrations, MockIntegration, TypeSafeApiIntegration
from <%- metadata.runtimeModuleName %>.models import *
from <%- metadata.runtimeModuleName %>.api.operation_config import OperationConfig
from os import path
from pathlib import Path

MOCK_DATA_PATH = path.join(str(Path(__file__).absolute().parent), "..", "mocks")

class MockIntegrations:
"""
Type-safe mock integrations for API operations
"""
<%_ if (metadata.enableMockIntegrations) { _%>
<%_ allOperations.forEach((operation) => { _%>
<%_ operation.responses.forEach((response) => { _%>
@staticmethod
def <%- operation.operationIdSnakeCase %>_<%- response.code %>(<% if (response.type !== 'void') { %>body: <% if (!response.isPrimitive) { %><%- response.pythonType %> = None<% } else { %>str<% } %><% } %>) -> MockIntegration:
"""
Mock integration to return a <%- response.code %> response from the <%- operation.name %> operation
<%_ if (!response.isPrimitive) { _%>
Call this with no arguments to use a generated mock response
<%_ } _%>
"""
<%_ if (response.type !== 'void') { _%>
<%_ if (!response.isPrimitive) { _%>
response_body = None
if body is None:
with open(path.join(MOCK_DATA_PATH, "{{httpMethod}}{}-{{code}}.json".format("{{path}}".replace("/", "-").lower())), "r") as f:
response_body = f.read()
else:
response_body = body.to_json()
<%_ } _%>
<%_ } _%>
return Integrations.mock(
status_code=<%- response.code %>,
<%_ if (response.type !== 'void') { _%>
<%_ if (!response.isPrimitive) { _%>
body=response_body,
<%_ } else { _%>
body=body,
<%_ } _%>
<%_ } _%>
)
<%_ }); _%>
<%_ }); _%>
@staticmethod
def mock_all(**kwargs) -> OperationConfig[TypeSafeApiIntegration]:
"""
Mock all operations for which generated JSON data can be returned.
The first available response is used for each operation. In most cases this is the successful 200 response.
Pass any additional or overridden integrations as kwargs, for example:
MockIntegrations.mock_all(
say_hello=TypeSafeApiIntegration(
integration=Integrations.lambda_(...)
)
)
"""
return OperationConfig(**{
**{
<%_ allOperations.forEach((operation) => { _%>
<%_ const firstResponse = operation.results[0] || operation.responses[0]; _%>
<%_ if (firstResponse && !firstResponse.isPrimitive) { _%>
"<%- operation.operationIdSnakeCase %>": TypeSafeApiIntegration(
integration=MockIntegrations.<%- operation.operationIdSnakeCase %>_<%- firstResponse.code %>(),
),
<%_ } _%>
<%_ }); _%>
},
**kwargs
})
<%_ } else { _%>
# No mock integrations have been generated, since mock data generation is disabled.
pass
<%_ } _%>
Loading

0 comments on commit 96946c2

Please sign in to comment.