Skip to content

Commit d7a838e

Browse files
committed
outbound redis, config, and binary bodies
Signed-off-by: Joel Dice <[email protected]>
1 parent 0dee603 commit d7a838e

File tree

3 files changed

+139
-55
lines changed

3 files changed

+139
-55
lines changed

py/app.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
1-
from spin_http import Request, Response, send
1+
from spin_http import Request, Response, http_send
2+
from spin_redis import redis_get, redis_set
3+
from spin_config import config_get
24

35
def handle_request(request):
46
print(f"Got request URI: {request.uri}")
5-
response = send(Request("GET", "https://some-random-api.ml/facts/dog", [], None))
6-
print(f"Got dog fact: {response.body}")
7-
return Response(200, [("content-type", "text/plain")], f"Hello from Python! Got request URI: {request.uri}")
7+
8+
response = http_send(Request("GET", "https://some-random-api.ml/facts/dog", [], None))
9+
print(f"Got dog fact: {str(response.body, 'utf-8')}")
10+
11+
redis_address = config_get("redis_address")
12+
redis_set(redis_address, "foo", b"bar")
13+
value = redis_get(redis_address, "foo")
14+
assert value == b"bar", f"expected \"bar\", got \"{str(value, 'utf-8')}\""
15+
16+
return Response(200,
17+
[("content-type", "text/plain")],
18+
bytes(f"Hello from Python! Got request URI: {request.uri}", "utf-8"))

spin.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ version = "0.1.0"
77

88
[[component]]
99
id = "spin-webrtc"
10+
config = { redis_address = "redis://127.0.0.1:6379" }
1011
source = "target/wasm32-wasi/release/python-wasi-vfs-wizer.wasm"
1112
allowed_http_hosts = ["insecure:allow-all"]
1213
[component.trigger]

src/lib.rs

Lines changed: 123 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
#![deny(warnings)]
22

33
use {
4-
anyhow::{Error, Result},
4+
anyhow::{anyhow, Error, Result},
5+
bytes::Bytes,
56
http::{header::HeaderName, request, HeaderValue},
67
once_cell::unsync::OnceCell,
7-
pyo3::{exceptions::PyAssertionError, types::PyModule, PyErr, PyObject, PyResult, Python},
8+
pyo3::{
9+
exceptions::PyAssertionError,
10+
types::{PyBytes, PyModule},
11+
Py, PyErr, PyObject, PyResult, Python,
12+
},
813
spin_sdk::{
14+
config,
915
http::{Request, Response},
10-
outbound_http,
16+
outbound_http, redis,
1117
},
1218
std::{ops::Deref, str},
1319
};
@@ -16,6 +22,14 @@ thread_local! {
1622
static HANDLE_REQUEST: OnceCell<PyObject> = OnceCell::new();
1723
}
1824

25+
fn bytes(py: Python<'_>, src: &[u8]) -> PyResult<Py<PyBytes>> {
26+
Ok(PyBytes::new_with(py, src.len(), |dst| {
27+
dst.copy_from_slice(src);
28+
Ok(())
29+
})?
30+
.into())
31+
}
32+
1933
#[derive(Clone)]
2034
#[pyo3::pyclass]
2135
#[pyo3(name = "Request")]
@@ -26,9 +40,8 @@ struct HttpRequest {
2640
uri: String,
2741
#[pyo3(get, set)]
2842
headers: Vec<(String, String)>,
29-
// todo: this should be a byte slice, but make sure it gets converted to/from Python correctly
3043
#[pyo3(get, set)]
31-
body: Option<String>,
44+
body: Option<Py<PyBytes>>,
3245
}
3346

