Skip to main content

graphrecords_python/graphrecord/
mod.rs

1#![allow(clippy::new_without_default, clippy::significant_drop_tightening)]
2
3pub mod attribute;
4mod borrowed;
5pub mod connector;
6pub mod datatype;
7pub mod errors;
8pub mod overview;
9pub mod plugins;
10pub mod schema;
11pub mod traits;
12pub mod value;
13
14use crate::{
15    conversion_lut::ConversionLut,
16    graphrecord::{
17        overview::{PyGroupOverview, PyOverview},
18        plugins::PyPlugin,
19    },
20    querying::PyOperand,
21};
22use attribute::PyGraphRecordAttribute;
23use borrowed::BorrowedGraphRecord;
24use connector::PyConnector;
25use errors::PyGraphRecordError;
26use graphrecords_core::{
27    errors::{ConversionError, GraphRecordError},
28    graphrecord::{
29        AttributeMap, EdgeDataFrameInput, EdgeIndex, GraphRecord, GraphRecordAttribute,
30        GraphRecordValue, Group, NodeDataFrameInput, connector::ConnectedGraphRecord,
31        plugins::Plugin,
32    },
33    prelude::NodeIndex,
34};
35use graphrecords_overview::{GroupOverviewable, Overviewable};
36use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
37use pyo3::{
38    exceptions::PyRuntimeError,
39    prelude::*,
40    types::{PyBytes, PyDict, PyFunction},
41};
42use pyo3_polars::PyDataFrame;
43use schema::PySchema;
44use std::{
45    collections::HashMap,
46    ops::{Deref, DerefMut},
47    ptr::NonNull,
48};
49use traits::DeepInto;
50use value::PyGraphRecordValue;
51
52pub type PyAttributes = HashMap<PyGraphRecordAttribute, PyGraphRecordValue>;
53pub type PyGroup = PyGraphRecordAttribute;
54pub type PyPluginName = PyGraphRecordAttribute;
55pub type PyNodeIndex = PyGraphRecordAttribute;
56pub type PyEdgeIndex = EdgeIndex;
57type Lut<T> = ConversionLut<usize, fn(&Bound<'_, PyAny>) -> PyResult<T>>;
58
59#[pyclass(frozen)]
60#[derive(Debug)]
61pub struct PyGraphRecord {
62    inner: PyGraphRecordInner,
63}
64
65#[derive(Debug)]
66#[allow(clippy::large_enum_variant)]
67enum PyGraphRecordInner {
68    Owned(RwLock<GraphRecord>),
69    Connected(RwLock<ConnectedGraphRecord<PyConnector>>),
70    Borrowed(BorrowedGraphRecord),
71}
72
73pub(crate) enum InnerRef<'a> {
74    Owned(RwLockReadGuard<'a, GraphRecord>),
75    Connected(RwLockReadGuard<'a, ConnectedGraphRecord<PyConnector>>),
76    Borrowed(RwLockReadGuard<'a, Option<NonNull<GraphRecord>>>),
77}
78
79impl Deref for InnerRef<'_> {
80    type Target = GraphRecord;
81
82    fn deref(&self) -> &GraphRecord {
83        match self {
84            InnerRef::Owned(guard) => guard,
85            InnerRef::Connected(guard) => guard,
86            // SAFETY: The guard is only constructed after checking `is_some()` in `inner()`.
87            // The pointer is valid for the duration of the `scope()`/`scope_mut()` call
88            // because the scope's Drop guard needs a write lock to clear it, and this read
89            // guard prevents that.
90            InnerRef::Borrowed(guard) => unsafe {
91                guard
92                    .expect("Borrowed pointer must be Some when InnerRef is alive")
93                    .as_ref()
94            },
95        }
96    }
97}
98
99pub(crate) enum InnerRefMut<'a> {
100    Owned(RwLockWriteGuard<'a, GraphRecord>),
101    Connected(RwLockWriteGuard<'a, ConnectedGraphRecord<PyConnector>>),
102    Borrowed(RwLockWriteGuard<'a, Option<NonNull<GraphRecord>>>),
103}
104
105impl Deref for InnerRefMut<'_> {
106    type Target = GraphRecord;
107
108    fn deref(&self) -> &GraphRecord {
109        match self {
110            InnerRefMut::Owned(guard) => guard,
111            InnerRefMut::Connected(guard) => guard,
112            // SAFETY: Same as `InnerRef::Borrowed`. Pointer was checked `is_some()` in
113            // `inner_mut()`, and the write guard keeps the scope's Drop from clearing it.
114            // Additionally, `inner_mut()` has already verified `is_mutable()` is true.
115            InnerRefMut::Borrowed(guard) => unsafe {
116                guard
117                    .expect("Borrowed pointer must be Some when InnerRefMut is alive")
118                    .as_ref()
119            },
120        }
121    }
122}
123
124impl DerefMut for InnerRefMut<'_> {
125    fn deref_mut(&mut self) -> &mut GraphRecord {
126        match self {
127            InnerRefMut::Owned(guard) => &mut *guard,
128            InnerRefMut::Connected(guard) => &mut *guard,
129            // SAFETY: Same as above, plus: the write guard ensures exclusive access to the
130            // pointer, so creating `&mut GraphRecord` is sound. The original `scope_mut()`
131            // call holds `&mut GraphRecord`, guaranteeing no other references to the pointee
132            // exist outside this lock. `inner_mut()` has verified `is_mutable()` is true,
133            // ensuring this path is only reachable for pointers originating from `&mut`.
134            InnerRefMut::Borrowed(guard) => unsafe {
135                guard
136                    .expect("Borrowed pointer must be Some when InnerRefMut is alive")
137                    .as_mut()
138            },
139        }
140    }
141}
142
143impl Clone for PyGraphRecord {
144    fn clone(&self) -> Self {
145        match &self.inner {
146            PyGraphRecordInner::Owned(lock) => Self {
147                inner: PyGraphRecordInner::Owned(RwLock::new(lock.read().clone())),
148            },
149            PyGraphRecordInner::Connected(lock) => Self {
150                inner: PyGraphRecordInner::Connected(RwLock::new(lock.read().clone())),
151            },
152            PyGraphRecordInner::Borrowed(_) => Self {
153                inner: PyGraphRecordInner::Borrowed(BorrowedGraphRecord::dead()),
154            },
155        }
156    }
157}
158
159impl PyGraphRecord {
160    pub(crate) fn inner(&self) -> PyResult<InnerRef<'_>> {
161        match &self.inner {
162            PyGraphRecordInner::Owned(lock) => Ok(InnerRef::Owned(lock.read())),
163            PyGraphRecordInner::Connected(lock) => Ok(InnerRef::Connected(lock.read())),
164            PyGraphRecordInner::Borrowed(borrowed) => {
165                let guard = borrowed.read();
166                if guard.is_some() {
167                    Ok(InnerRef::Borrowed(guard))
168                } else {
169                    Err(PyRuntimeError::new_err(
170                        "GraphRecord reference is no longer valid (used outside callback scope)",
171                    ))
172                }
173            }
174        }
175    }
176
177    pub(crate) fn connected(
178        &self,
179    ) -> PyResult<RwLockWriteGuard<'_, ConnectedGraphRecord<PyConnector>>> {
180        match &self.inner {
181            PyGraphRecordInner::Connected(lock) => Ok(lock.write()),
182            _ => Err(PyRuntimeError::new_err(
183                "GraphRecord has no connector attached",
184            )),
185        }
186    }
187
188    pub(crate) fn inner_mut(&self) -> PyResult<InnerRefMut<'_>> {
189        match &self.inner {
190            PyGraphRecordInner::Owned(lock) => Ok(InnerRefMut::Owned(lock.write())),
191            PyGraphRecordInner::Connected(lock) => Ok(InnerRefMut::Connected(lock.write())),
192            PyGraphRecordInner::Borrowed(borrowed) => {
193                if !borrowed.is_mutable() {
194                    return Err(PyRuntimeError::new_err("GraphRecord is read-only"));
195                }
196                let guard = borrowed.write();
197                if guard.is_some() {
198                    Ok(InnerRefMut::Borrowed(guard))
199                } else {
200                    Err(PyRuntimeError::new_err(
201                        "GraphRecord reference is no longer valid (used outside callback scope)",
202                    ))
203                }
204            }
205        }
206    }
207}
208
209impl From<GraphRecord> for PyGraphRecord {
210    fn from(value: GraphRecord) -> Self {
211        Self {
212            inner: PyGraphRecordInner::Owned(RwLock::new(value)),
213        }
214    }
215}
216
217impl From<ConnectedGraphRecord<PyConnector>> for PyGraphRecord {
218    fn from(value: ConnectedGraphRecord<PyConnector>) -> Self {
219        Self {
220            inner: PyGraphRecordInner::Connected(RwLock::new(value)),
221        }
222    }
223}
224
225impl TryFrom<PyGraphRecord> for GraphRecord {
226    type Error = PyErr;
227
228    fn try_from(value: PyGraphRecord) -> PyResult<Self> {
229        match value.inner {
230            PyGraphRecordInner::Owned(lock) => Ok(lock.into_inner()),
231            PyGraphRecordInner::Connected(lock) => Ok(lock.into_inner().into()),
232            PyGraphRecordInner::Borrowed(_) => Err(PyRuntimeError::new_err(
233                "Cannot convert a borrowed PyGraphRecord into an owned GraphRecord",
234            )),
235        }
236    }
237}
238
239#[pymethods]
240impl PyGraphRecord {
241    #[new]
242    pub fn new() -> Self {
243        GraphRecord::new().into()
244    }
245
246    pub fn _to_bytes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
247        let bytes = bincode::serialize(&*self.inner()?)
248            .map_err(|_| GraphRecordError::Conversion(ConversionError::BinarySerialization))
249            .map_err(PyGraphRecordError::from)?;
250
251        Ok(PyBytes::new(py, &bytes))
252    }
253
254    #[staticmethod]
255    pub fn _from_bytes(data: &Bound<'_, PyBytes>) -> PyResult<Self> {
256        let graphrecord: GraphRecord = bincode::deserialize(data.as_bytes())
257            .map_err(|_| GraphRecordError::Conversion(ConversionError::BinaryDeserialization))
258            .map_err(PyGraphRecordError::from)?;
259
260        Ok(graphrecord.into())
261    }
262
263    #[staticmethod]
264    pub fn with_schema(schema: PySchema) -> Self {
265        GraphRecord::with_schema(schema.into()).into()
266    }
267
268    #[staticmethod]
269    pub fn with_plugins(plugins: HashMap<PyPluginName, Py<PyAny>>) -> PyResult<Self> {
270        let plugins = plugins
271            .into_iter()
272            .map(|(name, plugin)| {
273                (
274                    name.into(),
275                    Box::new(PyPlugin::new(plugin)) as Box<dyn Plugin>,
276                )
277            })
278            .collect();
279
280        let graphrecord = GraphRecord::with_plugins(plugins).map_err(PyGraphRecordError::from)?;
281
282        Ok(graphrecord.into())
283    }
284
285    #[staticmethod]
286    #[pyo3(signature = (nodes, edges=None, schema=None))]
287    pub fn from_tuples(
288        nodes: Vec<(PyNodeIndex, PyAttributes)>,
289        edges: Option<Vec<(PyNodeIndex, PyNodeIndex, PyAttributes)>>,
290        schema: Option<PySchema>,
291    ) -> PyResult<Self> {
292        Ok(
293            GraphRecord::from_tuples(nodes.deep_into(), edges.deep_into(), schema.map(Into::into))
294                .map_err(PyGraphRecordError::from)?
295                .into(),
296        )
297    }
298
299    #[staticmethod]
300    #[pyo3(signature = (nodes_dataframes, edges_dataframes, schema=None))]
301    pub fn from_dataframes(
302        nodes_dataframes: Vec<(PyDataFrame, String)>,
303        edges_dataframes: Vec<(PyDataFrame, String, String)>,
304        schema: Option<PySchema>,
305    ) -> PyResult<Self> {
306        Ok(
307            GraphRecord::from_dataframes(
308                nodes_dataframes,
309                edges_dataframes,
310                schema.map(Into::into),
311            )
312            .map_err(PyGraphRecordError::from)?
313            .into(),
314        )
315    }
316
317    #[staticmethod]
318    #[pyo3(signature = (nodes_dataframes, schema=None))]
319    pub fn from_nodes_dataframes(
320        nodes_dataframes: Vec<(PyDataFrame, String)>,
321        schema: Option<PySchema>,
322    ) -> PyResult<Self> {
323        Ok(
324            GraphRecord::from_nodes_dataframes(nodes_dataframes, schema.map(Into::into))
325                .map_err(PyGraphRecordError::from)?
326                .into(),
327        )
328    }
329
330    #[staticmethod]
331    pub fn from_ron(path: &str) -> PyResult<Self> {
332        Ok(GraphRecord::from_ron(path)
333            .map_err(PyGraphRecordError::from)?
334            .into())
335    }
336
337    #[staticmethod]
338    pub fn with_connector(connector: Py<PyAny>) -> PyResult<Self> {
339        let connected = ConnectedGraphRecord::new(PyConnector::new(connector))
340            .map_err(PyGraphRecordError::from)?;
341
342        Ok(connected.into())
343    }
344
345    pub fn to_ron(&self, path: &str) -> PyResult<()> {
346        Ok(self
347            .inner()?
348            .to_ron(path)
349            .map_err(PyGraphRecordError::from)?)
350    }
351
352    #[allow(clippy::missing_panics_doc, reason = "infallible")]
353    pub fn to_dataframes(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
354        let export = self
355            .inner()?
356            .to_dataframes()
357            .map_err(PyGraphRecordError::from)?;
358
359        let outer_dict = PyDict::new(py);
360        let inner_dict = PyDict::new(py);
361
362        for (group, group_export) in export.groups {
363            let group_dict = PyDict::new(py);
364
365            let nodes_df = PyDataFrame(group_export.nodes);
366            group_dict
367                .set_item("nodes", nodes_df)
368                .expect("Setting item must succeed");
369
370            let edges_df = PyDataFrame(group_export.edges);
371            group_dict
372                .set_item("edges", edges_df)
373                .expect("Setting item must succeed");
374
375            inner_dict
376                .set_item(PyGraphRecordAttribute::from(group), group_dict)
377                .expect("Setting item must succeed");
378        }
379
380        outer_dict
381            .set_item("groups", inner_dict)
382            .expect("Setting item must succeed");
383
384        let ungrouped_dict = PyDict::new(py);
385
386        let nodes_df = PyDataFrame(export.ungrouped.nodes);
387        ungrouped_dict
388            .set_item("nodes", nodes_df)
389            .expect("Setting item must succeed");
390
391        let edges_df = PyDataFrame(export.ungrouped.edges);
392        ungrouped_dict
393            .set_item("edges", edges_df)
394            .expect("Setting item must succeed");
395
396        outer_dict
397            .set_item("ungrouped", ungrouped_dict)
398            .expect("Setting item must succeed");
399
400        Ok(outer_dict.into())
401    }
402
403    pub fn disconnect(&self) -> PyResult<Self> {
404        let graphrecord = self
405            .connected()?
406            .clone()
407            .disconnect()
408            .map_err(PyGraphRecordError::from)?;
409
410        Ok(graphrecord.into())
411    }
412
413    pub fn ingest(&self, data: Py<PyAny>) -> PyResult<()> {
414        self.connected()?
415            .ingest(data)
416            .map_err(PyGraphRecordError::from)?;
417
418        Ok(())
419    }
420
421    pub fn export(&self) -> PyResult<Py<PyAny>> {
422        let data = self
423            .connected()?
424            .export()
425            .map_err(PyGraphRecordError::from)?;
426
427        Ok(data)
428    }
429
430    pub fn add_plugin(&self, name: PyPluginName, plugin: Py<PyAny>) -> PyResult<()> {
431        let mut graphrecord = self.inner_mut()?;
432
433        graphrecord
434            .add_plugin(name.into(), Box::new(PyPlugin::new(plugin)))
435            .map_err(PyGraphRecordError::from)?;
436
437        Ok(())
438    }
439
440    pub fn remove_plugin(&self, name: PyPluginName) -> PyResult<()> {
441        let mut graphrecord = self.inner_mut()?;
442
443        graphrecord
444            .remove_plugin(&name.into())
445            .map_err(PyGraphRecordError::from)?;
446
447        Ok(())
448    }
449
450    #[getter]
451    pub fn plugins(&self) -> PyResult<Vec<PyPluginName>> {
452        Ok(self
453            .inner()?
454            .plugin_names()
455            .cloned()
456            .map(std::convert::Into::into)
457            .collect())
458    }
459
460    pub fn get_schema(&self) -> PyResult<PySchema> {
461        Ok(self.inner()?.get_schema().clone().into())
462    }
463
464    #[pyo3(signature = (schema, bypass_plugins=false))]
465    pub fn set_schema(&self, schema: PySchema, bypass_plugins: bool) -> PyResult<()> {
466        let mut graphrecord = self.inner_mut()?;
467
468        if bypass_plugins {
469            Ok(graphrecord
470                .set_schema_bypass_plugins(schema.into())
471                .map_err(PyGraphRecordError::from)?)
472        } else {
473            Ok(graphrecord
474                .set_schema(schema.into())
475                .map_err(PyGraphRecordError::from)?)
476        }
477    }
478
479    #[pyo3(signature = (bypass_plugins=false))]
480    pub fn freeze_schema(&self, bypass_plugins: bool) -> PyResult<()> {
481        let mut graphrecord = self.inner_mut()?;
482
483        if bypass_plugins {
484            Ok(graphrecord
485                .freeze_schema_bypass_plugins()
486                .map_err(PyGraphRecordError::from)?)
487        } else {
488            Ok(graphrecord
489                .freeze_schema()
490                .map_err(PyGraphRecordError::from)?)
491        }
492    }
493
494    #[pyo3(signature = (bypass_plugins=false))]
495    pub fn unfreeze_schema(&self, bypass_plugins: bool) -> PyResult<()> {
496        let mut graphrecord = self.inner_mut()?;
497
498        if bypass_plugins {
499            Ok(graphrecord
500                .unfreeze_schema_bypass_plugins()
501                .map_err(PyGraphRecordError::from)?)
502        } else {
503            Ok(graphrecord
504                .unfreeze_schema()
505                .map_err(PyGraphRecordError::from)?)
506        }
507    }
508
509    #[getter]
510    pub fn nodes(&self) -> PyResult<Vec<PyNodeIndex>> {
511        Ok(self
512            .inner()?
513            .node_indices()
514            .map(|node_index| node_index.clone().into())
515            .collect())
516    }
517
518    pub fn node(
519        &self,
520        node_index: Vec<PyNodeIndex>,
521    ) -> PyResult<HashMap<PyNodeIndex, PyAttributes>> {
522        let graphrecord = self.inner()?;
523
524        node_index
525            .into_iter()
526            .map(|node_index| {
527                let node_attributes = graphrecord
528                    .node_attributes(&node_index)
529                    .map_err(PyGraphRecordError::from)?;
530
531                Ok((node_index, node_attributes.deep_into()))
532            })
533            .collect()
534    }
535
536    #[getter]
537    pub fn edges(&self) -> PyResult<Vec<EdgeIndex>> {
538        Ok(self.inner()?.edge_indices().copied().collect())
539    }
540
541    pub fn edge(&self, edge_index: Vec<EdgeIndex>) -> PyResult<HashMap<EdgeIndex, PyAttributes>> {
542        let graphrecord = self.inner()?;
543
544        edge_index
545            .into_iter()
546            .map(|edge_index| {
547                let edge_attributes = graphrecord
548                    .edge_attributes(&edge_index)
549                    .map_err(PyGraphRecordError::from)?;
550
551                Ok((edge_index, edge_attributes.deep_into()))
552            })
553            .collect()
554    }
555
556    #[getter]
557    pub fn groups(&self) -> PyResult<Vec<PyGroup>> {
558        Ok(self
559            .inner()?
560            .groups()
561            .map(|group| group.clone().into())
562            .collect())
563    }
564
565    pub fn outgoing_edges(
566        &self,
567        node_index: Vec<PyNodeIndex>,
568    ) -> PyResult<HashMap<PyNodeIndex, Vec<EdgeIndex>>> {
569        let graphrecord = self.inner()?;
570
571        node_index
572            .into_iter()
573            .map(|node_index| {
574                let edges = graphrecord
575                    .outgoing_edges(&node_index)
576                    .map_err(PyGraphRecordError::from)?
577                    .copied()
578                    .collect();
579
580                Ok((node_index, edges))
581            })
582            .collect()
583    }
584
585    pub fn incoming_edges(
586        &self,
587        node_index: Vec<PyNodeIndex>,
588    ) -> PyResult<HashMap<PyNodeIndex, Vec<EdgeIndex>>> {
589        let graphrecord = self.inner()?;
590
591        node_index
592            .into_iter()
593            .map(|node_index| {
594                let edges = graphrecord
595                    .incoming_edges(&node_index)
596                    .map_err(PyGraphRecordError::from)?
597                    .copied()
598                    .collect();
599
600                Ok((node_index, edges))
601            })
602            .collect()
603    }
604
605    pub fn edge_endpoints(
606        &self,
607        edge_index: Vec<EdgeIndex>,
608    ) -> PyResult<HashMap<EdgeIndex, (PyNodeIndex, PyNodeIndex)>> {
609        let graphrecord = self.inner()?;
610
611        edge_index
612            .into_iter()
613            .map(|edge_index| {
614                let edge_endpoints = graphrecord
615                    .edge_endpoints(&edge_index)
616                    .map_err(PyGraphRecordError::from)?;
617
618                Ok((
619                    edge_index,
620                    (
621                        edge_endpoints.0.clone().into(),
622                        edge_endpoints.1.clone().into(),
623                    ),
624                ))
625            })
626            .collect()
627    }
628
629    pub fn edges_connecting(
630        &self,
631        source_node_indices: Vec<PyNodeIndex>,
632        target_node_indices: Vec<PyNodeIndex>,
633    ) -> PyResult<Vec<EdgeIndex>> {
634        let source_node_indices: Vec<GraphRecordAttribute> = source_node_indices.deep_into();
635        let target_node_indices: Vec<GraphRecordAttribute> = target_node_indices.deep_into();
636
637        Ok(self
638            .inner()?
639            .edges_connecting(
640                source_node_indices.iter().collect(),
641                target_node_indices.iter().collect(),
642            )
643            .copied()
644            .collect())
645    }
646
647    pub fn edges_connecting_undirected(
648        &self,
649        first_node_indices: Vec<PyNodeIndex>,
650        second_node_indices: Vec<PyNodeIndex>,
651    ) -> PyResult<Vec<EdgeIndex>> {
652        let first_node_indices: Vec<GraphRecordAttribute> = first_node_indices.deep_into();
653        let second_node_indices: Vec<GraphRecordAttribute> = second_node_indices.deep_into();
654
655        Ok(self
656            .inner()?
657            .edges_connecting_undirected(
658                first_node_indices.iter().collect(),
659                second_node_indices.iter().collect(),
660            )
661            .copied()
662            .collect())
663    }
664
665    #[pyo3(signature = (node_indices, bypass_plugins=false))]
666    pub fn remove_nodes(
667        &self,
668        node_indices: Vec<PyNodeIndex>,
669        bypass_plugins: bool,
670    ) -> PyResult<HashMap<PyNodeIndex, PyAttributes>> {
671        let mut graphrecord = self.inner_mut()?;
672
673        if bypass_plugins {
674            node_indices
675                .into_iter()
676                .map(|node_index| {
677                    let attributes = graphrecord
678                        .remove_node_bypass_plugins(&node_index)
679                        .map_err(PyGraphRecordError::from)?;
680                    Ok((node_index, attributes.deep_into()))
681                })
682                .collect()
683        } else {
684            node_indices
685                .into_iter()
686                .map(|node_index| {
687                    let attributes = graphrecord
688                        .remove_node(&node_index)
689                        .map_err(PyGraphRecordError::from)?;
690                    Ok((node_index, attributes.deep_into()))
691                })
692                .collect()
693        }
694    }
695
696    pub fn replace_node_attributes(
697        &self,
698        node_indices: Vec<PyNodeIndex>,
699        attributes: PyAttributes,
700    ) -> PyResult<()> {
701        let mut graphrecord = self.inner_mut()?;
702
703        let attributes: AttributeMap = attributes.deep_into();
704
705        for node_index in node_indices {
706            let mut current_attributes = graphrecord
707                .node_attributes_mut(&node_index)
708                .map_err(PyGraphRecordError::from)?;
709
710            current_attributes
711                .replace_attributes(attributes.clone())
712                .map_err(PyGraphRecordError::from)?;
713        }
714
715        Ok(())
716    }
717
718    pub fn update_node_attribute(
719        &self,
720        node_indices: Vec<PyNodeIndex>,
721        attribute: PyGraphRecordAttribute,
722        value: PyGraphRecordValue,
723    ) -> PyResult<()> {
724        let mut graphrecord = self.inner_mut()?;
725
726        let attribute: GraphRecordAttribute = attribute.into();
727        let value: GraphRecordValue = value.into();
728
729        for node_index in node_indices {
730            let mut node_attributes = graphrecord
731                .node_attributes_mut(&node_index)
732                .map_err(PyGraphRecordError::from)?;
733
734            node_attributes
735                .update_attribute(&attribute, value.clone())
736                .map_err(PyGraphRecordError::from)?;
737        }
738
739        Ok(())
740    }
741
742    pub fn remove_node_attribute(
743        &self,
744        node_indices: Vec<PyNodeIndex>,
745        attribute: PyGraphRecordAttribute,
746    ) -> PyResult<()> {
747        let mut graphrecord = self.inner_mut()?;
748
749        let attribute: GraphRecordAttribute = attribute.into();
750
751        for node_index in node_indices {
752            let mut node_attributes = graphrecord
753                .node_attributes_mut(&node_index)
754                .map_err(PyGraphRecordError::from)?;
755
756            node_attributes
757                .remove_attribute(&attribute)
758                .map_err(PyGraphRecordError::from)?;
759        }
760
761        Ok(())
762    }
763
764    #[pyo3(signature = (nodes, bypass_plugins=false))]
765    pub fn add_nodes(
766        &self,
767        nodes: Vec<(PyNodeIndex, PyAttributes)>,
768        bypass_plugins: bool,
769    ) -> PyResult<()> {
770        let mut graphrecord = self.inner_mut()?;
771
772        if bypass_plugins {
773            Ok(graphrecord
774                .add_nodes_bypass_plugins(nodes.deep_into())
775                .map_err(PyGraphRecordError::from)?)
776        } else {
777            Ok(graphrecord
778                .add_nodes(nodes.deep_into())
779                .map_err(PyGraphRecordError::from)?)
780        }
781    }
782
783    #[pyo3(signature = (nodes, group, bypass_plugins=false))]
784    pub fn add_nodes_with_group(
785        &self,
786        nodes: Vec<(PyNodeIndex, PyAttributes)>,
787        group: PyGroup,
788        bypass_plugins: bool,
789    ) -> PyResult<()> {
790        let mut graphrecord = self.inner_mut()?;
791
792        if bypass_plugins {
793            Ok(graphrecord
794                .add_nodes_with_group_bypass_plugins(nodes.deep_into(), group.into())
795                .map_err(PyGraphRecordError::from)?)
796        } else {
797            Ok(graphrecord
798                .add_nodes_with_group(nodes.deep_into(), group.into())
799                .map_err(PyGraphRecordError::from)?)
800        }
801    }
802
803    #[pyo3(signature = (nodes, groups, bypass_plugins=false))]
804    pub fn add_nodes_with_groups(
805        &self,
806        nodes: Vec<(PyNodeIndex, PyAttributes)>,
807        groups: Vec<PyGroup>,
808        bypass_plugins: bool,
809    ) -> PyResult<()> {
810        let mut graphrecord = self.inner_mut()?;
811        let groups: Vec<graphrecords_core::graphrecord::Group> = groups.deep_into();
812
813        if bypass_plugins {
814            Ok(graphrecord
815                .add_nodes_with_groups_bypass_plugins(nodes.deep_into(), &groups)
816                .map_err(PyGraphRecordError::from)?)
817        } else {
818            Ok(graphrecord
819                .add_nodes_with_groups(nodes.deep_into(), &groups)
820                .map_err(PyGraphRecordError::from)?)
821        }
822    }
823
824    #[pyo3(signature = (node_index, attributes, groups, bypass_plugins=false))]
825    pub fn add_node_with_groups(
826        &self,
827        node_index: PyNodeIndex,
828        attributes: PyAttributes,
829        groups: Vec<PyGroup>,
830        bypass_plugins: bool,
831    ) -> PyResult<()> {
832        let mut graphrecord = self.inner_mut()?;
833        let groups: Vec<graphrecords_core::graphrecord::Group> = groups.deep_into();
834
835        if bypass_plugins {
836            Ok(graphrecord
837                .add_node_with_groups_bypass_plugins(
838                    node_index.into(),
839                    attributes.deep_into(),
840                    &groups,
841                )
842                .map_err(PyGraphRecordError::from)?)
843        } else {
844            Ok(graphrecord
845                .add_node_with_groups(node_index.into(), attributes.deep_into(), &groups)
846                .map_err(PyGraphRecordError::from)?)
847        }
848    }
849
850    #[pyo3(signature = (nodes_dataframes, bypass_plugins=false))]
851    pub fn add_nodes_dataframes(
852        &self,
853        nodes_dataframes: Vec<(PyDataFrame, String)>,
854        bypass_plugins: bool,
855    ) -> PyResult<()> {
856        let mut graphrecord = self.inner_mut()?;
857
858        if bypass_plugins {
859            Ok(graphrecord
860                .add_nodes_dataframes_bypass_plugins(nodes_dataframes)
861                .map_err(PyGraphRecordError::from)?)
862        } else {
863            Ok(graphrecord
864                .add_nodes_dataframes(nodes_dataframes)
865                .map_err(PyGraphRecordError::from)?)
866        }
867    }
868
869    #[pyo3(signature = (nodes_dataframes, group, bypass_plugins=false))]
870    pub fn add_nodes_dataframes_with_group(
871        &self,
872        nodes_dataframes: Vec<(PyDataFrame, String)>,
873        group: PyGroup,
874        bypass_plugins: bool,
875    ) -> PyResult<()> {
876        let mut graphrecord = self.inner_mut()?;
877
878        if bypass_plugins {
879            Ok(graphrecord
880                .add_nodes_dataframes_with_group_bypass_plugins(nodes_dataframes, group.into())
881                .map_err(PyGraphRecordError::from)?)
882        } else {
883            Ok(graphrecord
884                .add_nodes_dataframes_with_group(nodes_dataframes, group.into())
885                .map_err(PyGraphRecordError::from)?)
886        }
887    }
888
889    #[pyo3(signature = (nodes_dataframes, groups, bypass_plugins=false))]
890    pub fn add_nodes_dataframes_with_groups(
891        &self,
892        nodes_dataframes: Vec<(PyDataFrame, String)>,
893        groups: Vec<PyGroup>,
894        bypass_plugins: bool,
895    ) -> PyResult<()> {
896        let mut graphrecord = self.inner_mut()?;
897        let groups: Vec<Group> = groups.deep_into();
898        let nodes_dataframes: Vec<NodeDataFrameInput> =
899            nodes_dataframes.into_iter().map(Into::into).collect();
900
901        if bypass_plugins {
902            Ok(graphrecord
903                .add_nodes_dataframes_with_groups_bypass_plugins(nodes_dataframes, &groups)
904                .map_err(PyGraphRecordError::from)?)
905        } else {
906            Ok(graphrecord
907                .add_nodes_dataframes_with_groups(nodes_dataframes, &groups)
908                .map_err(PyGraphRecordError::from)?)
909        }
910    }
911
912    #[pyo3(signature = (edge_indices, bypass_plugins=false))]
913    pub fn remove_edges(
914        &self,
915        edge_indices: Vec<EdgeIndex>,
916        bypass_plugins: bool,
917    ) -> PyResult<HashMap<EdgeIndex, PyAttributes>> {
918        let mut graphrecord = self.inner_mut()?;
919
920        if bypass_plugins {
921            edge_indices
922                .into_iter()
923                .map(|edge_index| {
924                    let attributes = graphrecord
925                        .remove_edge_bypass_plugins(&edge_index)
926                        .map_err(PyGraphRecordError::from)?;
927                    Ok((edge_index, attributes.deep_into()))
928                })
929                .collect()
930        } else {
931            edge_indices
932                .into_iter()
933                .map(|edge_index| {
934                    let attributes = graphrecord
935                        .remove_edge(&edge_index)
936                        .map_err(PyGraphRecordError::from)?;
937                    Ok((edge_index, attributes.deep_into()))
938                })
939                .collect()
940        }
941    }
942
943    pub fn replace_edge_attributes(
944        &self,
945        edge_indices: Vec<EdgeIndex>,
946        attributes: PyAttributes,
947    ) -> PyResult<()> {
948        let mut graphrecord = self.inner_mut()?;
949
950        let attributes: AttributeMap = attributes.deep_into();
951
952        for edge_index in edge_indices {
953            let mut current_attributes = graphrecord
954                .edge_attributes_mut(&edge_index)
955                .map_err(PyGraphRecordError::from)?;
956
957            current_attributes
958                .replace_attributes(attributes.clone())
959                .map_err(PyGraphRecordError::from)?;
960        }
961
962        Ok(())
963    }
964
965    pub fn update_edge_attribute(
966        &self,
967        edge_indices: Vec<EdgeIndex>,
968        attribute: PyGraphRecordAttribute,
969        value: PyGraphRecordValue,
970    ) -> PyResult<()> {
971        let mut graphrecord = self.inner_mut()?;
972
973        let attribute: GraphRecordAttribute = attribute.into();
974        let value: GraphRecordValue = value.into();
975
976        for edge_index in edge_indices {
977            let mut edge_attributes = graphrecord
978                .edge_attributes_mut(&edge_index)
979                .map_err(PyGraphRecordError::from)?;
980
981            edge_attributes
982                .update_attribute(&attribute, value.clone())
983                .map_err(PyGraphRecordError::from)?;
984        }
985
986        Ok(())
987    }
988
989    pub fn remove_edge_attribute(
990        &self,
991        edge_indices: Vec<EdgeIndex>,
992        attribute: PyGraphRecordAttribute,
993    ) -> PyResult<()> {
994        let mut graphrecord = self.inner_mut()?;
995
996        let attribute: GraphRecordAttribute = attribute.into();
997
998        for edge_index in edge_indices {
999            let mut edge_attributes = graphrecord
1000                .edge_attributes_mut(&edge_index)
1001                .map_err(PyGraphRecordError::from)?;
1002
1003            edge_attributes
1004                .remove_attribute(&attribute)
1005                .map_err(PyGraphRecordError::from)?;
1006        }
1007
1008        Ok(())
1009    }
1010
1011    #[pyo3(signature = (relations, bypass_plugins=false))]
1012    pub fn add_edges(
1013        &self,
1014        relations: Vec<(PyNodeIndex, PyNodeIndex, PyAttributes)>,
1015        bypass_plugins: bool,
1016    ) -> PyResult<Vec<EdgeIndex>> {
1017        let mut graphrecord = self.inner_mut()?;
1018
1019        if bypass_plugins {
1020            Ok(graphrecord
1021                .add_edges_bypass_plugins(relations.deep_into())
1022                .map_err(PyGraphRecordError::from)?)
1023        } else {
1024            Ok(graphrecord
1025                .add_edges(relations.deep_into())
1026                .map_err(PyGraphRecordError::from)?)
1027        }
1028    }
1029
1030    #[pyo3(signature = (relations, group, bypass_plugins=false))]
1031    pub fn add_edges_with_group(
1032        &self,
1033        relations: Vec<(PyNodeIndex, PyNodeIndex, PyAttributes)>,
1034        group: PyGroup,
1035        bypass_plugins: bool,
1036    ) -> PyResult<Vec<EdgeIndex>> {
1037        let mut graphrecord = self.inner_mut()?;
1038
1039        if bypass_plugins {
1040            Ok(graphrecord
1041                .add_edges_with_group_bypass_plugins(relations.deep_into(), &group)
1042                .map_err(PyGraphRecordError::from)?)
1043        } else {
1044            Ok(graphrecord
1045                .add_edges_with_group(relations.deep_into(), &group)
1046                .map_err(PyGraphRecordError::from)?)
1047        }
1048    }
1049
1050    #[pyo3(signature = (relations, groups, bypass_plugins=false))]
1051    pub fn add_edges_with_groups(
1052        &self,
1053        relations: Vec<(PyNodeIndex, PyNodeIndex, PyAttributes)>,
1054        groups: Vec<PyGroup>,
1055        bypass_plugins: bool,
1056    ) -> PyResult<Vec<EdgeIndex>> {
1057        let mut graphrecord = self.inner_mut()?;
1058        let groups: Vec<Group> = groups.deep_into();
1059
1060        if bypass_plugins {
1061            Ok(graphrecord
1062                .add_edges_with_groups_bypass_plugins(relations.deep_into(), &groups)
1063                .map_err(PyGraphRecordError::from)?)
1064        } else {
1065            Ok(graphrecord
1066                .add_edges_with_groups(relations.deep_into(), &groups)
1067                .map_err(PyGraphRecordError::from)?)
1068        }
1069    }
1070
1071    #[pyo3(signature = (source_node_index, target_node_index, attributes, groups, bypass_plugins=false))]
1072    pub fn add_edge_with_groups(
1073        &self,
1074        source_node_index: PyNodeIndex,
1075        target_node_index: PyNodeIndex,
1076        attributes: PyAttributes,
1077        groups: Vec<PyGroup>,
1078        bypass_plugins: bool,
1079    ) -> PyResult<EdgeIndex> {
1080        let mut graphrecord = self.inner_mut()?;
1081        let groups: Vec<graphrecords_core::graphrecord::Group> = groups.deep_into();
1082
1083        if bypass_plugins {
1084            Ok(graphrecord
1085                .add_edge_with_groups_bypass_plugins(
1086                    source_node_index.into(),
1087                    target_node_index.into(),
1088                    attributes.deep_into(),
1089                    &groups,
1090                )
1091                .map_err(PyGraphRecordError::from)?)
1092        } else {
1093            Ok(graphrecord
1094                .add_edge_with_groups(
1095                    source_node_index.into(),
1096                    target_node_index.into(),
1097                    attributes.deep_into(),
1098                    &groups,
1099                )
1100                .map_err(PyGraphRecordError::from)?)
1101        }
1102    }
1103
1104    #[pyo3(signature = (edges_dataframes, bypass_plugins=false))]
1105    pub fn add_edges_dataframes(
1106        &self,
1107        edges_dataframes: Vec<(PyDataFrame, String, String)>,
1108        bypass_plugins: bool,
1109    ) -> PyResult<Vec<EdgeIndex>> {
1110        let mut graphrecord = self.inner_mut()?;
1111
1112        if bypass_plugins {
1113            Ok(graphrecord
1114                .add_edges_dataframes_bypass_plugins(edges_dataframes)
1115                .map_err(PyGraphRecordError::from)?)
1116        } else {
1117            Ok(graphrecord
1118                .add_edges_dataframes(edges_dataframes)
1119                .map_err(PyGraphRecordError::from)?)
1120        }
1121    }
1122
1123    #[pyo3(signature = (edges_dataframes, group, bypass_plugins=false))]
1124    pub fn add_edges_dataframes_with_group(
1125        &self,
1126        edges_dataframes: Vec<(PyDataFrame, String, String)>,
1127        group: PyGroup,
1128        bypass_plugins: bool,
1129    ) -> PyResult<Vec<EdgeIndex>> {
1130        let mut graphrecord = self.inner_mut()?;
1131
1132        if bypass_plugins {
1133            Ok(graphrecord
1134                .add_edges_dataframes_with_group_bypass_plugins(edges_dataframes, &group)
1135                .map_err(PyGraphRecordError::from)?)
1136        } else {
1137            Ok(graphrecord
1138                .add_edges_dataframes_with_group(edges_dataframes, &group)
1139                .map_err(PyGraphRecordError::from)?)
1140        }
1141    }
1142
1143    #[pyo3(signature = (edges_dataframes, groups, bypass_plugins=false))]
1144    pub fn add_edges_dataframes_with_groups(
1145        &self,
1146        edges_dataframes: Vec<(PyDataFrame, String, String)>,
1147        groups: Vec<PyGroup>,
1148        bypass_plugins: bool,
1149    ) -> PyResult<Vec<EdgeIndex>> {
1150        let mut graphrecord = self.inner_mut()?;
1151        let groups: Vec<Group> = groups.deep_into();
1152        let edges_dataframes: Vec<EdgeDataFrameInput> =
1153            edges_dataframes.into_iter().map(Into::into).collect();
1154
1155        if bypass_plugins {
1156            Ok(graphrecord
1157                .add_edges_dataframes_with_groups_bypass_plugins(edges_dataframes, &groups)
1158                .map_err(PyGraphRecordError::from)?)
1159        } else {
1160            Ok(graphrecord
1161                .add_edges_dataframes_with_groups(edges_dataframes, &groups)
1162                .map_err(PyGraphRecordError::from)?)
1163        }
1164    }
1165
1166    #[pyo3(signature = (group, node_indices_to_add=None, edge_indices_to_add=None, bypass_plugins=false))]
1167    pub fn add_group(
1168        &self,
1169        group: PyGroup,
1170        node_indices_to_add: Option<Vec<PyNodeIndex>>,
1171        edge_indices_to_add: Option<Vec<EdgeIndex>>,
1172        bypass_plugins: bool,
1173    ) -> PyResult<()> {
1174        let mut graphrecord = self.inner_mut()?;
1175
1176        if bypass_plugins {
1177            Ok(graphrecord
1178                .add_group_bypass_plugins(
1179                    group.into(),
1180                    node_indices_to_add.deep_into(),
1181                    edge_indices_to_add,
1182                )
1183                .map_err(PyGraphRecordError::from)?)
1184        } else {
1185            Ok(graphrecord
1186                .add_group(
1187                    group.into(),
1188                    node_indices_to_add.deep_into(),
1189                    edge_indices_to_add,
1190                )
1191                .map_err(PyGraphRecordError::from)?)
1192        }
1193    }
1194
1195    #[pyo3(signature = (group, bypass_plugins=false))]
1196    pub fn remove_groups(&self, group: Vec<PyGroup>, bypass_plugins: bool) -> PyResult<()> {
1197        let mut graphrecord = self.inner_mut()?;
1198
1199        if bypass_plugins {
1200            group.into_iter().try_for_each(|group| {
1201                graphrecord
1202                    .remove_group_bypass_plugins(&group)
1203                    .map_err(PyGraphRecordError::from)?;
1204                Ok(())
1205            })
1206        } else {
1207            group.into_iter().try_for_each(|group| {
1208                graphrecord
1209                    .remove_group(&group)
1210                    .map_err(PyGraphRecordError::from)?;
1211                Ok(())
1212            })
1213        }
1214    }
1215
1216    #[pyo3(signature = (group, node_indices, bypass_plugins=false))]
1217    pub fn add_nodes_to_group(
1218        &self,
1219        group: PyGroup,
1220        node_indices: Vec<PyNodeIndex>,
1221        bypass_plugins: bool,
1222    ) -> PyResult<()> {
1223        let mut graphrecord = self.inner_mut()?;
1224
1225        if bypass_plugins {
1226            node_indices.into_iter().try_for_each(|node_index| {
1227                Ok(graphrecord
1228                    .add_node_to_group_bypass_plugins(group.clone().into(), node_index.into())
1229                    .map_err(PyGraphRecordError::from)?)
1230            })
1231        } else {
1232            node_indices.into_iter().try_for_each(|node_index| {
1233                Ok(graphrecord
1234                    .add_node_to_group(group.clone().into(), node_index.into())
1235                    .map_err(PyGraphRecordError::from)?)
1236            })
1237        }
1238    }
1239
1240    #[pyo3(signature = (node_index, groups, bypass_plugins=false))]
1241    pub fn add_node_to_groups(
1242        &self,
1243        node_index: PyNodeIndex,
1244        groups: Vec<PyGroup>,
1245        bypass_plugins: bool,
1246    ) -> PyResult<()> {
1247        let mut graphrecord = self.inner_mut()?;
1248        let groups: Vec<Group> = groups.deep_into();
1249
1250        if bypass_plugins {
1251            graphrecord
1252                .add_node_to_groups_bypass_plugins(&groups, node_index.into())
1253                .map_err(PyGraphRecordError::from)?;
1254        } else {
1255            graphrecord
1256                .add_node_to_groups(&groups, node_index.into())
1257                .map_err(PyGraphRecordError::from)?;
1258        }
1259
1260        Ok(())
1261    }
1262
1263    #[pyo3(signature = (node_indices, groups, bypass_plugins=false))]
1264    pub fn add_nodes_to_groups(
1265        &self,
1266        node_indices: Vec<PyNodeIndex>,
1267        groups: Vec<PyGroup>,
1268        bypass_plugins: bool,
1269    ) -> PyResult<()> {
1270        let mut graphrecord = self.inner_mut()?;
1271        let groups: Vec<Group> = groups.deep_into();
1272
1273        if bypass_plugins {
1274            graphrecord
1275                .add_nodes_to_groups_bypass_plugins(&groups, node_indices.deep_into())
1276                .map_err(PyGraphRecordError::from)?;
1277        } else {
1278            graphrecord
1279                .add_nodes_to_groups(&groups, node_indices.deep_into())
1280                .map_err(PyGraphRecordError::from)?;
1281        }
1282
1283        Ok(())
1284    }
1285
1286    #[pyo3(signature = (group, edge_indices, bypass_plugins=false))]
1287    pub fn add_edges_to_group(
1288        &self,
1289        group: PyGroup,
1290        edge_indices: Vec<EdgeIndex>,
1291        bypass_plugins: bool,
1292    ) -> PyResult<()> {
1293        let mut graphrecord = self.inner_mut()?;
1294
1295        if bypass_plugins {
1296            edge_indices.into_iter().try_for_each(|edge_index| {
1297                Ok(graphrecord
1298                    .add_edge_to_group_bypass_plugins(group.clone().into(), edge_index)
1299                    .map_err(PyGraphRecordError::from)?)
1300            })
1301        } else {
1302            edge_indices.into_iter().try_for_each(|edge_index| {
1303                Ok(graphrecord
1304                    .add_edge_to_group(group.clone().into(), edge_index)
1305                    .map_err(PyGraphRecordError::from)?)
1306            })
1307        }
1308    }
1309
1310    #[pyo3(signature = (edge_index, groups, bypass_plugins=false))]
1311    pub fn add_edge_to_groups(
1312        &self,
1313        edge_index: EdgeIndex,
1314        groups: Vec<PyGroup>,
1315        bypass_plugins: bool,
1316    ) -> PyResult<()> {
1317        let mut graphrecord = self.inner_mut()?;
1318        let groups: Vec<Group> = groups.deep_into();
1319
1320        if bypass_plugins {
1321            graphrecord
1322                .add_edge_to_groups_bypass_plugins(&groups, edge_index)
1323                .map_err(PyGraphRecordError::from)?;
1324        } else {
1325            graphrecord
1326                .add_edge_to_groups(&groups, edge_index)
1327                .map_err(PyGraphRecordError::from)?;
1328        }
1329
1330        Ok(())
1331    }
1332
1333    #[pyo3(signature = (edge_indices, groups, bypass_plugins=false))]
1334    pub fn add_edges_to_groups(
1335        &self,
1336        edge_indices: Vec<EdgeIndex>,
1337        groups: Vec<PyGroup>,
1338        bypass_plugins: bool,
1339    ) -> PyResult<()> {
1340        let mut graphrecord = self.inner_mut()?;
1341        let groups: Vec<Group> = groups.deep_into();
1342
1343        if bypass_plugins {
1344            graphrecord
1345                .add_edges_to_groups_bypass_plugins(&groups, edge_indices)
1346                .map_err(PyGraphRecordError::from)?;
1347        } else {
1348            graphrecord
1349                .add_edges_to_groups(&groups, edge_indices)
1350                .map_err(PyGraphRecordError::from)?;
1351        }
1352
1353        Ok(())
1354    }
1355
1356    #[pyo3(signature = (group, node_indices, bypass_plugins=false))]
1357    pub fn remove_nodes_from_group(
1358        &self,
1359        group: PyGroup,
1360        node_indices: Vec<PyNodeIndex>,
1361        bypass_plugins: bool,
1362    ) -> PyResult<()> {
1363        let mut graphrecord = self.inner_mut()?;
1364
1365        if bypass_plugins {
1366            node_indices.into_iter().try_for_each(|node_index| {
1367                Ok(graphrecord
1368                    .remove_node_from_group_bypass_plugins(&group, &node_index)
1369                    .map_err(PyGraphRecordError::from)?)
1370            })
1371        } else {
1372            node_indices.into_iter().try_for_each(|node_index| {
1373                Ok(graphrecord
1374                    .remove_node_from_group(&group, &node_index)
1375                    .map_err(PyGraphRecordError::from)?)
1376            })
1377        }
1378    }
1379
1380    #[pyo3(signature = (node_index, groups, bypass_plugins=false))]
1381    pub fn remove_node_from_groups(
1382        &self,
1383        node_index: PyNodeIndex,
1384        groups: Vec<PyGroup>,
1385        bypass_plugins: bool,
1386    ) -> PyResult<()> {
1387        let mut graphrecord = self.inner_mut()?;
1388        let groups: Vec<Group> = groups.deep_into();
1389        let node_index: NodeIndex = node_index.into();
1390
1391        if bypass_plugins {
1392            graphrecord
1393                .remove_node_from_groups_bypass_plugins(&groups, &node_index)
1394                .map_err(PyGraphRecordError::from)?;
1395        } else {
1396            graphrecord
1397                .remove_node_from_groups(&groups, &node_index)
1398                .map_err(PyGraphRecordError::from)?;
1399        }
1400
1401        Ok(())
1402    }
1403
1404    #[pyo3(signature = (node_indices, groups, bypass_plugins=false))]
1405    pub fn remove_nodes_from_groups(
1406        &self,
1407        node_indices: Vec<PyNodeIndex>,
1408        groups: Vec<PyGroup>,
1409        bypass_plugins: bool,
1410    ) -> PyResult<()> {
1411        let mut graphrecord = self.inner_mut()?;
1412        let groups: Vec<Group> = groups.deep_into();
1413        let node_indices: Vec<NodeIndex> = node_indices.deep_into();
1414
1415        if bypass_plugins {
1416            graphrecord
1417                .remove_nodes_from_groups_bypass_plugins(&groups, &node_indices)
1418                .map_err(PyGraphRecordError::from)?;
1419        } else {
1420            graphrecord
1421                .remove_nodes_from_groups(&groups, &node_indices)
1422                .map_err(PyGraphRecordError::from)?;
1423        }
1424
1425        Ok(())
1426    }
1427
1428    #[pyo3(signature = (group, edge_indices, bypass_plugins=false))]
1429    pub fn remove_edges_from_group(
1430        &self,
1431        group: PyGroup,
1432        edge_indices: Vec<EdgeIndex>,
1433        bypass_plugins: bool,
1434    ) -> PyResult<()> {
1435        let mut graphrecord = self.inner_mut()?;
1436
1437        if bypass_plugins {
1438            edge_indices.into_iter().try_for_each(|edge_index| {
1439                Ok(graphrecord
1440                    .remove_edge_from_group_bypass_plugins(&group, &edge_index)
1441                    .map_err(PyGraphRecordError::from)?)
1442            })
1443        } else {
1444            edge_indices.into_iter().try_for_each(|edge_index| {
1445                Ok(graphrecord
1446                    .remove_edge_from_group(&group, &edge_index)
1447                    .map_err(PyGraphRecordError::from)?)
1448            })
1449        }
1450    }
1451
1452    #[pyo3(signature = (edge_index, groups, bypass_plugins=false))]
1453    pub fn remove_edge_from_groups(
1454        &self,
1455        edge_index: EdgeIndex,
1456        groups: Vec<PyGroup>,
1457        bypass_plugins: bool,
1458    ) -> PyResult<()> {
1459        let mut graphrecord = self.inner_mut()?;
1460        let groups: Vec<Group> = groups.deep_into();
1461
1462        if bypass_plugins {
1463            graphrecord
1464                .remove_edge_from_groups_bypass_plugins(&groups, &edge_index)
1465                .map_err(PyGraphRecordError::from)?;
1466        } else {
1467            graphrecord
1468                .remove_edge_from_groups(&groups, &edge_index)
1469                .map_err(PyGraphRecordError::from)?;
1470        }
1471
1472        Ok(())
1473    }
1474
1475    #[pyo3(signature = (edge_indices, groups, bypass_plugins=false))]
1476    pub fn remove_edges_from_groups(
1477        &self,
1478        edge_indices: Vec<EdgeIndex>,
1479        groups: Vec<PyGroup>,
1480        bypass_plugins: bool,
1481    ) -> PyResult<()> {
1482        let mut graphrecord = self.inner_mut()?;
1483        let groups: Vec<Group> = groups.deep_into();
1484
1485        if bypass_plugins {
1486            graphrecord
1487                .remove_edges_from_groups_bypass_plugins(&groups, &edge_indices)
1488                .map_err(PyGraphRecordError::from)?;
1489        } else {
1490            graphrecord
1491                .remove_edges_from_groups(&groups, &edge_indices)
1492                .map_err(PyGraphRecordError::from)?;
1493        }
1494
1495        Ok(())
1496    }
1497
1498    pub fn nodes_in_group(
1499        &self,
1500        group: Vec<PyGroup>,
1501    ) -> PyResult<HashMap<PyGroup, Vec<PyNodeIndex>>> {
1502        let graphrecord = self.inner()?;
1503
1504        group
1505            .into_iter()
1506            .map(|group| {
1507                let nodes_attributes = graphrecord
1508                    .nodes_in_group(&group)
1509                    .map_err(PyGraphRecordError::from)?
1510                    .map(|node_index| node_index.clone().into())
1511                    .collect();
1512
1513                Ok((group, nodes_attributes))
1514            })
1515            .collect()
1516    }
1517
1518    pub fn ungrouped_nodes(&self) -> PyResult<Vec<PyNodeIndex>> {
1519        Ok(self
1520            .inner()?
1521            .ungrouped_nodes()
1522            .map(|node_index| node_index.clone().into())
1523            .collect())
1524    }
1525
1526    pub fn edges_in_group(
1527        &self,
1528        group: Vec<PyGroup>,
1529    ) -> PyResult<HashMap<PyGroup, Vec<EdgeIndex>>> {
1530        let graphrecord = self.inner()?;
1531
1532        group
1533            .into_iter()
1534            .map(|group| {
1535                let edges = graphrecord
1536                    .edges_in_group(&group)
1537                    .map_err(PyGraphRecordError::from)?
1538                    .copied()
1539                    .collect();
1540
1541                Ok((group, edges))
1542            })
1543            .collect()
1544    }
1545
1546    pub fn ungrouped_edges(&self) -> PyResult<Vec<EdgeIndex>> {
1547        Ok(self.inner()?.ungrouped_edges().copied().collect())
1548    }
1549
1550    pub fn groups_of_node(
1551        &self,
1552        node_index: Vec<PyNodeIndex>,
1553    ) -> PyResult<HashMap<PyNodeIndex, Vec<PyGroup>>> {
1554        let graphrecord = self.inner()?;
1555
1556        node_index
1557            .into_iter()
1558            .map(|node_index| {
1559                let groups = graphrecord
1560                    .groups_of_node(&node_index)
1561                    .map_err(PyGraphRecordError::from)?
1562                    .map(|group| group.clone().into())
1563                    .collect();
1564
1565                Ok((node_index, groups))
1566            })
1567            .collect()
1568    }
1569
1570    pub fn groups_of_edge(
1571        &self,
1572        edge_index: Vec<EdgeIndex>,
1573    ) -> PyResult<HashMap<EdgeIndex, Vec<PyGroup>>> {
1574        let graphrecord = self.inner()?;
1575
1576        edge_index
1577            .into_iter()
1578            .map(|edge_index| {
1579                let groups = graphrecord
1580                    .groups_of_edge(&edge_index)
1581                    .map_err(PyGraphRecordError::from)?
1582                    .map(|group| group.clone().into())
1583                    .collect();
1584
1585                Ok((edge_index, groups))
1586            })
1587            .collect()
1588    }
1589
1590    pub fn node_count(&self) -> PyResult<usize> {
1591        Ok(self.inner()?.node_count())
1592    }
1593
1594    pub fn edge_count(&self) -> PyResult<usize> {
1595        Ok(self.inner()?.edge_count())
1596    }
1597
1598    pub fn group_count(&self) -> PyResult<usize> {
1599        Ok(self.inner()?.group_count())
1600    }
1601
1602    pub fn contains_node(&self, node_index: PyNodeIndex) -> PyResult<bool> {
1603        Ok(self.inner()?.contains_node(&node_index.into()))
1604    }
1605
1606    pub fn contains_edge(&self, edge_index: EdgeIndex) -> PyResult<bool> {
1607        Ok(self.inner()?.contains_edge(&edge_index))
1608    }
1609
1610    pub fn contains_group(&self, group: PyGroup) -> PyResult<bool> {
1611        Ok(self.inner()?.contains_group(&group.into()))
1612    }
1613
1614    pub fn outgoing_neighbors(
1615        &self,
1616        node_indices: Vec<PyNodeIndex>,
1617    ) -> PyResult<HashMap<PyNodeIndex, Vec<PyNodeIndex>>> {
1618        let graphrecord = self.inner()?;
1619
1620        node_indices
1621            .into_iter()
1622            .map(|node_index| {
1623                let neighbors = graphrecord
1624                    .outgoing_neighbors(&node_index)
1625                    .map_err(PyGraphRecordError::from)?
1626                    .map(|neighbor| neighbor.clone().into())
1627                    .collect();
1628
1629                Ok((node_index, neighbors))
1630            })
1631            .collect()
1632    }
1633
1634    pub fn incoming_neighbors(
1635        &self,
1636        node_indices: Vec<PyNodeIndex>,
1637    ) -> PyResult<HashMap<PyNodeIndex, Vec<PyNodeIndex>>> {
1638        let graphrecord = self.inner()?;
1639
1640        node_indices
1641            .into_iter()
1642            .map(|node_index| {
1643                let neighbors = graphrecord
1644                    .incoming_neighbors(&node_index)
1645                    .map_err(PyGraphRecordError::from)?
1646                    .map(|neighbor| neighbor.clone().into())
1647                    .collect();
1648
1649                Ok((node_index, neighbors))
1650            })
1651            .collect()
1652    }
1653
1654    pub fn neighbors(
1655        &self,
1656        node_indices: Vec<PyNodeIndex>,
1657    ) -> PyResult<HashMap<PyNodeIndex, Vec<PyNodeIndex>>> {
1658        let graphrecord = self.inner()?;
1659
1660        node_indices
1661            .into_iter()
1662            .map(|node_index| {
1663                let neighbors = graphrecord
1664                    .neighbors(&node_index)
1665                    .map_err(PyGraphRecordError::from)?
1666                    .map(|neighbor| neighbor.clone().into())
1667                    .collect();
1668
1669                Ok((node_index, neighbors))
1670            })
1671            .collect()
1672    }
1673
1674    #[pyo3(signature = (bypass_plugins=false))]
1675    pub fn clear(&self, bypass_plugins: bool) -> PyResult<()> {
1676        let mut graphrecord = self.inner_mut()?;
1677
1678        if bypass_plugins {
1679            Ok(graphrecord
1680                .clear_bypass_plugins()
1681                .map_err(PyGraphRecordError::from)?)
1682        } else {
1683            Ok(graphrecord.clear().map_err(PyGraphRecordError::from)?)
1684        }
1685    }
1686
1687    pub fn query_nodes(&self, query: &Bound<'_, PyFunction>) -> PyResult<Py<PyAny>> {
1688        let graphrecord = self.inner()?;
1689
1690        PyOperand::query_nodes(&graphrecord, query)
1691    }
1692
1693    pub fn query_edges(&self, query: &Bound<'_, PyFunction>) -> PyResult<Py<PyAny>> {
1694        let graphrecord = self.inner()?;
1695
1696        PyOperand::query_edges(&graphrecord, query)
1697    }
1698
1699    #[allow(clippy::should_implement_trait)]
1700    pub fn clone(&self) -> Self {
1701        Clone::clone(self)
1702    }
1703
1704    pub fn overview(&self, truncate_details: Option<usize>) -> PyResult<PyOverview> {
1705        Ok(self
1706            .inner()?
1707            .overview(truncate_details)
1708            .map_err(PyGraphRecordError::from)?
1709            .into())
1710    }
1711
1712    pub fn group_overview(
1713        &self,
1714        group: PyGroup,
1715        truncate_details: Option<usize>,
1716    ) -> PyResult<PyGroupOverview> {
1717        Ok(self
1718            .inner()?
1719            .group_overview(&group.into(), truncate_details)
1720            .map_err(PyGraphRecordError::from)?
1721            .into())
1722    }
1723}