Thanks in advance to @brunorijsman for his excellent self answered example of callbacks between rust and python using py03 How to pass a Rust function as a callback to Python using pyo3, which my code is heavily influenced by.
I'm trying to interrupt a GraphQL request using callbacks to a python file which is importing from rust using py03.
I've been able to use a Mutex to store shared state, but i'm stuck at how to store or share with Juniper a callback. I'm planning to eventually await the callbacks on both sides with async and futures, but at the moment I can't find a way to make the callback in rust callable from a Juniper resolver once its been registered...
GQLwrapper rust module lib.rs:
#[macro_use]
extern crate lazy_static;
use actix_cors::Cors;
use actix_web::{
middleware, route,
web::{self, Data},
App, HttpResponse, HttpServer, Responder,
};
use juniper::http::{GraphQLRequest};
use juniper::{EmptySubscription, FieldResult, RootNode, EmptyMutation};
use pyo3::prelude::*;
use std::thread;
use std::{io, sync::Arc};
mod schema;
use crate::schema::{Model, Params};
pub struct QueryRoot;
#[juniper::graphql_object]
impl QueryRoot {
fn model<'mdl>(&self, _params: Params) -> FieldResult<Model> {
// I WANT TO BE ABLE TO CALL THE CALLBACK HERE WITH MY GRAPHQLDATA!
Ok(Model {
prompt: _params.prompt.to_owned(),
})
}
}
type Schema = RootNode<'static, QueryRoot, EmptyMutation, EmptySubscription>;
fn create_schema() -> Schema {
Schema::new(QueryRoot {}, EmptyMutation::new(), EmptySubscription::new())
}
/// GraphQL endpoint
#[route("/graphql", method = "GET", method = "POST")]
async fn graphql(st: web::Data<Schema>, data: web::Json<GraphQLRequest>) -> impl Responder {
let user = data.execute(&st, &()).await;
HttpResponse::Ok().json(user)
}
#[actix_web::main]
async fn main() -> io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
let schema = Arc::new(create_schema());
HttpServer::new(move || {
#[pyfunction]
fn init() -> PyResult<String> {
thread::spawn(move || main());
Ok("GQL server started...".to_string())
}
#[pyclass]
struct Callback {
#[allow(dead_code)]
callback_function: Box<dyn Fn(&PyAny) -> PyResult<()> + Send>,
}
#[pymethods]
impl Callback {
fn __call__(&self, python_api: &PyAny) -> PyResult<()> {
(self.callback_function)(python_api)
}
}
#[pyfunction]
fn rust_register_callback(python_api: &PyAny) -> PyResult<()> {
// this registers a python callback which will in turn be used to send rust_callback
// along with a message when a request is recieved.
Python::with_gil(|py| {
// THIS IS THE CALLBACK I WANT TO BE ABLE TO CALL FROM THE RESOLVER...
let callback = Box::new(Callback {
callback_function: Box::new(move |python_api| {
rust_callback(python_api)
}),
});
println!("Rust: register callback");
python_api
.getattr("set_response_callback")?
.call1((callback.into_py(py),"data piped from request".to_string()))?;
Ok(())
})
}
fn rust_callback(message: &PyAny) -> PyResult<()> {
// This callback will return a message from python and add to the GraphQL response.
// I need to be able to ultimately await this and use it to set state that will be passed back
println!("Rust: rust_callback");
println!("Rust: Message={}", message);
Ok(())
}
#[pymodule]
#[pyo3(name = "GQLwrapper")]
fn GQLwrapper(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(init, m)?)?;
m.add_function(wrap_pyfunction!(rust_register_callback, m)?)?;
m.add_class::<Callback>()?;
Ok(())
}
App::new()
.app_data(Data::from(schema.clone()))
.service(graphql)
.wrap(Cors::permissive())
.wrap(middleware::Logger::default())
})
.workers(2)
.bind("127.0.0.1:8080")?
.run()
.await
}
Python main.py:
from lib.wrapper import graphql_wrapper
import time
def runner():
print("Python: doing some work")
graphql_wrapper.call_response_callback()
graphql_wrapper.set_fn_to_call(runner)
time.sleep(5000)
python wrapper.py:
import GQLwrapper
class PythonApi:
def __init__(self):
self.response_callback = None
self.python_callback = None
GQLwrapper.init()
def set_response_callback(self, callback, data):
self.response_callback = callback
print(f'Python: Message={data}')
self.python_callback()
def call_response_callback(self):
assert self.response_callback is not None
self.response_callback("data to add back into response")
def set_fn_to_call(self, callback):
self.python_callback = callback
GQLwrapper.rust_register_callback(self)
graphql_wrapper = PythonApi()