use pyo3::prelude::*;
use pyo3::types::{PyDict, PyAny};
use crate::graph::BulkLoader as RustBulkLoader;
use crate::types::{Node, Edge, PropertyValue};
use super::to_py_result;
use uuid::Uuid;
#[pyclass(name = "BulkLoader")]
pub struct PyBulkLoader {
inner: Option<RustBulkLoader>,
}
#[pymethods]
impl PyBulkLoader {
fn add_node(
&mut self,
label: &str,
properties: &Bound<PyDict>,
py: Python,
) -> PyResult<String> {
let loader = self.inner.as_mut().ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"BulkLoader already finished. Create a new one."
)
})?;
let mut node = Node::new(label);
for (key, value) in properties {
let key_str = key.extract::<String>()?;
let prop_value = python_to_property_value(py, value.into())?;
node = node.with_property(key_str, prop_value);
}
let node_id = node.id;
let result = crate::python::runtime::block_on(py, async {
loader.add_node(node).await
});
to_py_result(result)?;
Ok(node_id.to_string())
}
fn add_edge(
&mut self,
py: Python<'_>,
source_id: &str,
target_id: &str,
label: &str,
) -> PyResult<()> {
let loader = self.inner.as_mut().ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"BulkLoader already finished"
)
})?;
let source = Uuid::parse_str(source_id)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Invalid source UUID: {}", e)
))?;
let target = Uuid::parse_str(target_id)
.map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(
format!("Invalid target UUID: {}", e)
))?;
let edge = Edge::new(source, target, label);
let result = crate::python::runtime::block_on(py, async {
loader.add_edge(edge).await
});
to_py_result(result)
}
fn finish(&mut self, py: Python) -> PyResult<Py<PyAny>> {
let loader = self.inner.take().ok_or_else(|| {
PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"BulkLoader already finished"
)
})?;
let stats = crate::python::runtime::block_on(py, async {
loader.finish().await
});
let stats = to_py_result(stats)?;
let dict = PyDict::new(py);
dict.set_item("nodes", stats.nodes_inserted)?;
dict.set_item("edges", stats.edges_inserted)?;
dict.set_item("duration_secs", stats.duration.as_secs_f64())?;
dict.set_item("nodes_per_second", stats.nodes_per_second)?;
Ok(dict.into())
}
fn __repr__(&self) -> String {
if self.inner.is_some() {
"<BulkLoader: active>".to_string()
} else {
"<BulkLoader: finished>".to_string()
}
}
fn __enter__(slf: Py<Self>) -> Py<Self> {
slf
}
fn __exit__(
&mut self,
py: Python,
_exc_type: &Bound<PyAny>,
_exc_value: &Bound<PyAny>,
_traceback: &Bound<PyAny>,
) -> PyResult<bool> {
if self.inner.is_some() {
self.finish(py)?;
}
Ok(false)
}
}
impl PyBulkLoader {
pub(crate) fn new(loader: RustBulkLoader) -> PyResult<Self> {
Ok(PyBulkLoader {
inner: Some(loader),
})
}
}
fn python_to_property_value(py: Python, obj: Py<PyAny>) -> PyResult<PropertyValue> {
if let Ok(s) = obj.extract::<String>(py) {
return Ok(PropertyValue::String(s));
}
if let Ok(b) = obj.extract::<bool>(py) {
return Ok(PropertyValue::Bool(b));
}
if let Ok(i) = obj.extract::<i64>(py) {
return Ok(PropertyValue::Int(i));
}
if let Ok(f) = obj.extract::<f64>(py) {
return Ok(PropertyValue::Float(f));
}
if obj.is_none(py) {
return Ok(PropertyValue::String(String::new()));
}
let type_name = obj.bind(py).get_type().name()
.map(|n| n.to_string())
.unwrap_or_else(|_| "unknown".to_string());
Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
format!(
"Unsupported property type: {}. Supported: str, int, float, bool",
type_name
)
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_property_conversion() {
Python::attach(|py| {
let s: Py<PyAny> = "hello"
.into_pyobject(py)
.expect("string conversion must work")
.into_any()
.unbind();
let prop = python_to_property_value(py, s).unwrap();
assert!(matches!(prop, PropertyValue::String(_)));
let i: Py<PyAny> = 42_i64
.into_pyobject(py)
.expect("int conversion must work")
.into_any()
.unbind();
let prop = python_to_property_value(py, i).unwrap();
assert!(matches!(prop, PropertyValue::Int(42)));
let f: Py<PyAny> = 3.14_f64
.into_pyobject(py)
.expect("float conversion must work")
.into_any()
.unbind();
let prop = python_to_property_value(py, f).unwrap();
assert!(matches!(prop, PropertyValue::Float(_)));
let b: Py<PyAny> = true
.into_pyobject(py)
.expect("bool conversion must work")
.to_owned()
.into_any()
.unbind();
let prop = python_to_property_value(py, b).unwrap();
assert!(matches!(prop, PropertyValue::Bool(true)));
});
}
}