-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Python: Allow injecting Lambda Context (#1985)
* Provide Python wrappers for Lambda related types * Introduce `PyContext` to wrap raw context object * Use new `PyContext` in handlers * Expose `lambda` module to Python * Use `LambdaContext` in example service * Start Lambda handler in a different thread * Print summary of Lambda context in Pokemon service * Make sure to include Python `builtins` in tests * Make `lambda_ctx` optional Co-authored-by: Matteo Bigoi <[email protected]> * Only inject types if they are type-hinted as `Optional[T]` * Export Lambda module as `aws_lambda` instead of `lambda_` * Comment why we need to run Hyper server in a background thread * Move `is_optional_of` to `util` module * Use `HeaderMap::from_iter` to build headers * Support edge case of `(None, T)` in `util::is_optional_of` * Make Lambda related types feature gated * Remove feature gate for Lambda * Make `xray_trace_id` an `Option` * Remove `aws-lambda` feature from generated `Cargo.toml`s * Fix linting issues * Pin `lambda_runtime` to `0.7.1` * Remove duplicate dependency in `Cargo.toml` Co-authored-by: Matteo Bigoi <[email protected]>
- Loading branch information
Showing
14 changed files
with
896 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
126 changes: 126 additions & 0 deletions
126
rust-runtime/aws-smithy-http-server-python/src/context.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
//! Python context definition. | ||
use http::Extensions; | ||
use pyo3::{PyObject, PyResult, Python, ToPyObject}; | ||
|
||
mod lambda; | ||
pub mod layer; | ||
#[cfg(test)] | ||
mod testing; | ||
|
||
/// PyContext is a wrapper for context object provided by the user. | ||
/// It injects some values (currently only [super::lambda::PyLambdaContext]) that is type-hinted by the user. | ||
/// | ||
/// | ||
/// PyContext is initialised during the startup, it inspects the provided context object for fields | ||
/// that are type-hinted to inject some values provided by the framework (see [PyContext::new()]). | ||
/// | ||
/// After finding fields that needs to be injected, [layer::AddPyContextLayer], a [tower::Layer], | ||
/// populates request-scoped values from incoming request. | ||
/// | ||
/// And finally PyContext implements [ToPyObject] (so it can by passed to Python handlers) | ||
/// that provides [PyObject] provided by the user with the additional values injected by the framework. | ||
#[derive(Clone)] | ||
pub struct PyContext { | ||
inner: PyObject, | ||
// TODO(Refactor): We should ideally keep record of injectable fields in a hashmap like: | ||
// `injectable_fields: HashMap<Field, Box<dyn Injectable>>` where `Injectable` provides a method to extract a `PyObject` from a `Request`, | ||
// but I couldn't find a way to extract a trait object from a Python object. | ||
// We could introduce a registry to keep track of every injectable type but I'm not sure that is the best way to do it, | ||
// so until we found a good way to achive that, I didn't want to introduce any abstraction here and | ||
// keep it simple because we only have one field that is injectable. | ||
lambda_ctx: lambda::PyContextLambda, | ||
} | ||
|
||
impl PyContext { | ||
pub fn new(inner: PyObject) -> PyResult<Self> { | ||
Ok(Self { | ||
lambda_ctx: lambda::PyContextLambda::new(inner.clone())?, | ||
inner, | ||
}) | ||
} | ||
|
||
pub fn populate_from_extensions(&self, _ext: &Extensions) { | ||
self.lambda_ctx | ||
.populate_from_extensions(self.inner.clone(), _ext); | ||
} | ||
} | ||
|
||
impl ToPyObject for PyContext { | ||
fn to_object(&self, _py: Python<'_>) -> PyObject { | ||
self.inner.clone() | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use http::Extensions; | ||
use pyo3::{prelude::*, py_run}; | ||
|
||
use super::testing::get_context; | ||
|
||
#[test] | ||
fn py_context() -> PyResult<()> { | ||
pyo3::prepare_freethreaded_python(); | ||
|
||
let ctx = get_context( | ||
r#" | ||
class Context: | ||
foo: int = 0 | ||
bar: str = 'qux' | ||
ctx = Context() | ||
ctx.foo = 42 | ||
"#, | ||
); | ||
Python::with_gil(|py| { | ||
py_run!( | ||
py, | ||
ctx, | ||
r#" | ||
assert ctx.foo == 42 | ||
assert ctx.bar == 'qux' | ||
# Make some modifications | ||
ctx.foo += 1 | ||
ctx.bar = 'baz' | ||
"# | ||
); | ||
}); | ||
|
||
ctx.populate_from_extensions(&Extensions::new()); | ||
|
||
Python::with_gil(|py| { | ||
py_run!( | ||
py, | ||
ctx, | ||
r#" | ||
# Make sure we are preserving any modifications | ||
assert ctx.foo == 43 | ||
assert ctx.bar == 'baz' | ||
"# | ||
); | ||
}); | ||
|
||
Ok(()) | ||
} | ||
|
||
#[test] | ||
fn works_with_none() -> PyResult<()> { | ||
// Users can set context to `None` by explicity or implicitly by not providing a custom context class, | ||
// it shouldn't be fail in that case. | ||
|
||
pyo3::prepare_freethreaded_python(); | ||
|
||
let ctx = get_context("ctx = None"); | ||
Python::with_gil(|py| { | ||
py_run!(py, ctx, "assert ctx is None"); | ||
}); | ||
|
||
Ok(()) | ||
} | ||
} |
Oops, something went wrong.