0

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()
Happy Machine
  • 987
  • 8
  • 30

0 Answers0