mirror of https://github.com/smithy-lang/smithy-rs
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:
parent
8911e86515
commit
8e84ee2eff
|
@ -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")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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")
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,") }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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)")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::*;
|
||||||
|
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue