From 557e2006d9efea7b46051e36e98e9449b3f00f79 Mon Sep 17 00:00:00 2001 From: Matteo Bigoi <1781140+crisidev@users.noreply.github.com> Date: Thu, 20 Apr 2023 15:18:15 +0100 Subject: [PATCH] [Python] Improve Python stubs generation (#2606) ## Motivation and Context This PR improves the Python stubs generation. ## Description The main change is about avoiding to setup a placeholder for the Python module and use the real module name, which allows to generate correct docstrings during codegeneration. We also change the stubs layout on disk, with the main stub entrypoint called `__init__.pyi` instead of `$module_name.pyi`. The README from the Rust runtime crate has been moved completely to the example folder and I run autoformatting and style checks on the Python example code. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._ --------- Signed-off-by: Bigo <1781140+crisidev@users.noreply.github.com> Co-authored-by: Burak --- .../smithy/PythonServerCodegenVisitor.kt | 4 +- .../server/python/smithy/PythonType.kt | 42 ++++---- .../generators/PythonApplicationGenerator.kt | 12 +-- .../PythonServerStructureGenerator.kt | 13 ++- .../generators/PythonServerUnionGenerator.kt | 14 ++- .../PythonTypeInformationGenerationTest.kt | 3 +- examples/python/README.md | 99 +++++++++++++++---- examples/python/pokemon_service.py | 74 +++++--------- .../aws-smithy-http-server-python/README.md | 70 ------------- .../aws-smithy-http-server-python/stubgen.py | 2 +- 10 files changed, 156 insertions(+), 177 deletions(-) diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt index fa1f8eeb65..9dd3d0d658 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCodegenVisitor.kt @@ -152,7 +152,7 @@ class PythonServerCodegenVisitor( rustCrate.useShapeWriter(shape) { // Use Python specific structure generator that adds the #[pyclass] attribute // and #[pymethods] implementation. - PythonServerStructureGenerator(model, codegenContext.symbolProvider, this, shape).render() + PythonServerStructureGenerator(model, codegenContext, this, shape).render() shape.getTrait()?.also { errorTrait -> ErrorImplGenerator( @@ -190,7 +190,7 @@ class PythonServerCodegenVisitor( override fun unionShape(shape: UnionShape) { logger.info("[python-server-codegen] Generating an union shape $shape") rustCrate.useShapeWriter(shape) { - PythonServerUnionGenerator(model, codegenContext.symbolProvider, this, shape, renderUnknownVariant = false).render() + PythonServerUnionGenerator(model, codegenContext, this, shape, renderUnknownVariant = false).render() } if (shape.isReachableFromOperationInput() && shape.canReachConstrainedShape( diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt index 95af2cd14e..42f878d6fc 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt @@ -100,17 +100,14 @@ sealed class PythonType { override val namespace = type.namespace } - data class Opaque(override val name: String, val rustNamespace: String? = null) : PythonType() { - // Since Python doesn't have a something like Rust's `crate::` we are using a custom placeholder here - // and in our stub generation script we will replace placeholder with the real root module name. - private val pythonRootModulePlaceholder = "__root_module_name__" + data class Opaque(override val name: String, val pythonRootModuleName: String, val rustNamespace: String? = null) : PythonType() { override val namespace: String? = rustNamespace?.split("::")?.joinToString(".") { when (it) { - "crate" -> pythonRootModulePlaceholder + "crate" -> pythonRootModuleName // In Python, we expose submodules from `aws_smithy_http_server_python` - // like `types`, `middleware`, `tls` etc. from `__root_module__name` - "aws_smithy_http_server_python" -> pythonRootModulePlaceholder + // like `types`, `middleware`, `tls` etc. from Python root module + "aws_smithy_http_server_python" -> pythonRootModuleName else -> it } } @@ -120,26 +117,29 @@ sealed class PythonType { /** * Return corresponding [PythonType] for a [RustType]. */ -fun RustType.pythonType(): PythonType = +fun RustType.pythonType(pythonRootModuleName: String): PythonType = when (this) { is RustType.Unit -> PythonType.None is RustType.Bool -> PythonType.Bool is RustType.Float -> PythonType.Float is RustType.Integer -> PythonType.Int is RustType.String -> PythonType.Str - is RustType.Vec -> PythonType.List(this.member.pythonType()) - is RustType.Slice -> PythonType.List(this.member.pythonType()) - is RustType.HashMap -> PythonType.Dict(this.key.pythonType(), this.member.pythonType()) - is RustType.HashSet -> PythonType.Set(this.member.pythonType()) - is RustType.Reference -> this.member.pythonType() - is RustType.Option -> PythonType.Optional(this.member.pythonType()) - is RustType.Box -> this.member.pythonType() - is RustType.Dyn -> this.member.pythonType() - is RustType.Application -> PythonType.Application(this.type.pythonType(), this.args.map { it.pythonType() }) - is RustType.Opaque -> PythonType.Opaque(this.name, this.namespace) - // TODO(Constraints): How to handle this? - // Revisit as part of https://github.com/awslabs/smithy-rs/issues/2114 - is RustType.MaybeConstrained -> this.member.pythonType() + is RustType.Vec -> PythonType.List(this.member.pythonType(pythonRootModuleName)) + is RustType.Slice -> PythonType.List(this.member.pythonType(pythonRootModuleName)) + is RustType.HashMap -> PythonType.Dict(this.key.pythonType(pythonRootModuleName), this.member.pythonType(pythonRootModuleName)) + is RustType.HashSet -> PythonType.Set(this.member.pythonType(pythonRootModuleName)) + is RustType.Reference -> this.member.pythonType(pythonRootModuleName) + is RustType.Option -> PythonType.Optional(this.member.pythonType(pythonRootModuleName)) + is RustType.Box -> this.member.pythonType(pythonRootModuleName) + is RustType.Dyn -> this.member.pythonType(pythonRootModuleName) + is RustType.Application -> PythonType.Application( + this.type.pythonType(pythonRootModuleName), + this.args.map { + it.pythonType(pythonRootModuleName) + }, + ) + is RustType.Opaque -> PythonType.Opaque(this.name, pythonRootModuleName, rustNamespace = this.namespace) + is RustType.MaybeConstrained -> this.member.pythonType(pythonRootModuleName) } /** diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt index deec6f6b29..96ecbfe75c 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt @@ -250,11 +250,11 @@ class PythonApplicationGenerator( """, *codegenScope, ) { - val middlewareRequest = PythonType.Opaque("Request", "crate::middleware") - val middlewareResponse = PythonType.Opaque("Response", "crate::middleware") + val middlewareRequest = PythonType.Opaque("Request", libName, rustNamespace = "crate::middleware") + val middlewareResponse = PythonType.Opaque("Response", libName, rustNamespace = "crate::middleware") val middlewareNext = PythonType.Callable(listOf(middlewareRequest), PythonType.Awaitable(middlewareResponse)) val middlewareFunc = PythonType.Callable(listOf(middlewareRequest, middlewareNext), PythonType.Awaitable(middlewareResponse)) - val tlsConfig = PythonType.Opaque("TlsConfig", "crate::tls") + val tlsConfig = PythonType.Opaque("TlsConfig", libName, rustNamespace = "crate::tls") rustTemplate( """ @@ -344,9 +344,9 @@ class PythonApplicationGenerator( val operationName = symbolProvider.toSymbol(operation).name val fnName = RustReservedWords.escapeIfNeeded(symbolProvider.toSymbol(operation).name.toSnakeCase()) - val input = PythonType.Opaque("${operationName}Input", "crate::input") - val output = PythonType.Opaque("${operationName}Output", "crate::output") - val context = PythonType.Opaque("Ctx") + val input = PythonType.Opaque("${operationName}Input", libName, rustNamespace = "crate::input") + val output = PythonType.Opaque("${operationName}Output", libName, rustNamespace = "crate::output") + val context = PythonType.Opaque("Ctx", libName) val returnType = PythonType.Union(listOf(output, PythonType.Awaitable(output))) val handler = PythonType.Union( listOf( diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt index 1502f5422a..d9d2df8cc2 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt @@ -18,16 +18,17 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rust import software.amazon.smithy.rust.codegen.core.rustlang.rustInlineTemplate import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate import software.amazon.smithy.rust.codegen.core.rustlang.writable -import software.amazon.smithy.rust.codegen.core.smithy.RustSymbolProvider import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureGenerator import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.core.util.isEventStream +import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.python.smithy.PythonEventStreamSymbolProvider import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency import software.amazon.smithy.rust.codegen.server.python.smithy.PythonType import software.amazon.smithy.rust.codegen.server.python.smithy.pythonType import software.amazon.smithy.rust.codegen.server.python.smithy.renderAsDocstring +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext /** * To share structures defined in Rust with Python, `pyo3` provides the `PyClass` trait. @@ -36,11 +37,13 @@ import software.amazon.smithy.rust.codegen.server.python.smithy.renderAsDocstrin */ class PythonServerStructureGenerator( model: Model, - private val symbolProvider: RustSymbolProvider, + private val codegenContext: ServerCodegenContext, private val writer: RustWriter, private val shape: StructureShape, -) : StructureGenerator(model, symbolProvider, writer, shape, emptyList()) { +) : StructureGenerator(model, codegenContext.symbolProvider, writer, shape, emptyList()) { + private val symbolProvider = codegenContext.symbolProvider + private val libName = codegenContext.settings.moduleName.toSnakeCase() private val pyO3 = PythonServerCargoDependency.PyO3.toType() override fun renderStructure() { @@ -157,9 +160,9 @@ class PythonServerStructureGenerator( private fun memberPythonType(shape: MemberShape, symbol: Symbol): PythonType = if (shape.isEventStream(model)) { val eventStreamSymbol = PythonEventStreamSymbolProvider.parseSymbol(symbol) - val innerT = eventStreamSymbol.innerT.pythonType() + val innerT = eventStreamSymbol.innerT.pythonType(libName) PythonType.AsyncIterator(innerT) } else { - symbol.rustType().pythonType() + symbol.rustType().pythonType(libName) } } diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerUnionGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerUnionGenerator.kt index 6b7a7bee8a..01a2a833af 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerUnionGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerUnionGenerator.kt @@ -26,6 +26,7 @@ import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency import software.amazon.smithy.rust.codegen.server.python.smithy.pythonType import software.amazon.smithy.rust.codegen.server.python.smithy.renderAsDocstring +import software.amazon.smithy.rust.codegen.server.smithy.ServerCodegenContext /* * Generate unions that are compatible with Python by wrapping the Rust implementation into @@ -34,11 +35,13 @@ import software.amazon.smithy.rust.codegen.server.python.smithy.renderAsDocstrin */ class PythonServerUnionGenerator( model: Model, - private val symbolProvider: SymbolProvider, + private val codegenContext: ServerCodegenContext, private val writer: RustWriter, shape: UnionShape, private val renderUnknownVariant: Boolean = true, -) : UnionGenerator(model, symbolProvider, writer, shape, renderUnknownVariant) { +) : UnionGenerator(model, codegenContext.symbolProvider, writer, shape, renderUnknownVariant) { + private val symbolProvider = codegenContext.symbolProvider + private val libName = codegenContext.settings.moduleName.toSnakeCase() private val sortedMembers: List = shape.allMembers.values.sortedBy { symbolProvider.toMemberName(it) } private val unionSymbol = symbolProvider.toSymbol(shape) @@ -125,7 +128,7 @@ class PythonServerUnionGenerator( } } else { val memberSymbol = symbolProvider.toSymbol(member) - val pythonType = memberSymbol.rustType().pythonType() + val pythonType = memberSymbol.rustType().pythonType(libName) val targetType = memberSymbol.rustType() Attribute("staticmethod").render(writer) writer.rust( @@ -166,7 +169,7 @@ class PythonServerUnionGenerator( } } else { val memberSymbol = symbolProvider.toSymbol(member) - val pythonType = memberSymbol.rustType().pythonType() + val pythonType = memberSymbol.rustType().pythonType(libName) val targetSymbol = symbolProvider.toSymbol(model.expectShape(member.target)) val rustType = memberSymbol.rustType() writer.rust( @@ -181,12 +184,13 @@ class PythonServerUnionGenerator( } else { "variant.clone()" } + val errorVariant = memberSymbol.rustType().pythonType(libName).renderAsDocstring() rustTemplate( """ match self.0.as_$funcNamePart() { Ok(variant) => Ok($variantType), Err(_) => Err(#{pyo3}::exceptions::PyValueError::new_err( - r"${unionSymbol.name} variant is not of type ${memberSymbol.rustType().pythonType().renderAsDocstring()}" + r"${unionSymbol.name} variant is not of type $errorVariant" )), } """, diff --git a/codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt b/codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt index 1473edcffe..c552d32edd 100644 --- a/codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt +++ b/codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt @@ -28,9 +28,8 @@ internal class PythonTypeInformationGenerationTest { val foo = model.lookup("test#Foo") val codegenContext = serverTestCodegenContext(model) - val symbolProvider = codegenContext.symbolProvider val writer = RustWriter.forModule("model") - PythonServerStructureGenerator(model, symbolProvider, writer, foo).render() + PythonServerStructureGenerator(model, codegenContext, writer, foo).render() val result = writer.toString() diff --git a/examples/python/README.md b/examples/python/README.md index ebe6cfb39e..e3fd43929a 100644 --- a/examples/python/README.md +++ b/examples/python/README.md @@ -12,8 +12,8 @@ The Python implementation of the service can be found inside * [Python stub generation](#python-stub-generation) * [Run](#run) * [Test](#test) -* [Uvloop](#uvloop) * [MacOs](#macos) +* [Running servers on AWS Lambda](#running-servers-on-aws-lambda) ## Build @@ -26,10 +26,14 @@ build. Ensure these dependencies are installed. -``` +```bash pip install maturin uvloop aiohttp mypy ``` +The server can depend on [uvloop](https://pypi.org/project/uvloop/) for a faster +event loop implementation and it will be automatically used instead of the standard +library event loop if it is found in the dependencies' closure. + ### Makefile The build logic is drive by the Makefile: @@ -37,11 +41,13 @@ The build logic is drive by the Makefile: * `make codegen`: run the codegenerator. * `make build`: build the Maturin package in debug mode (includes type stubs generation). -* `make release`: build the Maturin package in release mode (includes type stub +* `make release`: build the Maturin package in release mode (includes type stubs generation). -* `make install`: install the latest release or debug build. +* `make install-wheel`: install the latest release or debug wheel using `pip`. * `make run`: run the example server. * `make test`: run the end-to-end integration test. +* `make clean`: clean the Cargo artefacts. +* `make dist-clean`: clean the Cargo artefacts and generated folders. ### Python stub generation @@ -52,7 +58,7 @@ It will do first compilation of the crate, generate the types and exit. The script takes some command line arguments: -``` +```bash ./stubgen.sh module_name manifest_path output_directory ``` @@ -72,19 +78,6 @@ tests against it. More info can be found in the `tests` folder of `pokemon-service-test` package. -## Uvloop - -The server can depend on [uvloop](https://pypi.org/project/uvloop/) for a -faster event loop implementation. Uvloop can be installed with your favourite -package manager or by using pip: - -```sh -pip instal uvloop -``` - -and it will be automatically used instead of the standard library event loop if -it is found in the dependencies' closure. - ## MacOs To compile and test on MacOs, please follow the official PyO3 guidelines on how @@ -92,3 +85,73 @@ to [configure your linker](https://pyo3.rs/latest/building_and_distribution.html Please note that the `.cargo/config.toml` with linkers override can be local to your project. + +## Running servers on AWS Lambda + +`aws-smithy-http-server-python` supports running your services on [AWS Lambda](https://aws.amazon.com/lambda/). + +You need to use `run_lambda` method instead of `run` method to start +the [custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) +instead of the [Hyper](https://hyper.rs/) HTTP server. + +In your `app.py`: + +```diff +from pokemon_service_server_sdk import App +from pokemon_service_server_sdk.error import ResourceNotFoundException + +# ... + +# Get the number of requests served by this server. +@app.get_server_statistics +def get_server_statistics( + _: GetServerStatisticsInput, context: Context +) -> GetServerStatisticsOutput: + calls_count = context.get_calls_count() + logging.debug("The service handled %d requests", calls_count) + return GetServerStatisticsOutput(calls_count=calls_count) + +# ... + +-app.run() ++app.run_lambda() +``` + +`aws-smithy-http-server-python` comes with a +[custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) +so you should run your service without any provided runtimes. +You can achieve that with a `Dockerfile` similar to this: + +```dockerfile +# You can use any image that has your desired Python version +FROM public.ecr.aws/lambda/python:3.8-x86_64 + +# Copy your application code to `LAMBDA_TASK_ROOT` +COPY app.py ${LAMBDA_TASK_ROOT} + +# When you build your Server SDK for your service, you will get a Python wheel. +# You just need to copy that wheel and install it via `pip` inside your image. +# Note that you need to build your library for Linux, and Python version used to +# build your SDK should match with your image's Python version. +# For cross compiling, you can consult to: +# https://pyo3.rs/latest/building_and_distribution.html#cross-compiling +COPY wheels/ ${LAMBDA_TASK_ROOT}/wheels +RUN pip3 install ${LAMBDA_TASK_ROOT}/wheels/*.whl + +# You can install your application's other dependencies listed in `requirements.txt`. +COPY requirements.txt . +RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" + +# Create a symlink for your application's entrypoint, +# so we can use `/app.py` to refer it +RUN ln -s ${LAMBDA_TASK_ROOT}/app.py /app.py + +# By default `public.ecr.aws/lambda/python` images comes with Python runtime, +# we need to override `ENTRYPOINT` and `CMD` to not call that runtime and +# instead run directly your service and it will start our custom runtime. +ENTRYPOINT [ "/var/lang/bin/python3.8" ] +CMD [ "/app.py" ] +``` + +See [https://docs.aws.amazon.com/lambda/latest/dg/images-create.html#images-create-from-base](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html#images-create-from-base) +for more details on building your custom image. diff --git a/examples/python/pokemon_service.py b/examples/python/pokemon_service.py index 210d232687..7ee4a7284f 100644 --- a/examples/python/pokemon_service.py +++ b/examples/python/pokemon_service.py @@ -6,47 +6,38 @@ import argparse import logging import random -from threading import Lock from dataclasses import dataclass -from typing import Dict, Any, List, Optional, Callable, Awaitable, AsyncIterator +from threading import Lock +from typing import Any, AsyncIterator, Awaitable, Callable, Dict, List, Optional from pokemon_service_server_sdk import App -from pokemon_service_server_sdk.tls import TlsConfig from pokemon_service_server_sdk.aws_lambda import LambdaContext from pokemon_service_server_sdk.error import ( + InvalidPokeballError, + MasterBallUnsuccessful, ResourceNotFoundException, UnsupportedRegionError, - MasterBallUnsuccessful, - InvalidPokeballError, ) from pokemon_service_server_sdk.input import ( + CapturePokemonInput, + CheckHealthInput, DoNothingInput, GetPokemonSpeciesInput, GetServerStatisticsInput, - CheckHealthInput, StreamPokemonRadioInput, - CapturePokemonInput, ) from pokemon_service_server_sdk.logging import TracingHandler -from pokemon_service_server_sdk.middleware import ( - MiddlewareException, - Response, - Request, -) -from pokemon_service_server_sdk.model import ( - CapturePokemonEvents, - CaptureEvent, - FlavorText, - Language, -) +from pokemon_service_server_sdk.middleware import MiddlewareException, Request, Response +from pokemon_service_server_sdk.model import CaptureEvent, CapturePokemonEvents, FlavorText, Language from pokemon_service_server_sdk.output import ( + CapturePokemonOutput, + CheckHealthOutput, DoNothingOutput, GetPokemonSpeciesOutput, GetServerStatisticsOutput, - CheckHealthOutput, StreamPokemonRadioOutput, - CapturePokemonOutput, ) +from pokemon_service_server_sdk.tls import TlsConfig from pokemon_service_server_sdk.types import ByteStream # Logging can bee setup using standard Python tooling. We provide @@ -164,17 +155,16 @@ def get_random_radio_stream(self) -> str: # Next is either the next middleware in the stack or the handler. Next = Callable[[Request], Awaitable[Response]] + # This middleware checks the `Content-Type` from the request header, # logs some information depending on that and then calls `next`. @app.middleware async def check_content_type_header(request: Request, next: Next) -> Response: content_type = request.headers.get("content-type") if content_type == "application/json": - logging.debug("Found valid `application/json` content type") + logging.debug("found valid `application/json` content type") else: - logging.warning( - f"Invalid content type {content_type}, dumping headers: {request.headers.items()}" - ) + logging.warning("invalid content type %s, dumping headers: %s", content_type, request.headers.items()) return await next(request) @@ -184,7 +174,7 @@ async def check_content_type_header(request: Request, next: Next) -> Response: @app.middleware async def add_x_amzn_answer_header(request: Request, next: Next) -> Response: request.headers["x-amzn-answer"] = "42" - logging.debug("Setting `x-amzn-answer` header to 42") + logging.debug("setting `x-amzn-answer` header to 42") return await next(request) @@ -205,18 +195,15 @@ async def check_x_amzn_answer_header(request: Request, next: Next) -> Response: # DoNothing operation used for raw benchmarking. @app.do_nothing def do_nothing(_: DoNothingInput) -> DoNothingOutput: - # logging.debug("Running the DoNothing operation") return DoNothingOutput() # Get the translation of a Pokémon specie or an error. @app.get_pokemon_species -def get_pokemon_species( - input: GetPokemonSpeciesInput, context: Context -) -> GetPokemonSpeciesOutput: +def get_pokemon_species(input: GetPokemonSpeciesInput, context: Context) -> GetPokemonSpeciesOutput: if context.lambda_ctx is not None: logging.debug( - "Lambda Context: %s", + "lambda Context: %s", dict( request_id=context.lambda_ctx.request_id, deadline=context.lambda_ctx.deadline, @@ -229,24 +216,19 @@ def get_pokemon_species( context.increment_calls_count() flavor_text_entries = context.get_pokemon_description(input.name) if flavor_text_entries: - logging.debug("Total requests executed: %s", context.get_calls_count()) - logging.info("Found description for Pokémon %s", input.name) - logging.error("Found some stuff") - return GetPokemonSpeciesOutput( - name=input.name, flavor_text_entries=flavor_text_entries - ) + logging.debug("total requests executed: %s", context.get_calls_count()) + logging.info("found description for Pokémon %s", input.name) + return GetPokemonSpeciesOutput(name=input.name, flavor_text_entries=flavor_text_entries) else: - logging.warning("Description for Pokémon %s not in the database", input.name) + logging.warning("description for Pokémon %s not in the database", input.name) raise ResourceNotFoundException("Requested Pokémon not available") # Get the number of requests served by this server. @app.get_server_statistics -def get_server_statistics( - _: GetServerStatisticsInput, context: Context -) -> GetServerStatisticsOutput: +def get_server_statistics(_: GetServerStatisticsInput, context: Context) -> GetServerStatisticsOutput: calls_count = context.get_calls_count() - logging.debug("The service handled %d requests", calls_count) + logging.debug("the service handled %d requests", calls_count) return GetServerStatisticsOutput(calls_count=calls_count) @@ -398,7 +380,7 @@ async def events(input: CapturePokemonInput) -> AsyncIterator[CapturePokemonEven break # You can catch modeled errors and act accordingly, they will terminate the event stream except MasterBallUnsuccessful as err: - logging.error(f"masterball unsuccessful: {err}") + logging.error("masterball unsuccessful: %s", err) # Here event stream is going to be completed because we stopped receiving events and we'll # no longer `yield` new values and this asynchronous Python generator will end here @@ -411,17 +393,15 @@ async def events(input: CapturePokemonInput) -> AsyncIterator[CapturePokemonEven # Stream a random Pokémon song. @app.stream_pokemon_radio -async def stream_pokemon_radio( - _: StreamPokemonRadioInput, context: Context -) -> StreamPokemonRadioOutput: +async def stream_pokemon_radio(_: StreamPokemonRadioInput, context: Context) -> StreamPokemonRadioOutput: import aiohttp radio_url = context.get_random_radio_stream() - logging.info("Random radio URL for this stream is %s", radio_url) + logging.info("random radio URL for this stream is %s", radio_url) async with aiohttp.ClientSession() as session: async with session.get(radio_url) as response: data = ByteStream(await response.read()) - logging.debug("Successfully fetched radio url %s", radio_url) + logging.debug("successfully fetched radio url %s", radio_url) return StreamPokemonRadioOutput(data=data) diff --git a/rust-runtime/aws-smithy-http-server-python/README.md b/rust-runtime/aws-smithy-http-server-python/README.md index 7b8d218f3c..da3dc2c131 100644 --- a/rust-runtime/aws-smithy-http-server-python/README.md +++ b/rust-runtime/aws-smithy-http-server-python/README.md @@ -2,76 +2,6 @@ Server libraries for smithy-rs generated servers, targeting pure Python business logic. -## Running servers on AWS Lambda - -`aws-smithy-http-server-python` supports running your services on [AWS Lambda](https://aws.amazon.com/lambda/). - -You need to use `run_lambda` method instead of `run` method to start -the [custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) -instead of the [Hyper](https://hyper.rs/) HTTP server. - -In your `app.py`: - -```diff -from pokemon_service_server_sdk import App -from pokemon_service_server_sdk.error import ResourceNotFoundException - -# ... - -# Get the number of requests served by this server. -@app.get_server_statistics -def get_server_statistics( - _: GetServerStatisticsInput, context: Context -) -> GetServerStatisticsOutput: - calls_count = context.get_calls_count() - logging.debug("The service handled %d requests", calls_count) - return GetServerStatisticsOutput(calls_count=calls_count) - -# ... - --app.run() -+app.run_lambda() -``` - -`aws-smithy-http-server-python` comes with a -[custom runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) -so you should run your service without any provided runtimes. -You can achieve that with a `Dockerfile` similar to this: - -```dockerfile -# You can use any image that has your desired Python version -FROM public.ecr.aws/lambda/python:3.8-x86_64 - -# Copy your application code to `LAMBDA_TASK_ROOT` -COPY app.py ${LAMBDA_TASK_ROOT} - -# When you build your Server SDK for your service, you will get a Python wheel. -# You just need to copy that wheel and install it via `pip` inside your image. -# Note that you need to build your library for Linux, and Python version used to -# build your SDK should match with your image's Python version. -# For cross compiling, you can consult to: -# https://pyo3.rs/latest/building_and_distribution.html#cross-compiling -COPY wheels/ ${LAMBDA_TASK_ROOT}/wheels -RUN pip3 install ${LAMBDA_TASK_ROOT}/wheels/*.whl - -# You can install your application's other dependencies listed in `requirements.txt`. -COPY requirements.txt . -RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" - -# Create a symlink for your application's entrypoint, -# so we can use `/app.py` to refer it -RUN ln -s ${LAMBDA_TASK_ROOT}/app.py /app.py - -# By default `public.ecr.aws/lambda/python` images comes with Python runtime, -# we need to override `ENTRYPOINT` and `CMD` to not call that runtime and -# instead run directly your service and it will start our custom runtime. -ENTRYPOINT [ "/var/lang/bin/python3.8" ] -CMD [ "/app.py" ] -``` - -See [https://docs.aws.amazon.com/lambda/latest/dg/images-create.html#images-create-from-base](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html#images-create-from-base) -for more details on building your custom image. - This crate is part of the [AWS SDK for Rust](https://awslabs.github.io/aws-sdk-rust/) and the [smithy-rs](https://github.com/awslabs/smithy-rs) code generator. In most cases, it should not be used directly. diff --git a/rust-runtime/aws-smithy-http-server-python/stubgen.py b/rust-runtime/aws-smithy-http-server-python/stubgen.py index 458ee2b9c5..0edf1d3ef2 100644 --- a/rust-runtime/aws-smithy-http-server-python/stubgen.py +++ b/rust-runtime/aws-smithy-http-server-python/stubgen.py @@ -393,7 +393,7 @@ def walk_module(writer: Writer, mod: Any): def generate(module: str, outdir: str): - path = Path(outdir) / f"{module}.pyi" + path = Path(outdir) / "__init__.pyi" writer = Writer( path, module,