Codegenerate StructureShape, BlobShape, application, server and Python runtime (#1403)

We are introducing code-generation for Python bindings of StructureShape, BlobShape, EnumShape, OperationShape.

This PR also add a runtime server implementation to support serving Python business logic directly and with an idiomatic experience.

Co-authored-by: david-perez <d@vidp.dev>
This commit is contained in:
Matteo Bigoi 2022-06-14 17:16:35 +01:00 committed by GitHub
parent 8911e86515
commit 8e84ee2eff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1436 additions and 99 deletions

View File

@ -16,7 +16,7 @@ val defaultRustFlags: String by project
val defaultRustDocFlags: String by project val defaultRustDocFlags: String by project
val properties = PropertyRetriever(rootProject, project) val properties = PropertyRetriever(rootProject, project)
val pluginName = "rust-server-codegen" val pluginName = "rust-server-codegen-python"
val workingDirUnderBuildDir = "smithyprojections/codegen-server-test-python/" val workingDirUnderBuildDir = "smithyprojections/codegen-server-test-python/"
configure<software.amazon.smithy.gradle.SmithyExtension> { configure<software.amazon.smithy.gradle.SmithyExtension> {
@ -39,13 +39,6 @@ dependencies {
val allCodegenTests = listOf( val allCodegenTests = listOf(
CodegenTest("com.amazonaws.simple#SimpleService", "simple"), CodegenTest("com.amazonaws.simple#SimpleService", "simple"),
CodegenTest("aws.protocoltests.restjson#RestJson", "rest_json"),
CodegenTest("aws.protocoltests.restjson.validation#RestJsonValidation", "rest_json_validation"),
CodegenTest("aws.protocoltests.json10#JsonRpc10", "json_rpc10"),
CodegenTest("aws.protocoltests.json#JsonProtocol", "json_rpc11"),
CodegenTest("aws.protocoltests.misc#MiscService", "misc"),
CodegenTest("com.amazonaws.ebs#Ebs", "ebs"),
CodegenTest("com.amazonaws.s3#AmazonS3", "s3"),
CodegenTest("com.aws.example#PokemonService", "pokemon_service_sdk") CodegenTest("com.aws.example#PokemonService", "pokemon_service_sdk")
) )

View File

@ -11,7 +11,6 @@ import software.amazon.smithy.codegen.core.ReservedWordSymbolProvider
import software.amazon.smithy.model.Model import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ServiceShape import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.rust.codegen.rustlang.RustReservedWordSymbolProvider import software.amazon.smithy.rust.codegen.rustlang.RustReservedWordSymbolProvider
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenVisitor
import software.amazon.smithy.rust.codegen.smithy.BaseSymbolMetadataProvider import software.amazon.smithy.rust.codegen.smithy.BaseSymbolMetadataProvider
import software.amazon.smithy.rust.codegen.smithy.DefaultConfig import software.amazon.smithy.rust.codegen.smithy.DefaultConfig
import software.amazon.smithy.rust.codegen.smithy.EventStreamSymbolProvider import software.amazon.smithy.rust.codegen.smithy.EventStreamSymbolProvider
@ -23,12 +22,13 @@ import software.amazon.smithy.rust.codegen.smithy.customize.CombinedCodegenDecor
import java.util.logging.Level import java.util.logging.Level
import java.util.logging.Logger import java.util.logging.Logger
/** Rust with Python bindings Codegen Plugin. /**
* Rust with Python bindings Codegen Plugin.
* This is the entrypoint for code generation, triggered by the smithy-build plugin. * This is the entrypoint for code generation, triggered by the smithy-build plugin.
* `resources/META-INF.services/software.amazon.smithy.build.SmithyBuildPlugin` refers to this class by name which * `resources/META-INF.services/software.amazon.smithy.build.SmithyBuildPlugin` refers to this class by name which
* enables the smithy-build plugin to invoke `execute` with all of the Smithy plugin context + models. * enables the smithy-build plugin to invoke `execute` with all of the Smithy plugin context + models.
*/ */
class RustCodegenServerPlugin : SmithyBuildPlugin { class PythonCodegenServerPlugin : SmithyBuildPlugin {
private val logger = Logger.getLogger(javaClass.name) private val logger = Logger.getLogger(javaClass.name)
override fun getName(): String = "rust-server-codegen-python" override fun getName(): String = "rust-server-codegen-python"
@ -43,9 +43,9 @@ class RustCodegenServerPlugin : SmithyBuildPlugin {
// - writer: The active RustWriter at the given location // - writer: The active RustWriter at the given location
val codegenDecorator = CombinedCodegenDecorator.fromClasspath(context) val codegenDecorator = CombinedCodegenDecorator.fromClasspath(context)
// ServerCodegenVisitor is the main driver of code generation that traverses the model and generates code // PythonServerCodegenVisitor is the main driver of code generation that traverses the model and generates code
logger.info("Loaded plugin to generate Rust/Python bindings for the server SSDK") logger.info("Loaded plugin to generate Rust/Python bindings for the server SSDK for projection ${context.projectionName}")
ServerCodegenVisitor(context, codegenDecorator).execute() PythonServerCodegenVisitor(context, codegenDecorator).execute()
} }
companion object { companion object {
@ -61,6 +61,9 @@ class RustCodegenServerPlugin : SmithyBuildPlugin {
symbolVisitorConfig: SymbolVisitorConfig = DefaultConfig symbolVisitorConfig: SymbolVisitorConfig = DefaultConfig
) = ) =
SymbolVisitor(model, serviceShape = serviceShape, config = symbolVisitorConfig) SymbolVisitor(model, serviceShape = serviceShape, config = symbolVisitorConfig)
// Rename a set of symbols that do not implement `PyClass` and have been wrapped in
// `aws_smithy_http_server_python::types`.
.let { PythonServerSymbolProvider(it) }
// Generate different types for EventStream shapes (e.g. transcribe streaming) // Generate different types for EventStream shapes (e.g. transcribe streaming)
.let { .let {
EventStreamSymbolProvider(symbolVisitorConfig.runtimeConfig, it, model) EventStreamSymbolProvider(symbolVisitorConfig.runtimeConfig, it, model)

View File

@ -0,0 +1,29 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy
import software.amazon.smithy.rust.codegen.rustlang.CargoDependency
import software.amazon.smithy.rust.codegen.rustlang.CratesIo
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
/**
* Object used *exclusively* in the runtime of the Python server, for separation concerns.
* Analogous to the companion object in [CargoDependency] and [ServerCargoDependency]; see its documentation for details.
* For a dependency that is used in the client, or in both the client and the server, use [CargoDependency] directly.
*/
object PythonServerCargoDependency {
val PyO3: CargoDependency = CargoDependency("pyo3", CratesIo("0.16"), features = setOf("extension-module"))
val PyO3Asyncio: CargoDependency = CargoDependency("pyo3-asyncio", CratesIo("0.16"), features = setOf("attributes", "tokio-runtime"))
val Tokio: CargoDependency = CargoDependency("tokio", CratesIo("1.0"), features = setOf("full"))
val Tracing: CargoDependency = CargoDependency("tracing", CratesIo("0.1"))
val Tower: CargoDependency = CargoDependency("tower", CratesIo("0.4"))
val TowerHttp: CargoDependency = CargoDependency("tower-http", CratesIo("0.3"), features = setOf("trace"))
val Hyper: CargoDependency = CargoDependency("hyper", CratesIo("0.14"), features = setOf("server", "http1", "http2", "tcp", "stream"))
val NumCpus: CargoDependency = CargoDependency("num_cpus", CratesIo("1.13"))
fun SmithyHttpServer(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-server")
fun SmithyHttpServerPython(runtimeConfig: RuntimeConfig) = runtimeConfig.runtimeCrate("http-server-python")
}

View File

@ -0,0 +1,131 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy
import software.amazon.smithy.build.PluginContext
import software.amazon.smithy.model.shapes.ServiceShape
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerEnumGenerator
import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerServiceGenerator
import software.amazon.smithy.rust.codegen.server.python.smithy.generators.PythonServerStructureGenerator
import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenVisitor
import software.amazon.smithy.rust.codegen.server.smithy.protocols.ServerProtocolLoader
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.DefaultPublicModules
import software.amazon.smithy.rust.codegen.smithy.RustCrate
import software.amazon.smithy.rust.codegen.smithy.SymbolVisitorConfig
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.generators.BuilderGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.CodegenTarget
import software.amazon.smithy.rust.codegen.smithy.generators.implBlock
import software.amazon.smithy.rust.codegen.util.getTrait
/**
* Entrypoint for Python server-side code generation. This class will walk the in-memory model and
* generate all the needed types by calling the accept() function on the available shapes.
*
* This class inherits from [ServerCodegenVisitor] since it uses most of the functionlities of the super class
* and have to override the symbol provider with [PythonServerSymbolProvider].
*/
class PythonServerCodegenVisitor(context: PluginContext, codegenDecorator: RustCodegenDecorator) :
ServerCodegenVisitor(context, codegenDecorator) {
init {
val symbolVisitorConfig =
SymbolVisitorConfig(
runtimeConfig = settings.runtimeConfig,
codegenConfig = settings.codegenConfig,
handleRequired = true
)
val baseModel = baselineTransform(context.model)
val service = settings.getService(baseModel)
val (protocol, generator) =
ServerProtocolLoader(
codegenDecorator.protocols(
service.id,
ServerProtocolLoader.DefaultProtocols
)
)
.protocolFor(context.model, service)
protocolGeneratorFactory = generator
model = generator.transformModel(codegenDecorator.transformModel(service, baseModel))
val baseProvider = PythonCodegenServerPlugin.baseSymbolProvider(model, service, symbolVisitorConfig)
// Override symbolProvider.
symbolProvider =
codegenDecorator.symbolProvider(generator.symbolProvider(model, baseProvider))
// Override `codegenContext` which carries the symbolProvider.
codegenContext = CodegenContext(model, symbolProvider, service, protocol, settings, target = CodegenTarget.SERVER)
// Override `rustCrate` which carries the symbolProvider.
rustCrate = RustCrate(context.fileManifest, symbolProvider, DefaultPublicModules, settings.codegenConfig)
// Override `protocolGenerator` which carries the symbolProvider.
protocolGenerator = protocolGeneratorFactory.buildProtocolGenerator(codegenContext)
}
/**
* Structure Shape Visitor
*
* For each structure shape, generate:
* - A Rust structure for the shape ([StructureGenerator]).
* - `pyo3::PyClass` trait implementation.
* - A builder for the shape.
*
* This function _does not_ generate any serializers.
*/
override fun structureShape(shape: StructureShape) {
logger.info("[python-server-codegen] Generating a structure $shape")
rustCrate.useShapeWriter(shape) { writer ->
// Use Python specific structure generator that adds the #[pyclass] attribute
// and #[pymethods] implementation.
PythonServerStructureGenerator(model, symbolProvider, writer, shape).render(CodegenTarget.SERVER)
val builderGenerator =
BuilderGenerator(codegenContext.model, codegenContext.symbolProvider, shape)
builderGenerator.render(writer)
writer.implBlock(shape, symbolProvider) {
builderGenerator.renderConvenienceMethod(this)
}
}
}
/**
* String Shape Visitor
*
* Although raw strings require no code generation, enums are actually [EnumTrait] applied to string shapes.
*/
override fun stringShape(shape: StringShape) {
logger.info("[rust-server-codegen] Generating an enum $shape")
shape.getTrait<EnumTrait>()?.also { enum ->
rustCrate.useShapeWriter(shape) { writer ->
PythonServerEnumGenerator(model, symbolProvider, writer, shape, enum, codegenContext.runtimeConfig).render()
}
}
}
/**
* Generate service-specific code for the model:
* - Serializers
* - Deserializers
* - Trait implementations
* - Protocol tests
* - Operation structures
* - Python operation handlers
*/
override fun serviceShape(shape: ServiceShape) {
logger.info("[python-server-codegen] Generating a service $shape")
PythonServerServiceGenerator(
rustCrate,
protocolGenerator,
protocolGeneratorFactory.support(),
protocolGeneratorFactory.protocol(codegenContext).httpBindingResolver,
codegenContext,
)
.render()
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
/**
* Object used *exclusively* in the runtime of the Python server, for separation concerns.
* Analogous to the companion object in [RuntimeType] and [ServerRuntimeType]; see its documentation for details.
* For a runtime type that is used in the client, or in both the client and the server, use [RuntimeType] directly.
*/
object PythonServerRuntimeType {
fun SharedSocket(runtimeConfig: RuntimeConfig) =
RuntimeType("SharedSocket", PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_http_server_python")
fun Blob(runtimeConfig: RuntimeConfig) =
RuntimeType("Blob", PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_http_server_python::types")
fun PyError(runtimeConfig: RuntimeConfig) =
RuntimeType("Error", PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig), "${runtimeConfig.crateSrcPrefix}_http_server_python")
}

View File

@ -0,0 +1,44 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.WrappingSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.rustType
/**
* Input / output / error structures can refer to complex types like the ones implemented inside
* `aws_smithy_types` (a good example is `aws_smithy_types::Blob`).
* `aws_smithy_http_server_python::types` wraps those types that do not implement directly the
* `pyo3::PyClass` trait and cannot be shared safely with Python, providing an idiomatic Python / Rust API.
*
* This symbol provider ensures types not implementing `pyo3::PyClass` are swapped with their wrappers from
* `aws_smithy_http_server_python::types`.
*/
class PythonServerSymbolProvider(private val base: RustSymbolProvider) :
WrappingSymbolProvider(base) {
private val runtimeConfig = config().runtimeConfig
/**
* Convert a shape to a Symbol.
*
* Swap the shape's symbol if its associated type does not implement `pyo3::PyClass`.
*/
override fun toSymbol(shape: Shape): Symbol {
return when (base.toSymbol(shape).rustType()) {
RuntimeType.Blob(runtimeConfig).toSymbol().rustType() -> {
PythonServerRuntimeType.Blob(runtimeConfig).toSymbol()
}
else -> {
base.toSymbol(shape)
}
}
}
}

View File

@ -0,0 +1,88 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.customizations
import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.docs
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustBlock
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerRuntimeType
import software.amazon.smithy.rust.codegen.server.smithy.customizations.AddInternalServerErrorToAllOperationsDecorator
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.customize.CombinedCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsCustomization
import software.amazon.smithy.rust.codegen.smithy.generators.LibRsSection
import software.amazon.smithy.rust.codegen.smithy.generators.ManifestCustomizations
/**
* Configure the [lib] section of `Cargo.toml`.
*
* [lib]
* name = "$CRATE_NAME"
* crate-type = ["cdylib"]
*/
class CdylibManifestDecorator : RustCodegenDecorator {
override val name: String = "CdylibDecorator"
override val order: Byte = 0
override fun crateManifestCustomizations(
codegenContext: CodegenContext
): ManifestCustomizations =
mapOf("lib" to mapOf("name" to codegenContext.settings.moduleName, "crate-type" to listOf("cdylib")))
}
/**
* Add `pub use aws_smithy_http_server_python::types::$TYPE` to lib.rs.
*/
class PubUsePythonTypes(private val runtimeConfig: RuntimeConfig) : LibRsCustomization() {
override fun section(section: LibRsSection): Writable {
return when (section) {
is LibRsSection.Body -> writable {
docs("Re-exported Python types from supporting crates.")
rustBlock("pub mod python_types") {
rust("pub use #T;", PythonServerRuntimeType.Blob(runtimeConfig).toSymbol())
}
}
else -> emptySection
}
}
}
/**
* Decorator applying the customization from [PubUsePythonTypes] class.
*/
class PubUsePythonTypesDecorator : RustCodegenDecorator {
override val name: String = "PubUsePythonTypesDecorator"
override val order: Byte = 0
override fun libRsCustomizations(
codegenContext: CodegenContext,
baseCustomizations: List<LibRsCustomization>
): List<LibRsCustomization> {
return baseCustomizations + PubUsePythonTypes(codegenContext.runtimeConfig)
}
}
val DECORATORS = listOf(
/**
* Add the [InternalServerError] error to all operations.
* This is done because the Python interpreter can raise exceptions during execution
*/
AddInternalServerErrorToAllOperationsDecorator(),
// Add the [lib] section to Cargo.toml to configure the generation of the shared library:
CdylibManifestDecorator(),
// Add `pub use` of `aws_smithy_http_server_python::types`.
PubUsePythonTypesDecorator()
)
// Combined codegen decorator for Python services.
class PythonServerCodegenDecorator : CombinedCodegenDecorator(DECORATORS) {
override val name: String = "PythonServerCodegenDecorator"
override val order: Byte = -1
}

View File

@ -0,0 +1,151 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.rustBlockTemplate
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency
import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.util.toSnakeCase
/**
* Generates a Python compatible application and server that can be configured from Python.
*
* Example:
* from pool import DatabasePool
* from my_library import App, OperationInput, OperationOutput
* @dataclass
* class Context:
* db = DatabasePool()
*
* app = App()
* app.context(Context())
*
* @app.operation
* def operation(input: OperationInput, ctx: State) -> OperationOutput:
* description = await ctx.db.get_description(input.name)
* return OperationOutput(description)
*
* app.run()
*
* The application holds a mapping between operation names (lowercase, snakecase),
* the context as defined in Python and some task local with the Python event loop
* for the current process.
*
* The application exposes several methods to Python:
* * `App()`: constructor to create an instance of `App`.
* * `run()`: run the application on a number of workers.
* * `context()`: register the context object that is passed to the Python handlers.
* * One register method per operation that can be used as decorator. For example if
* the model has one operation called `RegisterServer`, it will codegenerate a method
* of `App` called `register_service()` that can be used to decorate the Python implementation
* of this operation.
*
* This class also renders the implementation of the `aws_smity_http_server_python::PyServer` trait,
* that abstracts the processes / event loops / workers lifecycles.
*/
class PythonApplicationGenerator(
codegenContext: CodegenContext,
private val operations: List<OperationShape>,
) {
private val symbolProvider = codegenContext.symbolProvider
private val runtimeConfig = codegenContext.runtimeConfig
private val codegenScope =
arrayOf(
"SmithyPython" to PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig).asType(),
"SmithyServer" to ServerCargoDependency.SmithyHttpServer(runtimeConfig).asType(),
"pyo3" to PythonServerCargoDependency.PyO3.asType(),
"pyo3_asyncio" to PythonServerCargoDependency.PyO3Asyncio.asType(),
"tokio" to PythonServerCargoDependency.Tokio.asType(),
"tracing" to PythonServerCargoDependency.Tracing.asType(),
"tower" to PythonServerCargoDependency.Tower.asType(),
"tower_http" to PythonServerCargoDependency.TowerHttp.asType(),
"num_cpus" to PythonServerCargoDependency.NumCpus.asType(),
"hyper" to PythonServerCargoDependency.Hyper.asType()
)
fun render(writer: RustWriter) {
writer.rustTemplate(
"""
##[#{pyo3}::pyclass(extends = #{SmithyPython}::PyApp)]
##[derive(Debug, Clone)]
pub struct App { }
""",
*codegenScope
)
renderPyMethods(writer)
}
private fun renderPyMethods(writer: RustWriter) {
writer.rustBlockTemplate(
"""
##[#{pyo3}::pymethods]
impl App
""",
*codegenScope
) {
rustBlockTemplate(
"""
/// Override the `router()` function of #{SmithyPython}::PyApp allowing to dynamically
/// codegenerate the routes.
pub fn router(self_: #{pyo3}::PyRef<'_, Self>) -> Option<#{pyo3}::PyObject>
""",
*codegenScope
) {
rustTemplate(
"""
let router = crate::operation_registry::OperationRegistryBuilder::default();
let sup = self_.as_ref();
""",
*codegenScope
)
for (operation in operations) {
val operationName = symbolProvider.toSymbol(operation).name
val name = operationName.toSnakeCase()
rustTemplate(
"""
let locals = sup.locals.clone();
let handler = sup.handlers.get("$name").expect("Python handler for `{$name}` not found").clone();
let router = router.$name(move |input, state| {
#{pyo3_asyncio}::tokio::scope(locals.clone(), crate::operation_handler::$name(input, state, handler))
});
""",
*codegenScope
)
}
rustTemplate(
"""
let router: #{SmithyServer}::Router = router.build().expect("Unable to build operation registry").into();
use #{pyo3}::IntoPy;
Some(#{SmithyPython}::PyRouter(router).into_py(self_.py()))
""",
*codegenScope
)
}
operations.map { operation ->
val operationName = symbolProvider.toSymbol(operation).name
val name = operationName.toSnakeCase()
rustTemplate(
"""
/// Method to register `$name` Python implementation inside the handlers map.
/// It can be used as a function decorator in Python.
pub fn $name(self_: #{pyo3}::PyRefMut<'_, Self>, func: #{pyo3}::PyObject) -> #{pyo3}::PyResult<()> {
let mut sup = self_.into_super();
#{pyo3}::Python::with_gil(|py| sup.register_operation(py, "$name", func))
}
""",
*codegenScope
)
}
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.traits.ErrorTrait
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency
import software.amazon.smithy.rust.codegen.util.hasTrait
/**
* This module contains utilities to render PyO3 attributes.
*
* TODO(https://github.com/awslabs/smithy-rs/issues/1465): Switch to `Attribute.Custom` and get rid of this class.
*/
private val codegenScope = arrayOf(
"pyo3" to PythonServerCargoDependency.PyO3.asType(),
)
// Renders #[pyo3::pyclass] attribute, inheriting from `Exception` if the shape has the `ErrorTrait` attached.
fun RustWriter.renderPyClass(shape: Shape) {
if (shape.hasTrait<ErrorTrait>()) {
rustTemplate("##[#{pyo3}::pyclass(extends = #{pyo3}::exceptions::PyException)]", *codegenScope)
} else {
rustTemplate("##[#{pyo3}::pyclass]", *codegenScope)
}
}
// Renders #[pyo3::pymethods] attribute.
fun RustWriter.renderPyMethods() {
rustTemplate("##[#{pyo3}::pymethods]", *codegenScope)
}
// Renders #[pyo3(get, set)] attribute.
fun RustWriter.renderPyGetterSetter() {
rustTemplate("##[#{pyo3}(get, set)]", *codegenScope)
}

View File

@ -0,0 +1,47 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency
import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerCombinedErrorGenerator
import software.amazon.smithy.rust.codegen.smithy.RuntimeType
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.generators.error.errorSymbol
/**
* Generates a unified error enum for [operation]. It depends on [ServerCombinedErrorGenerator]
* to generate the errors from the model and adds the Rust implementation `From<pyo3::PyErr>`.
*/
class PythonServerCombinedErrorGenerator(
model: Model,
private val symbolProvider: RustSymbolProvider,
private val operation: OperationShape
) : ServerCombinedErrorGenerator(model, symbolProvider, operation) {
override fun render(writer: RustWriter) {
super.render(writer)
writer.rustTemplate(
"""
impl #{From}<#{pyo3}::PyErr> for #{Error} {
fn from(variant: #{pyo3}::PyErr) -> #{Error} {
crate::error::InternalServerError {
message: variant.to_string()
}.into()
}
}
""",
"pyo3" to PythonServerCargoDependency.PyO3.asType(),
"Error" to operation.errorSymbol(symbolProvider),
"From" to RuntimeType.From
)
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.StringShape
import software.amazon.smithy.model.traits.EnumTrait
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerEnumGenerator
import software.amazon.smithy.rust.codegen.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
/**
* To share enums defined in Rust with Python, `pyo3` provides the `PyClass` trait.
* This class generates enums definitions, implements the `PyClass` trait and adds
* some utility functions like `__str__()` and `__repr__()`.
*/
class PythonServerEnumGenerator(
model: Model,
symbolProvider: RustSymbolProvider,
private val writer: RustWriter,
private val shape: StringShape,
enumTrait: EnumTrait,
runtimeConfig: RuntimeConfig,
) : ServerEnumGenerator(model, symbolProvider, writer, shape, enumTrait, runtimeConfig) {
override fun render() {
writer.renderPyClass(shape)
super.render()
renderPyO3Methods()
}
override fun renderFromForStr() {
writer.renderPyClass(shape)
super.renderFromForStr()
}
private fun renderPyO3Methods() {
writer.renderPyMethods()
writer.rust(
"""
impl $enumName {
fn __repr__(&self) -> String {
self.as_str().to_owned()
}
fn __str__(&self) -> String {
self.as_str().to_owned()
}
}
"""
)
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.asType
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency
import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency
import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerOperationHandlerGenerator
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.util.toSnakeCase
/**
* The Rust code responsible to run the Python business logic on the Python interpreter
* is implemented in this class, which inherits from [ServerOperationHandlerGenerator].
*
* We codegenerate all operations handlers (steps usually left to the developer in a pure
* Rust application), which are built into a `Router` by [PythonApplicationGenerator].
*
* To call a Python function from Rust, anything dealing with Python runs inside an async
* block that allows to catch stacktraces. The handler function is extracted from `PyHandler`
* and called with the necessary arguments inside a blocking Tokio task.
* At the end the block is awaited and errors are collected and reported.
*
* To call a Python coroutine, the same happens, but scheduled in a `tokio::Future`.
*/
class PythonServerOperationHandlerGenerator(
codegenContext: CodegenContext,
private val operations: List<OperationShape>,
) : ServerOperationHandlerGenerator(codegenContext, operations) {
private val symbolProvider = codegenContext.symbolProvider
private val runtimeConfig = codegenContext.runtimeConfig
private val codegenScope =
arrayOf(
"SmithyPython" to PythonServerCargoDependency.SmithyHttpServerPython(runtimeConfig).asType(),
"SmithyServer" to ServerCargoDependency.SmithyHttpServer(runtimeConfig).asType(),
"pyo3" to PythonServerCargoDependency.PyO3.asType(),
"pyo3asyncio" to PythonServerCargoDependency.PyO3Asyncio.asType(),
"tokio" to PythonServerCargoDependency.Tokio.asType(),
"tracing" to PythonServerCargoDependency.Tracing.asType()
)
override fun render(writer: RustWriter) {
super.render(writer)
renderPythonOperationHandlerImpl(writer)
}
private fun renderPythonOperationHandlerImpl(writer: RustWriter) {
for (operation in operations) {
val operationName = symbolProvider.toSymbol(operation).name
val input = "crate::input::${operationName}Input"
val output = "crate::output::${operationName}Output"
val error = "crate::error::${operationName}Error"
val fnName = operationName.toSnakeCase()
writer.rustTemplate(
"""
/// Python handler for operation `$operationName`.
pub async fn $fnName(
input: $input,
state: #{SmithyServer}::Extension<#{SmithyPython}::PyState>,
handler: std::sync::Arc<#{SmithyPython}::PyHandler>,
) -> std::result::Result<$output, $error> {
// Async block used to run the handler and catch any Python error.
let result = async {
let handler = handler.clone();
if handler.is_coroutine {
#{pycoroutine:W}
} else {
#{pyfunction:W}
}
};
#{pyerror:W}
}
""",
*codegenScope,
"pycoroutine" to renderPyCoroutine(fnName, output),
"pyfunction" to renderPyFunction(fnName, output),
"pyerror" to renderPyError(),
)
}
}
private fun renderPyFunction(name: String, output: String): Writable =
writable {
rustTemplate(
"""
#{tracing}::debug!("Executing Python handler function `$name()`");
#{tokio}::task::spawn_blocking(move || {
#{pyo3}::Python::with_gil(|py| {
let pyhandler: &#{pyo3}::types::PyFunction = handler.extract(py)?;
let output = if handler.args == 1 {
pyhandler.call1((input,))?
} else {
pyhandler.call1((input, &*state.0.context))?
};
output.extract::<$output>()
})
})
.await.map_err(|e| #{pyo3}::exceptions::PyRuntimeError::new_err(e.to_string()))?
""",
*codegenScope
)
}
private fun renderPyCoroutine(name: String, output: String): Writable =
writable {
rustTemplate(
"""
#{tracing}::debug!("Executing Python handler coroutine `$name()`");
let result = #{pyo3}::Python::with_gil(|py| {
let pyhandler: &#{pyo3}::types::PyFunction = handler.extract(py)?;
let coroutine = if handler.args == 1 {
pyhandler.call1((input,))?
} else {
pyhandler.call1((input, &*state.0.context))?
};
#{pyo3asyncio}::tokio::into_future(coroutine)
})?;
result.await.map(|r| #{pyo3}::Python::with_gil(|py| r.extract::<$output>(py)))?
""",
*codegenScope
)
}
private fun renderPyError(): Writable =
writable {
rustTemplate(
"""
// Catch and record a Python traceback.
result.await.map_err(|e| {
#{pyo3}::Python::with_gil(|py| {
let traceback = match e.traceback(py) {
Some(t) => t.format().unwrap_or_else(|e| e.to_string()),
None => "Unknown traceback".to_string()
};
#{tracing}::error!("{}\n{}", e, traceback);
});
e.into()
})
""",
*codegenScope
)
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.rustlang.RustModule
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.server.smithy.generators.ServerServiceGenerator
import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.RustCrate
import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolGenerator
import software.amazon.smithy.rust.codegen.smithy.generators.protocol.ProtocolSupport
import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver
/**
* PythonServerServiceGenerator
*
* Service generator is the main codegeneration entry point for Smithy services. Individual structures and unions are
* generated in codegen visitor, but this class handles all protocol-specific code generation (i.e. operations).
*/
class PythonServerServiceGenerator(
private val rustCrate: RustCrate,
protocolGenerator: ProtocolGenerator,
protocolSupport: ProtocolSupport,
httpBindingResolver: HttpBindingResolver,
private val context: CodegenContext,
) : ServerServiceGenerator(rustCrate, protocolGenerator, protocolSupport, httpBindingResolver, context) {
override fun renderCombinedErrors(writer: RustWriter, operation: OperationShape) {
PythonServerCombinedErrorGenerator(context.model, context.symbolProvider, operation).render(writer)
}
override fun renderOperationHandler(writer: RustWriter, operations: List<OperationShape>) {
PythonServerOperationHandlerGenerator(context, operations).render(writer)
}
override fun renderExtras(operations: List<OperationShape>) {
rustCrate.withModule(RustModule.public("python_server_application", "Python server and application implementation.")) { writer ->
PythonApplicationGenerator(context, operations)
.render(writer)
}
}
}

View File

@ -0,0 +1,85 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.MemberShape
import software.amazon.smithy.model.shapes.StructureShape
import software.amazon.smithy.model.traits.ErrorTrait
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.rustlang.Writable
import software.amazon.smithy.rust.codegen.rustlang.render
import software.amazon.smithy.rust.codegen.rustlang.rust
import software.amazon.smithy.rust.codegen.rustlang.rustTemplate
import software.amazon.smithy.rust.codegen.rustlang.writable
import software.amazon.smithy.rust.codegen.smithy.RustSymbolProvider
import software.amazon.smithy.rust.codegen.smithy.generators.StructureGenerator
import software.amazon.smithy.rust.codegen.smithy.rustType
import software.amazon.smithy.rust.codegen.util.hasTrait
/**
* To share structures defined in Rust with Python, `pyo3` provides the `PyClass` trait.
* This class generates input / output / error structures definitions and implements the
* `PyClass` trait.
*/
open class PythonServerStructureGenerator(
model: Model,
private val symbolProvider: RustSymbolProvider,
private val writer: RustWriter,
private val shape: StructureShape
) : StructureGenerator(model, symbolProvider, writer, shape) {
override fun renderStructure() {
writer.renderPyClass(shape)
super.renderStructure()
renderPyO3Methods()
}
override fun renderStructureMember(writer: RustWriter, member: MemberShape, memberName: String, memberSymbol: Symbol) {
writer.renderPyGetterSetter()
super.renderStructureMember(writer, member, memberName, memberSymbol)
}
private fun renderPyO3Methods() {
if (shape.hasTrait<ErrorTrait>() || accessorMembers.isNotEmpty()) {
writer.renderPyMethods()
writer.rustTemplate(
"""
impl $name {
##[new]
pub fn new(#{bodysignature:W}) -> Self {
Self {
#{bodymembers:W}
}
}
fn __repr__(&self) -> String {
format!("{self:?}")
}
fn __str__(&self) -> String {
format!("{self:?}")
}
}
""",
"bodysignature" to renderStructSignatureMembers(),
"bodymembers" to renderStructBodyMembers()
)
}
}
private fun renderStructSignatureMembers(): Writable =
writable {
forEachMember(members) { _, memberName, memberSymbol ->
val memberType = memberSymbol.rustType()
rust("$memberName: ${memberType.render()},")
}
}
private fun renderStructBodyMembers(): Writable =
writable {
forEachMember(members) { _, memberName, _ -> rust("$memberName,") }
}
}

View File

@ -2,4 +2,4 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
# #
software.amazon.smithy.rust.codegen.server.python.smithy.RustCodegenServerPlugin software.amazon.smithy.rust.codegen.server.python.smithy.PythonCodegenServerPlugin

View File

@ -0,0 +1,6 @@
#
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
#
software.amazon.smithy.rust.codegen.server.python.smithy.customizations.PythonServerCodegenDecorator

View File

@ -48,19 +48,20 @@ import java.util.logging.Logger
* Entrypoint for server-side code generation. This class will walk the in-memory model and * Entrypoint for server-side code generation. This class will walk the in-memory model and
* generate all the needed types by calling the accept() function on the available shapes. * generate all the needed types by calling the accept() function on the available shapes.
*/ */
class ServerCodegenVisitor(context: PluginContext, private val codegenDecorator: RustCodegenDecorator) : open class ServerCodegenVisitor(context: PluginContext, private val codegenDecorator: RustCodegenDecorator) :
ShapeVisitor.Default<Unit>() { ShapeVisitor.Default<Unit>() {
private val logger = Logger.getLogger(javaClass.name)
private val settings = ServerRustSettings.from(context.model, context.settings)
private val symbolProvider: RustSymbolProvider
private val rustCrate: RustCrate
private val fileManifest = context.fileManifest private val fileManifest = context.fileManifest
private val model: Model
private val codegenContext: CodegenContext protected val logger = Logger.getLogger(javaClass.name)
private val protocolGeneratorFactory: ProtocolGeneratorFactory<ProtocolGenerator> protected val settings = ServerRustSettings.from(context.model, context.settings)
private val protocolGenerator: ProtocolGenerator
var model: Model
var protocolGeneratorFactory: ProtocolGeneratorFactory<ProtocolGenerator>
var protocolGenerator: ProtocolGenerator
var codegenContext: CodegenContext
var symbolProvider: RustSymbolProvider
var rustCrate: RustCrate
init { init {
val symbolVisitorConfig = val symbolVisitorConfig =
@ -95,7 +96,7 @@ class ServerCodegenVisitor(context: PluginContext, private val codegenDecorator:
* Base model transformation applied to all services. * Base model transformation applied to all services.
* See below for details. * See below for details.
*/ */
private fun baselineTransform(model: Model) = protected fun baselineTransform(model: Model) =
model model
// Add errors attached at the service level to the models // Add errors attached at the service level to the models
.let { ModelTransformer.create().copyServiceErrorsToOperations(it, settings.getService(it)) } .let { ModelTransformer.create().copyServiceErrorsToOperations(it, settings.getService(it)) }
@ -175,7 +176,7 @@ class ServerCodegenVisitor(context: PluginContext, private val codegenDecorator:
} }
/** /**
* String Shape Visitor * Enum Shape Visitor
* *
* Although raw strings require no code generation, enums are actually [EnumTrait] applied to string shapes. * Although raw strings require no code generation, enums are actually [EnumTrait] applied to string shapes.
*/ */

View File

@ -18,23 +18,23 @@ import software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator
/** /**
* Add at least one error to all operations in the model. * Add at least one error to all operations in the model.
* *
* When this decorator is applied, even operations that do not have a Smithy error attatched, * When this decorator is applied, even operations that do not have a Smithy error attached,
* will return `Result<OperationOutput, OperationError>`. * will return `Result<OperationOutput, OperationError>`.
* *
* To enable this decorator write its class name to a resource file like this: * To enable this decorator write its class name to a resource file like this:
* ``` * ```
* C="software.amazon.smithy.rust.codegen.server.smithy.customizations.AddInternalServerErrorToInfallibleOpsDecorator" * C="software.amazon.smithy.rust.codegen.server.smithy.customizations.AddInternalServerErrorToInfallibleOperationsDecorator"
* F="software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator" * F="software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator"
* D="codegen-server/src/main/resources/META-INF/services" * D="codegen-server/src/main/resources/META-INF/services"
* mkdir -p "$D" && echo "$C" > "$D/$F" * mkdir -p "$D" && echo "$C" > "$D/$F"
* ``` * ```
*/ */
class AddInternalServerErrorToInfallibleOpsDecorator : RustCodegenDecorator { class AddInternalServerErrorToInfallibleOperationsDecorator : RustCodegenDecorator {
override val name: String = "AddInternalServerErrorToInfallibleOps" override val name: String = "AddInternalServerErrorToInfallibleOperations"
override val order: Byte = 0 override val order: Byte = 0
override fun transformModel(service: ServiceShape, model: Model): Model = override fun transformModel(service: ServiceShape, model: Model): Model =
addErrorShapeToModelOps(service, model, { shape -> shape.errors.isEmpty() }) addErrorShapeToModelOperations(service, model, { shape -> shape.errors.isEmpty() })
} }
/** /**
@ -44,26 +44,26 @@ class AddInternalServerErrorToInfallibleOpsDecorator : RustCodegenDecorator {
* and there is no native mapping of these actual errors to the API errors, servers can generate * and there is no native mapping of these actual errors to the API errors, servers can generate
* the code with this decorator to add an internal error shape on-the-fly to all the operations. * the code with this decorator to add an internal error shape on-the-fly to all the operations.
* *
* When this decorator is applied, even operations that do not have a Smithy error attatched, * When this decorator is applied, even operations that do not have a Smithy error attached,
* will return `Result<OperationOutput, OperationError>`. * will return `Result<OperationOutput, OperationError>`.
* *
* To enable this decorator write its class name to a resource file like this: * To enable this decorator write its class name to a resource file like this:
* ``` * ```
* C="software.amazon.smithy.rust.codegen.server.smithy.customizations.AddInternalServerErrorToAllOpsDecorator" * C="software.amazon.smithy.rust.codegen.server.smithy.customizations.AddInternalServerErrorToAllOperationsDecorator"
* F="software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator" * F="software.amazon.smithy.rust.codegen.smithy.customize.RustCodegenDecorator"
* D="codegen-server/src/main/resources/META-INF/services" * D="codegen-server/src/main/resources/META-INF/services"
* mkdir -p "$D" && echo "$C" > "$D/$F" * mkdir -p "$D" && echo "$C" > "$D/$F"
* ``` * ```
*/ */
class AddInternalServerErrorToAllOpsDecorator : RustCodegenDecorator { class AddInternalServerErrorToAllOperationsDecorator : RustCodegenDecorator {
override val name: String = "AddInternalServerErrorToAllOps" override val name: String = "AddInternalServerErrorToAllOperations"
override val order: Byte = 0 override val order: Byte = 0
override fun transformModel(service: ServiceShape, model: Model): Model = override fun transformModel(service: ServiceShape, model: Model): Model =
addErrorShapeToModelOps(service, model, { _ -> true }) addErrorShapeToModelOperations(service, model, { _ -> true })
} }
fun addErrorShapeToModelOps(service: ServiceShape, model: Model, opSelector: (OperationShape) -> Boolean): Model { fun addErrorShapeToModelOperations(service: ServiceShape, model: Model, opSelector: (OperationShape) -> Boolean): Model {
val errorShape = internalServerError(service.id.getNamespace()) val errorShape = internalServerError(service.id.getNamespace())
val modelShapes = model.toBuilder().addShapes(listOf(errorShape)).build() val modelShapes = model.toBuilder().addShapes(listOf(errorShape)).build()
return ModelTransformer.create().mapShapes(modelShapes) { shape -> return ModelTransformer.create().mapShapes(modelShapes) { shape ->

View File

@ -25,14 +25,14 @@ import software.amazon.smithy.rust.codegen.util.toSnakeCase
* Generates a unified error enum for [operation]. [ErrorGenerator] handles generating the individual variants, * Generates a unified error enum for [operation]. [ErrorGenerator] handles generating the individual variants,
* but we must still combine those variants into an enum covering all possible errors for a given operation. * but we must still combine those variants into an enum covering all possible errors for a given operation.
*/ */
class ServerCombinedErrorGenerator( open class ServerCombinedErrorGenerator(
private val model: Model, private val model: Model,
private val symbolProvider: RustSymbolProvider, private val symbolProvider: RustSymbolProvider,
private val operation: OperationShape private val operation: OperationShape
) { ) {
private val operationIndex = OperationIndex.of(model) private val operationIndex = OperationIndex.of(model)
fun render(writer: RustWriter) { open fun render(writer: RustWriter) {
val errors = operationIndex.getErrors(operation) val errors = operationIndex.getErrors(operation)
val operationSymbol = symbolProvider.toSymbol(operation) val operationSymbol = symbolProvider.toSymbol(operation)
val symbol = operation.errorSymbol(symbolProvider) val symbol = operation.errorSymbol(symbolProvider)
@ -87,7 +87,7 @@ class ServerCombinedErrorGenerator(
for (error in errors) { for (error in errors) {
val errorSymbol = symbolProvider.toSymbol(error) val errorSymbol = symbolProvider.toSymbol(error)
writer.rustBlock("impl From<#T> for #T", errorSymbol, symbol) { writer.rustBlock("impl #T<#T> for #T", RuntimeType.From, errorSymbol, symbol) {
rustBlock("fn from(variant: #T) -> #T", errorSymbol, symbol) { rustBlock("fn from(variant: #T) -> #T", errorSymbol, symbol) {
rust("Self::${errorSymbol.name}(variant)") rust("Self::${errorSymbol.name}(variant)")
} }

View File

@ -19,7 +19,7 @@ import software.amazon.smithy.rust.codegen.smithy.generators.CodegenTarget
import software.amazon.smithy.rust.codegen.smithy.generators.EnumGenerator import software.amazon.smithy.rust.codegen.smithy.generators.EnumGenerator
import software.amazon.smithy.rust.codegen.util.dq import software.amazon.smithy.rust.codegen.util.dq
class ServerEnumGenerator( open class ServerEnumGenerator(
model: Model, model: Model,
symbolProvider: RustSymbolProvider, symbolProvider: RustSymbolProvider,
private val writer: RustWriter, private val writer: RustWriter,
@ -55,15 +55,12 @@ class ServerEnumGenerator(
Self::EnumVariantNotFound(Box::new(e)) Self::EnumVariantNotFound(Box::new(e))
} }
} }
impl #{From}<$errorStruct> for #{JsonDeserialize} { impl #{From}<$errorStruct> for #{JsonDeserialize} {
fn from(e: $errorStruct) -> Self { fn from(e: $errorStruct) -> Self {
Self::custom(format!("unknown variant {}", e)) Self::custom(format!("unknown variant {}", e))
} }
} }
impl #{StdError} for $errorStruct { } impl #{StdError} for $errorStruct { }
impl #{Display} for $errorStruct { impl #{Display} for $errorStruct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f) self.0.fmt(f)
@ -83,7 +80,6 @@ class ServerEnumGenerator(
""" """
impl std::str::FromStr for $enumName { impl std::str::FromStr for $enumName {
type Err = $errorStruct; type Err = $errorStruct;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
$enumName::try_from(s) $enumName::try_from(s)
} }

View File

@ -25,7 +25,7 @@ import software.amazon.smithy.rust.codegen.util.toPascalCase
/** /**
* ServerOperationHandlerGenerator * ServerOperationHandlerGenerator
*/ */
class ServerOperationHandlerGenerator( open class ServerOperationHandlerGenerator(
codegenContext: CodegenContext, codegenContext: CodegenContext,
private val operations: List<OperationShape>, private val operations: List<OperationShape>,
) { ) {
@ -48,7 +48,7 @@ class ServerOperationHandlerGenerator(
"http" to RuntimeType.http, "http" to RuntimeType.http,
) )
fun render(writer: RustWriter) { open fun render(writer: RustWriter) {
renderHandlerImplementations(writer, false) renderHandlerImplementations(writer, false)
renderHandlerImplementations(writer, true) renderHandlerImplementations(writer, true)
} }

View File

@ -6,7 +6,9 @@
package software.amazon.smithy.rust.codegen.server.smithy.generators package software.amazon.smithy.rust.codegen.server.smithy.generators
import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.knowledge.TopDownIndex
import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rust.codegen.rustlang.RustModule import software.amazon.smithy.rust.codegen.rustlang.RustModule
import software.amazon.smithy.rust.codegen.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolTestGenerator import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocolTestGenerator
import software.amazon.smithy.rust.codegen.smithy.CodegenContext import software.amazon.smithy.rust.codegen.smithy.CodegenContext
import software.amazon.smithy.rust.codegen.smithy.RustCrate import software.amazon.smithy.rust.codegen.smithy.RustCrate
@ -20,7 +22,7 @@ import software.amazon.smithy.rust.codegen.smithy.protocols.HttpBindingResolver
* Service generator is the main codegeneration entry point for Smithy services. Individual structures and unions are * Service generator is the main codegeneration entry point for Smithy services. Individual structures and unions are
* generated in codegen visitor, but this class handles all protocol-specific code generation (i.e. operations). * generated in codegen visitor, but this class handles all protocol-specific code generation (i.e. operations).
*/ */
class ServerServiceGenerator( open class ServerServiceGenerator(
private val rustCrate: RustCrate, private val rustCrate: RustCrate,
private val protocolGenerator: ProtocolGenerator, private val protocolGenerator: ProtocolGenerator,
private val protocolSupport: ProtocolSupport, private val protocolSupport: ProtocolSupport,
@ -28,13 +30,13 @@ class ServerServiceGenerator(
private val context: CodegenContext, private val context: CodegenContext,
) { ) {
private val index = TopDownIndex.of(context.model) private val index = TopDownIndex.of(context.model)
protected val operations = index.getContainedOperations(context.serviceShape).sortedBy { it.id }
/** /**
* Render Service Specific code. Code will end up in different files via [useShapeWriter]. See `SymbolVisitor.kt` * Render Service Specific code. Code will end up in different files via [useShapeWriter]. See `SymbolVisitor.kt`
* which assigns a symbol location to each shape. * which assigns a symbol location to each shape.
*/ */
fun render() { fun render() {
val operations = index.getContainedOperations(context.serviceShape).sortedBy { it.id }
for (operation in operations) { for (operation in operations) {
rustCrate.useShapeWriter(operation) { operationWriter -> rustCrate.useShapeWriter(operation) { operationWriter ->
protocolGenerator.serverRenderOperation( protocolGenerator.serverRenderOperation(
@ -44,21 +46,36 @@ class ServerServiceGenerator(
ServerProtocolTestGenerator(context, protocolSupport, operation, operationWriter) ServerProtocolTestGenerator(context, protocolSupport, operation, operationWriter)
.render() .render()
} }
if (operation.errors.isNotEmpty()) { if (operation.errors.isNotEmpty()) {
rustCrate.withModule(RustModule.Error) { writer -> rustCrate.withModule(RustModule.Error) { writer ->
ServerCombinedErrorGenerator(context.model, context.symbolProvider, operation) renderCombinedErrors(writer, operation)
.render(writer)
} }
} }
} }
rustCrate.withModule(RustModule.public("operation_handler", "Operation handlers definition and implementation.")) { writer -> rustCrate.withModule(RustModule.public("operation_handler", "Operation handlers definition and implementation.")) { writer ->
ServerOperationHandlerGenerator(context, operations) renderOperationHandler(writer, operations)
.render(writer)
} }
rustCrate.withModule(RustModule.public("operation_registry", "A registry of your service's operations.")) { writer -> rustCrate.withModule(RustModule.public("operation_registry", "A registry of your service's operations.")) { writer ->
ServerOperationRegistryGenerator(context, httpBindingResolver, operations) renderOperationRegistry(writer, operations)
.render(writer)
} }
renderExtras(operations)
}
// Render any extra section needed by subclasses of `ServerServiceGenerator`.
open fun renderExtras(operations: List<OperationShape>) { }
// Render combined errors.
open fun renderCombinedErrors(writer: RustWriter, operation: OperationShape) {
ServerCombinedErrorGenerator(context.model, context.symbolProvider, operation).render(writer)
}
// Render operations handler.
open fun renderOperationHandler(writer: RustWriter, operations: List<OperationShape>) {
ServerOperationHandlerGenerator(context, operations).render(writer)
}
// Render operations registry.
private fun renderOperationRegistry(writer: RustWriter, operations: List<OperationShape>) {
ServerOperationRegistryGenerator(context, httpBindingResolver, operations).render(writer)
} }
} }

View File

@ -306,7 +306,7 @@ private class ServerHttpBoundProtocolTraitImplGenerator(
if (operationShape.errors.isNotEmpty()) { if (operationShape.errors.isNotEmpty()) {
rustTemplate( rustTemplate(
""" """
impl From<Result<#{O}, #{E}>> for $outputName { impl #{From}<Result<#{O}, #{E}>> for $outputName {
fn from(res: Result<#{O}, #{E}>) -> Self { fn from(res: Result<#{O}, #{E}>) -> Self {
match res { match res {
Ok(v) => Self::Output(v), Ok(v) => Self::Output(v),
@ -316,31 +316,34 @@ private class ServerHttpBoundProtocolTraitImplGenerator(
} }
""".trimIndent(), """.trimIndent(),
"O" to outputSymbol, "O" to outputSymbol,
"E" to errorSymbol "E" to errorSymbol,
"From" to RuntimeType.From
) )
} else { } else {
rustTemplate( rustTemplate(
""" """
impl From<#{O}> for $outputName { impl #{From}<#{O}> for $outputName {
fn from(o: #{O}) -> Self { fn from(o: #{O}) -> Self {
Self(o) Self(o)
} }
} }
""".trimIndent(), """.trimIndent(),
"O" to outputSymbol "O" to outputSymbol,
"From" to RuntimeType.From
) )
} }
// Implement conversion function to "unwrap" into the model operation input types. // Implement conversion function to "unwrap" into the model operation input types.
rustTemplate( rustTemplate(
""" """
impl From<$inputName> for #{I} { impl #{From}<$inputName> for #{I} {
fn from(i: $inputName) -> Self { fn from(i: $inputName) -> Self {
i.0 i.0
} }
} }
""".trimIndent(), """.trimIndent(),
"I" to inputSymbol "I" to inputSymbol,
"From" to RuntimeType.From
) )
} }

View File

@ -40,7 +40,7 @@ class AdditionalErrorsDecoratorTest {
fun `add InternalServerError to infallible operations only`() { fun `add InternalServerError to infallible operations only`() {
model.lookup<OperationShape>("test#Infallible").errors.isEmpty() shouldBe true model.lookup<OperationShape>("test#Infallible").errors.isEmpty() shouldBe true
model.lookup<OperationShape>("test#Fallible").errors.size shouldBe 1 model.lookup<OperationShape>("test#Fallible").errors.size shouldBe 1
val transformedModel = AddInternalServerErrorToInfallibleOpsDecorator().transformModel(service, model) val transformedModel = AddInternalServerErrorToInfallibleOperationsDecorator().transformModel(service, model)
transformedModel.lookup<OperationShape>("test#Infallible").errors.size shouldBe 1 transformedModel.lookup<OperationShape>("test#Infallible").errors.size shouldBe 1
transformedModel.lookup<OperationShape>("test#Fallible").errors.size shouldBe 1 transformedModel.lookup<OperationShape>("test#Fallible").errors.size shouldBe 1
} }
@ -49,7 +49,7 @@ class AdditionalErrorsDecoratorTest {
fun `add InternalServerError to all model operations`() { fun `add InternalServerError to all model operations`() {
model.lookup<OperationShape>("test#Infallible").errors.isEmpty() shouldBe true model.lookup<OperationShape>("test#Infallible").errors.isEmpty() shouldBe true
model.lookup<OperationShape>("test#Fallible").errors.size shouldBe 1 model.lookup<OperationShape>("test#Fallible").errors.size shouldBe 1
val transformedModel = AddInternalServerErrorToAllOpsDecorator().transformModel(service, model) val transformedModel = AddInternalServerErrorToAllOperationsDecorator().transformModel(service, model)
transformedModel.lookup<OperationShape>("test#Infallible").errors.size shouldBe 1 transformedModel.lookup<OperationShape>("test#Infallible").errors.size shouldBe 1
transformedModel.lookup<OperationShape>("test#Fallible").errors.size shouldBe 2 transformedModel.lookup<OperationShape>("test#Fallible").errors.size shouldBe 2
} }

View File

@ -92,7 +92,7 @@ open class EnumGenerator(
const val Values = "values" const val Values = "values"
} }
fun render() { open fun render() {
if (enumTrait.hasNames()) { if (enumTrait.hasNames()) {
// pub enum Blah { V1, V2, .. } // pub enum Blah { V1, V2, .. }
renderEnum() renderEnum()

View File

@ -50,20 +50,20 @@ fun redactIfNecessary(member: MemberShape, model: Model, safeToPrint: String): S
} }
} }
class StructureGenerator( open class StructureGenerator(
val model: Model, val model: Model,
private val symbolProvider: RustSymbolProvider, private val symbolProvider: RustSymbolProvider,
private val writer: RustWriter, private val writer: RustWriter,
private val shape: StructureShape private val shape: StructureShape
) { ) {
private val errorTrait = shape.getTrait<ErrorTrait>() private val errorTrait = shape.getTrait<ErrorTrait>()
private val members: List<MemberShape> = shape.allMembers.values.toList() protected val members: List<MemberShape> = shape.allMembers.values.toList()
private val accessorMembers: List<MemberShape> = when (errorTrait) { protected val accessorMembers: List<MemberShape> = when (errorTrait) {
null -> members null -> members
// Let the ErrorGenerator render the error message accessor if this is an error struct // Let the ErrorGenerator render the error message accessor if this is an error struct
else -> members.filter { "message" != symbolProvider.toMemberName(it) } else -> members.filter { "message" != symbolProvider.toMemberName(it) }
} }
private val name = symbolProvider.toSymbol(shape).name protected val name = symbolProvider.toSymbol(shape).name
fun render(forWhom: CodegenTarget = CodegenTarget.CLIENT) { fun render(forWhom: CodegenTarget = CodegenTarget.CLIENT) {
renderStructure() renderStructure()
@ -153,7 +153,13 @@ class StructureGenerator(
} }
} }
private fun renderStructure() { open fun renderStructureMember(writer: RustWriter, member: MemberShape, memberName: String, memberSymbol: Symbol) {
writer.renderMemberDoc(member, memberSymbol)
memberSymbol.expectRustMetadata().render(writer)
writer.write("$memberName: #T,", symbolProvider.toSymbol(member))
}
open fun renderStructure() {
val symbol = symbolProvider.toSymbol(shape) val symbol = symbolProvider.toSymbol(shape)
val containerMeta = symbol.expectRustMetadata() val containerMeta = symbol.expectRustMetadata()
writer.documentShape(shape, model) writer.documentShape(shape, model)
@ -161,10 +167,8 @@ class StructureGenerator(
containerMeta.copy(derives = withoutDebug).render(writer) containerMeta.copy(derives = withoutDebug).render(writer)
writer.rustBlock("struct $name ${lifetimeDeclaration()}") { writer.rustBlock("struct $name ${lifetimeDeclaration()}") {
forEachMember(members) { member, memberName, memberSymbol -> writer.forEachMember(members) { member, memberName, memberSymbol ->
renderMemberDoc(member, memberSymbol) renderStructureMember(writer, member, memberName, memberSymbol)
memberSymbol.expectRustMetadata().render(this)
write("$memberName: #T,", symbolProvider.toSymbol(member))
} }
} }
@ -172,7 +176,7 @@ class StructureGenerator(
renderDebugImpl() renderDebugImpl()
} }
private fun RustWriter.forEachMember( protected fun RustWriter.forEachMember(
toIterate: List<MemberShape>, toIterate: List<MemberShape>,
block: RustWriter.(MemberShape, String, Symbol) -> Unit block: RustWriter.(MemberShape, String, Symbol) -> Unit
) { ) {

View File

@ -297,7 +297,7 @@ class HttpBindingGenerator(
} }
is BlobShape -> rust( is BlobShape -> rust(
"Ok(#T::new(body))", "Ok(#T::new(body))",
RuntimeType.Blob(runtimeConfig) symbolProvider.toSymbol(targetShape)
) )
// `httpPayload` can be applied to set/map/list shapes. // `httpPayload` can be applied to set/map/list shapes.
// However, none of the AWS protocols support it. // However, none of the AWS protocols support it.

View File

@ -14,10 +14,20 @@ Python server runtime for Smithy Rust Server Framework.
publish = false publish = false
[dependencies] [dependencies]
pyo3 = { version = "0.16" } aws-smithy-http-server = { path = "../aws-smithy-http-server" }
pyo3-asyncio = { version = "0.16", features = ["attributes", "tokio-runtime"] } aws-smithy-types = { path = "../aws-smithy-types" }
bytes = "1.1"
delegate = "0.6"
http = "0.2"
hyper = { version = "0.14", features = ["server", "http1", "http2", "tcp", "stream"] }
num_cpus = "1.13"
paste = "1.0"
pyo3 = { version = "0.16.5" }
pyo3-asyncio = { version = "0.16.0", features = ["attributes", "tokio-runtime"] }
socket2 = { version = "0.4", features = ["all"] } socket2 = { version = "0.4", features = ["all"] }
thiserror = "1.0.31"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
tower = "0.4"
tracing = "0.1.34" tracing = "0.1.34"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@ -0,0 +1,22 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
//! Python error definition.
use thiserror::Error;
/// Python error that implements foreign errors.
#[derive(Error, Debug)]
pub enum Error {
/// Custom error.
#[error("{0}")]
Custom(String),
/// Errors coming from `pyo3::PyErr`.
#[error("PyO3 error: {0}")]
PyO3(#[from] pyo3::PyErr),
/// Error coming from `tokio::task::JoinError`.
#[error("Tokio task join error: {0}")]
TaskJoin(#[from] tokio::task::JoinError),
}

View File

@ -11,10 +11,33 @@
//! //!
//! [PyO3]: https://pyo3.rs/ //! [PyO3]: https://pyo3.rs/
mod error;
mod logging; mod logging;
mod server;
mod socket; mod socket;
mod state;
pub mod types;
#[doc(inline)]
pub use error::Error;
#[doc(inline)] #[doc(inline)]
pub use logging::{setup, LogLevel}; pub use logging::{setup, LogLevel};
#[doc(inline)] #[doc(inline)]
pub use server::{PyApp, PyRouter};
#[doc(inline)]
pub use socket::SharedSocket; pub use socket::SharedSocket;
#[doc(inline)]
pub use state::{PyHandler, PyHandlers, PyState};
#[cfg(test)]
mod tests {
use std::sync::Once;
static INIT: Once = Once::new();
pub(crate) fn initialize() {
INIT.call_once(|| {
pyo3::prepare_freethreaded_python();
});
}
}

View File

@ -88,7 +88,7 @@ impl From<LogLevel> for Level {
} }
} }
/// Modifies the Python `logging` module to deliver its log messages using `tracing::Subscriber` events. /// Modifies the Python `logging` module to deliver its log messages using [tracing::Subscriber] events.
/// ///
/// To achieve this goal, the following changes are made to the module: /// To achieve this goal, the following changes are made to the module:
/// - A new builtin function `logging.python_tracing` transcodes `logging.LogRecord`s to `tracing::Event`s. This function /// - A new builtin function `logging.python_tracing` transcodes `logging.LogRecord`s to `tracing::Event`s. This function
@ -131,9 +131,10 @@ def basicConfig(*pargs, **kwargs):
Ok(()) Ok(())
} }
/// Consumes a Python `logging.LogRecord` and emits a Rust `tracing::Event` instead. /// Consumes a Python `logging.LogRecord` and emits a Rust [tracing::Event] instead.
#[cfg(not(test))] #[cfg(not(test))]
#[pyfunction] #[pyfunction]
#[pyo3(text_signature = "(record)")]
fn python_tracing(record: &PyAny) -> PyResult<()> { fn python_tracing(record: &PyAny) -> PyResult<()> {
let level = record.getattr("levelno")?; let level = record.getattr("levelno")?;
let message = record.getattr("getMessage")?.call0()?; let message = record.getattr("getMessage")?.call0()?;
@ -154,6 +155,7 @@ fn python_tracing(record: &PyAny) -> PyResult<()> {
#[cfg(test)] #[cfg(test)]
#[pyfunction] #[pyfunction]
#[pyo3(text_signature = "(record)")]
fn python_tracing(record: &PyAny) -> PyResult<()> { fn python_tracing(record: &PyAny) -> PyResult<()> {
let message = record.getattr("getMessage")?.call0()?; let message = record.getattr("getMessage")?.call0()?;
pretty_assertions::assert_eq!(message.to_string(), "a message"); pretty_assertions::assert_eq!(message.to_string(), "a message");
@ -163,19 +165,10 @@ fn python_tracing(record: &PyAny) -> PyResult<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::sync::Once;
static INIT: Once = Once::new();
fn initialize() {
INIT.call_once(|| {
pyo3::prepare_freethreaded_python();
});
}
#[test] #[test]
fn tracing_handler_is_injected_in_python() { fn tracing_handler_is_injected_in_python() {
initialize(); crate::tests::initialize();
Python::with_gil(|py| { Python::with_gil(|py| {
setup_python_logging(py, LogLevel::Info).unwrap(); setup_python_logging(py, LogLevel::Info).unwrap();
let logging = py.import("logging").unwrap(); let logging = py.import("logging").unwrap();

View File

@ -0,0 +1,213 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
use std::sync::Arc;
use aws_smithy_http_server::Router;
use pyo3::prelude::*;
use crate::{PyHandler, PyHandlers, PyState, SharedSocket};
/// Python compatible wrapper for the [aws_smithy_http_server::Router] type.
#[pyclass(text_signature = "(router)")]
#[derive(Debug, Clone)]
pub struct PyRouter(pub Router);
/// Python application definition, holding the handlers map, the optional Python context object
/// and the asyncio task locals with the running event loop.
#[pyclass(subclass, text_signature = "()")]
#[derive(Debug, Clone)]
pub struct PyApp {
pub handlers: PyHandlers,
pub context: Option<Arc<PyObject>>,
pub locals: pyo3_asyncio::TaskLocals,
}
#[pymethods]
impl PyApp {
/// Create a new instance of [PyApp].
#[new]
pub fn new(py: Python) -> PyResult<Self> {
let asyncio = py.import("asyncio")?;
let event_loop = asyncio.call_method0("get_event_loop")?;
let locals = pyo3_asyncio::TaskLocals::new(event_loop);
Ok(Self {
handlers: PyHandlers::new(),
context: None,
locals,
})
}
/// Start a single worker with its own Tokio and Python async runtime and provided shared socket.
///
/// Python asynchronous loop needs to be started and handled during the lifetime of the process.
/// First of all we install [uvloop] as the main Python event loop. Thanks to libuv, uvloop
/// performs ~20% better than Python standard event loop in most benchmarks, while being 100%
/// compatible.
/// We retrieve the Python context object, if setup by the user calling [PyApp::context] method,
/// generate the [PyState] structure and build the [aws_smithy_http_server::Router], filling
/// it with the functions generated by `PythonServerOperationHandlerGenerator.kt`.
/// At last we get a cloned reference to the underlying [socket2::Socket].
///
/// Now that all the setup is done, we can start the two runtimes and run the [hyper] server.
/// We spawn a thread with a new [tokio::runtime], setup the middlewares and finally block the
/// thread on `hyper::serve`.
/// The main process continues and at the end it is blocked on Python `loop.run_forever()`.
///
/// [uvloop]: https://github.com/MagicStack/uvloop
#[pyo3(text_signature = "($self, socket, worker_number)")]
fn start_hyper_thread(
&mut self,
py: Python,
socket: &PyCell<SharedSocket>,
worker_number: isize,
) -> PyResult<()> {
// Setup the Python asyncio loop to use `uvloop`.
let asyncio = py.import("asyncio")?;
let uvloop = py.import("uvloop")?;
uvloop.call_method0("install")?;
tracing::debug!("Setting up uvloop for current process");
let event_loop = asyncio.call_method0("new_event_loop")?;
asyncio.call_method1("set_event_loop", (event_loop,))?;
// Create the `PyState` object from the Python context object.
let context = self.context.clone().unwrap_or_else(|| Arc::new(py.None()));
let state = PyState::new(context);
// Build the router.
let router: PyRouter = self.router(py).expect("`start_hyper_thread()` is meant to be called only by subclasses implementing the `router()` method").extract(py)?;
// Clone the socket.
let borrow = socket.try_borrow_mut()?;
let held_socket: &SharedSocket = &*borrow;
let raw_socket = held_socket.get_socket()?;
// Store Python event loop locals.
self.locals = pyo3_asyncio::TaskLocals::new(event_loop);
// Spawn a new background [std::thread] to run the application.
tracing::debug!("Start the Tokio runtime in a background task");
std::thread::spawn(move || {
// The thread needs a new [tokio] runtime.
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.thread_name(format!("smithy-rs[{}]", worker_number))
.build()
.expect("Unable to start a new tokio runtime for this process");
// Register operations into a Router, add middleware and start the `hyper` server,
// all inside a [tokio] blocking function.
tracing::debug!("Add middlewares to Rust Python router");
let app = router.0.layer(
tower::ServiceBuilder::new()
.layer(aws_smithy_http_server::AddExtensionLayer::new(state)),
);
let server = hyper::Server::from_tcp(
raw_socket
.try_into()
.expect("Unable to convert `socket2::Socket` into `std::net::TcpListener`"),
)
.expect("Unable to create hyper server from shared socket")
.serve(app.into_make_service());
tracing::debug!("Starting hyper server from shared socket");
rt.block_on(async move {
// Run forever-ish...
if let Err(err) = server.await {
tracing::error!("server error: {}", err);
}
});
});
// Block on the event loop forever.
tracing::debug!("Run and block on the Python event loop");
let event_loop = (*event_loop).call_method0("run_forever");
tracing::info!("Rust Python server started successfully");
if event_loop.is_err() {
tracing::warn!("Ctrl-c handler, quitting");
}
Ok(())
}
/// Register a new operation in the handlers map.
///
/// The operation registered in the map are used inside the code-generated `router()` method
/// and passed to the [aws_smithy_http_server::Router] as part of the operation handlers call.
#[pyo3(text_signature = "($self, name, func)")]
pub fn register_operation(&mut self, py: Python, name: &str, func: PyObject) -> PyResult<()> {
let inspect = py.import("inspect")?;
// Check if the function is a coroutine.
// NOTE: that `asyncio.iscoroutine()` doesn't work here.
let is_coroutine = inspect
.call_method1("iscoroutinefunction", (&func,))?
.extract::<bool>()?;
// Find number of expected methods (a Python implementation could not accept the context).
let func_args = inspect
.call_method1("getargs", (func.getattr(py, "__code__")?,))?
.getattr("args")?
.extract::<Vec<String>>()?;
let func = PyHandler {
func,
is_coroutine,
args: func_args.len(),
};
tracing::info!(
"Registering {} function `{}` for operation {} with {} arguments",
if func.is_coroutine { "async" } else { "sync" },
name,
func.func,
func.args
);
// Insert the handler in the handlers map.
self.handlers
.insert(String::from(name), std::sync::Arc::new(func));
Ok(())
}
/// Register a new context object inside the Rust state.
#[pyo3(text_signature = "($self, context)")]
pub fn context(&mut self, _py: Python, context: PyObject) {
self.context = Some(Arc::new(context));
}
/// This method is here because it is meant to be overriden by the code-generated
/// `App` structure (see PythonServerApplicationGenerator.kt) with the code needed
/// to build the [aws_smithy_http_server::Router] and register the operations on it.
#[pyo3(text_signature = "($self)")]
pub fn router(&self, _py: Python) -> Option<PyObject> {
None
}
/// Main entrypoint: start the server on multiple workers.
///
/// The multiprocessing server is achieved using the ability of a Python interpreter
/// to clone and start itself as a new process.
/// The shared sockets is created and Using the [multiprocessing::Process] module, multiple
/// workers with the method `self.start_single_python_worker()` as target are started.
///
/// [multiprocessing::Process]: https://docs.python.org/3/library/multiprocessing.html
#[pyo3(text_signature = "($self, address, port, backlog, workers)")]
fn run(
&mut self,
py: Python,
address: Option<String>,
port: Option<i32>,
backlog: Option<i32>,
workers: Option<usize>,
) -> PyResult<()> {
let mp = py.import("multiprocessing")?;
mp.call_method0("allow_connection_pickling")?;
let address = address.unwrap_or_else(|| String::from("127.0.0.1"));
let port = port.unwrap_or(8080);
let socket = SharedSocket::new(address, port, backlog)?;
for idx in 0..workers.unwrap_or_else(num_cpus::get) {
let sock = socket.try_clone()?;
let process = mp.getattr("Process")?;
let handle = process.call1((
py.None(),
self.clone().into_py(py).getattr(py, "start_hyper_worker")?,
format!("smithy-rs[{}]", idx),
(sock.into_py(py), idx),
))?;
handle.call_method0("start")?;
}
Ok(())
}
}

View File

@ -74,12 +74,20 @@ impl SharedSocket {
/// Clone the inner socket allowing it to be shared between multiple /// Clone the inner socket allowing it to be shared between multiple
/// Python processes. /// Python processes.
#[pyo3(text_signature = "($self, socket, worker_number)")]
pub fn try_clone(&self) -> PyResult<SharedSocket> { pub fn try_clone(&self) -> PyResult<SharedSocket> {
let copied = self.inner.try_clone()?; let copied = self.inner.try_clone()?;
Ok(SharedSocket { inner: copied }) Ok(SharedSocket { inner: copied })
} }
} }
impl SharedSocket {
/// Get a cloned inner socket.
pub fn get_socket(&self) -> Result<Socket, std::io::Error> {
self.inner.try_clone()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -0,0 +1,47 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
//! [PyState] and Python handlers..
use std::{collections::HashMap, ops::Deref, sync::Arc};
use pyo3::prelude::*;
/// The Python business logic implementation needs to carry some information
/// to be executed properly like the size of its arguments and if it is
/// a coroutine.
#[derive(Debug, Clone)]
pub struct PyHandler {
pub func: PyObject,
pub args: usize,
pub is_coroutine: bool,
}
impl Deref for PyHandler {
type Target = PyObject;
fn deref(&self) -> &Self::Target {
&self.func
}
}
/// Mapping holding the Python business logic handlers.
pub type PyHandlers = HashMap<String, Arc<PyHandler>>;
/// [PyState] structure holding the Python context.
///
/// The possibility of passing the State or not is decided in Python if the method
/// `context()` is called on the `App` to register a context object.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyState {
pub context: Arc<PyObject>,
}
impl PyState {
/// Create a new [PyState] structure.
pub fn new(context: Arc<PyObject>) -> Self {
Self { context }
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
//! Python wrapped types from aws-smithy-types.
use pyo3::prelude::*;
/// Python Wrapper for [aws_smithy_types::Blob].
#[pyclass]
#[derive(Debug, Clone, PartialEq)]
pub struct Blob(aws_smithy_types::Blob);
impl Blob {
/// Creates a new blob from the given `input`.
pub fn new<T: Into<Vec<u8>>>(input: T) -> Self {
Self(aws_smithy_types::Blob::new(input))
}
/// Consumes the `Blob` and returns a `Vec<u8>` with its contents.
pub fn into_inner(self) -> Vec<u8> {
self.0.into_inner()
}
}
impl AsRef<[u8]> for Blob {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[pymethods]
impl Blob {
/// Create a new Python instance of `Blob`.
#[new]
pub fn pynew(input: Vec<u8>) -> Self {
Self(aws_smithy_types::Blob::new(input))
}
/// Python getter for the `Blob` byte array.
#[getter(data)]
pub fn get_data(&self) -> &[u8] {
self.as_ref()
}
/// Python setter for the `Blob` byte array.
#[setter(data)]
pub fn set_data(&mut self, data: Vec<u8>) {
*self = Self::pynew(data);
}
}
#[cfg(test)]
mod tests {
use pyo3::py_run;
use super::*;
#[test]
fn blob_can_be_used_in_python_when_initialized_in_rust() {
crate::tests::initialize();
Python::with_gil(|py| {
let blob = Blob::new("some data".as_bytes().to_vec());
let blob = PyCell::new(py, blob).unwrap();
py_run!(
py,
blob,
r#"
assert blob.data == b"some data"
assert len(blob.data) == 9
blob.data = b"some other data"
assert blob.data == b"some other data"
assert len(blob.data) == 15
"#
);
})
}
#[test]
fn blob_can_be_initialized_in_python() {
crate::tests::initialize();
Python::with_gil(|py| {
let types = PyModule::new(py, "types").unwrap();
types.add_class::<Blob>().unwrap();
py_run!(
py,
types,
r#"
blob = types.Blob(b"some data")
assert blob.data == b"some data"
assert len(blob.data) == 9
blob.data = b"some other data"
assert blob.data == b"some other data"
assert len(blob.data) == 15
"#
);
})
}
}

View File

@ -48,7 +48,7 @@ pub struct IntoMakeService<S> {
} }
impl<S> IntoMakeService<S> { impl<S> IntoMakeService<S> {
pub(super) fn new(service: S) -> Self { pub fn new(service: S) -> Self {
Self { service } Self { service }
} }
} }

View File

@ -7,7 +7,6 @@
//! //!
//! [Smithy specification]: https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html //! [Smithy specification]: https://awslabs.github.io/smithy/1.0/spec/core/http-traits.html
use self::future::RouterFuture;
use self::request_spec::RequestSpec; use self::request_spec::RequestSpec;
use self::tiny_map::TinyMap; use self::tiny_map::TinyMap;
use crate::body::{boxed, Body, BoxBody, HttpBody}; use crate::body::{boxed, Body, BoxBody, HttpBody};
@ -34,7 +33,7 @@ pub mod request_spec;
mod route; mod route;
mod tiny_map; mod tiny_map;
pub use self::{into_make_service::IntoMakeService, route::Route}; pub use self::{future::RouterFuture, into_make_service::IntoMakeService, route::Route};
/// The router is a [`tower::Service`] that routes incoming requests to other `Service`s /// The router is a [`tower::Service`] that routes incoming requests to other `Service`s
/// based on the request's URI and HTTP method or on some specific header setting the target operation. /// based on the request's URI and HTTP method or on some specific header setting the target operation.