3447
#[pyo3::pymethods]
@@ -38,7 +51,7 @@ impl HttpRequest {
3851
method: String,
3952
uri: String,
4053
headers: Vec<(String, String)>,
41-
body: Option<String>,
54+
body: Option<Py<PyBytes>>,
4255
) -> Self {
4356
Self {
4457
method,
@@ -57,15 +70,14 @@ struct HttpResponse {
5770
status: u16,
5871
#[pyo3(get, set)]
5972
headers: Vec<(String, String)>,
60-
// todo: this should be a byte slice, but make sure it gets converted to/from Python correctly
6173
#[pyo3(get, set)]
62-
body: Option<String>,
74+
body: Option<Py<PyBytes>>,
6375
}
6476

6577
#[pyo3::pymethods]
6678
impl HttpResponse {
6779
#[new]
68-
fn new(status: u16, headers: Vec<(String, String)>, body: Option<String>) -> Self {
80+
fn new(status: u16, headers: Vec<(String, String)>, body: Option<Py<PyBytes>>) -> Self {
6981
Self {
7082
status,
7183
headers,
@@ -89,23 +101,31 @@ impl<T: std::error::Error + Send + Sync + 'static> From<T> for Anyhow {
89101
}
90102

91103
#[pyo3::pyfunction]
92-
fn send(request: HttpRequest) -> Result<HttpResponse, Anyhow> {
104+
#[pyo3(pass_module)]
105+
fn http_send(module: &PyModule, request: HttpRequest) -> PyResult<HttpResponse> {
93106
let mut builder = request::Builder::new()
94107
.method(request.method.deref())
95108
.uri(request.uri.deref());
96109

97110
if let Some(headers) = builder.headers_mut() {
98111
for (key, value) in &request.headers {
99112
headers.insert(
100-
HeaderName::from_bytes(key.as_bytes())?,
101-
HeaderValue::from_bytes(value.as_bytes())?,
113+
HeaderName::from_bytes(key.as_bytes()).map_err(Anyhow::from)?,
114+
HeaderValue::from_bytes(value.as_bytes()).map_err(Anyhow::from)?,
102115
);
103116
}
104117
}
105118

106119
let response = outbound_http::send_request(
107-
builder.body(request.body.map(|buffer| buffer.into_bytes().into()))?,
108-
)?;
120+
builder
121+
.body(
122+
request
123+
.body
124+
.map(|buffer| Bytes::copy_from_slice(buffer.as_bytes(module.py()))),
125+
)
126+
.map_err(Anyhow::from)?,
127+
)
128+
.map_err(Anyhow::from)?;
109129

110130
Ok(HttpResponse {
111131
status: response.status().as_u16(),
@@ -115,26 +135,66 @@ fn send(request: HttpRequest) -> Result<HttpResponse, Anyhow> {
115135
.map(|(key, value)| {
116136
Ok((
117137
key.as_str().to_owned(),
118-
str::from_utf8(value.as_bytes())?.to_owned(),
138+
str::from_utf8(value.as_bytes())
139+
.map_err(Anyhow::from)?
140+
.to_owned(),
119141
))
120142
})
121-
.collect::<Result<_, Anyhow>>()?,
143+
.collect::<PyResult<_>>()?,
122144
body: response
123145
.into_body()
124-
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned()),
146+
.as_deref()
147+
.map(|buffer| bytes(module.py(), buffer))
148+
.transpose()?,
125149
})
126150
}
127151

128152
#[pyo3::pymodule]
129153
#[pyo3(name = "spin_http")]
130154
fn spin_http_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> {
131-
module.add_function(pyo3::wrap_pyfunction!(send, module)?)?;
155+
module.add_function(pyo3::wrap_pyfunction!(http_send, module)?)?;
132156
module.add_class::<HttpRequest>()?;
133157
module.add_class::<HttpResponse>()
134158
}
135159

160+
#[pyo3::pyfunction]
161+
#[pyo3(pass_module)]
162+
fn redis_get(module: &PyModule, address: String, key: String) -> PyResult<Py<PyBytes>> {
163+
bytes(
164+
module.py(),
165+
&redis::get(&address, &key)
166+
.map_err(|_| Anyhow(anyhow!("Error executing Redis get command")))?,
167+
)
168+
}
169+
170+
#[pyo3::pyfunction]
171+
fn redis_set(address: String, key: String, value: &PyBytes) -> PyResult<()> {
172+
redis::set(&address, &key, value.as_bytes())
173+
.map_err(|_| PyErr::from(Anyhow(anyhow!("Error executing Redis set command"))))
174+
}
175+
176+
#[pyo3::pymodule]
177+
#[pyo3(name = "spin_redis")]
178+
fn spin_redis_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> {
179+
module.add_function(pyo3::wrap_pyfunction!(redis_get, module)?)?;
180+
module.add_function(pyo3::wrap_pyfunction!(redis_set, module)?)
181+
}
182+
183+
#[pyo3::pyfunction]
184+
fn config_get(key: String) -> Result<String, Anyhow> {
185+
config::get(&key).map_err(Anyhow::from)
186+
}
187+
188+
#[pyo3::pymodule]
189+
#[pyo3(name = "spin_config")]
190+
fn spin_config_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> {
191+
module.add_function(pyo3::wrap_pyfunction!(config_get, module)?)
192+
}
193+
136194
fn do_init() -> Result<()> {
137195
pyo3::append_to_inittab!(spin_http_module);
196+
pyo3::append_to_inittab!(spin_redis_module);
197+
pyo3::append_to_inittab!(spin_config_module);
138198

139199
pyo3::prepare_freethreaded_python();
140200

@@ -157,45 +217,57 @@ pub extern "C" fn init() {
157217

158218
#[spin_sdk::http_component]
159219
fn handle(request: Request) -> Result<Response> {
160-
let uri = request.uri().to_string();
161-
let request = HttpRequest {
162-
method: request.method().as_str().to_owned(),
163-
uri,
164-
headers: request
165-
.headers()
166-
.iter()
167-
.map(|(k, v)| {
168-
Ok((
169-
k.as_str().to_owned(),
170-
str::from_utf8(v.as_bytes())?.to_owned(),
171-
))
172-
})
173-
.collect::<Result<_>>()?,
174-
body: request
175-
.body()
176-
.as_ref()
177-
.map(|bytes| Ok::<_, Error>(str::from_utf8(bytes)?.to_owned()))
178-
.transpose()?,
179-
};
220+
Python::with_gil(|py| {
221+
let uri = request.uri().to_string();
180222

181-
let response = Python::with_gil(|py| {
182-
HANDLE_REQUEST.with(|cell| {
223+
let request = HttpRequest {
224+
method: request.method().as_str().to_owned(),
225+
uri,
226+
headers: request
227+
.headers()
228+
.iter()
229+
.map(|(k, v)| {
230+
Ok((
231+
k.as_str().to_owned(),
232+
str::from_utf8(v.as_bytes())
233+
.map_err(Anyhow::from)?
234+
.to_owned(),
235+
))
236+
})
237+
.collect::<PyResult<_>>()?,
238+
body: request
239+
.body()
240+
.as_deref()
241+
.map(|buffer| bytes(py, buffer))
242+
.transpose()?,
243+
};
244+
245+
let response = HANDLE_REQUEST.with(|cell| {
183246
cell.get()
184247
.unwrap()
185248
.call1(py, (request,))?
186249
.extract::<HttpResponse>(py)
187-
})
188-
})?;
250+
})?;
189251

190-
let mut builder = http::Response::builder().status(response.status);
191-
if let Some(headers) = builder.headers_mut() {
192-
for (key, value) in &response.headers {
193-
headers.insert(
194-
HeaderName::try_from(key.deref())?,
195-
HeaderValue::from_bytes(value.as_bytes())?,
196-
);
252+
let mut builder = http::Response::builder().status(response.status);
253+
if let Some(headers) = builder.headers_mut() {
254+
for (key, value) in &response.headers {
255+
headers.insert(
256+
HeaderName::try_from(key.deref()).map_err(Anyhow::from)?,
257+
HeaderValue::from_bytes(value.as_bytes()).map_err(Anyhow::from)?,
258+
);
259+
}
197260
}
198-
}
199261

200-
Ok(builder.body(response.body.map(|buffer| buffer.into()))?)
262+
Ok::<_, PyErr>(
263+
builder
264+
.body(
265+
response
266+
.body
267+
.map(|buffer| Bytes::copy_from_slice(buffer.as_bytes(py))),
268+
)
269+
.map_err(Anyhow::from)?,
270+
)
271+
})
272+
.map_err(Error::from)
201273
}

0 commit comments

Comments
 (0)