ontoenv/
lib.rs

1use ::ontoenv::api::{OntoEnv as OntoEnvRs, ResolveTarget};
2use ::ontoenv::config;
3use ::ontoenv::consts::{IMPORTS, ONTOLOGY, TYPE};
4use ::ontoenv::ToUriString;
5use ::ontoenv::ontology::{Ontology as OntologyRs, OntologyLocation};
6use ::ontoenv::transform;
7use anyhow::Error;
8use oxigraph::model::{BlankNode, Literal, NamedNode, SubjectRef, Term};
9use pyo3::{
10    prelude::*,
11    types::{IntoPyDict, PyString, PyTuple},
12};
13use std::borrow::Borrow;
14use std::collections::{HashMap, HashSet};
15use std::path::PathBuf;
16use std::sync::{Arc, Mutex, Once};
17
18fn anyhow_to_pyerr(e: Error) -> PyErr {
19    PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string())
20}
21
22static INIT: Once = Once::new();
23
24#[allow(dead_code)]
25struct MyTerm(Term);
26impl From<Result<Bound<'_, PyAny>, pyo3::PyErr>> for MyTerm {
27    fn from(s: Result<Bound<'_, PyAny>, pyo3::PyErr>) -> Self {
28        let s = s.unwrap();
29        let typestr = s.get_type().name().unwrap();
30        let typestr = typestr.to_string();
31        let data_type: Option<NamedNode> = match s.getattr("datatype") {
32            Ok(dt) => {
33                if dt.is_none() {
34                    None
35                } else {
36                    Some(NamedNode::new(dt.to_string()).unwrap())
37                }
38            }
39            Err(_) => None,
40        };
41        let lang: Option<String> = match s.getattr("language") {
42            Ok(l) => {
43                if l.is_none() {
44                    None
45                } else {
46                    Some(l.to_string())
47                }
48            }
49            Err(_) => None,
50        };
51        let n: Term = match typestr.borrow() {
52            "URIRef" => Term::NamedNode(NamedNode::new(s.to_string()).unwrap()),
53            "Literal" => match (data_type, lang) {
54                (Some(dt), None) => Term::Literal(Literal::new_typed_literal(s.to_string(), dt)),
55                (None, Some(l)) => {
56                    Term::Literal(Literal::new_language_tagged_literal(s.to_string(), l).unwrap())
57                }
58                (_, _) => Term::Literal(Literal::new_simple_literal(s.to_string())),
59            },
60            "BNode" => Term::BlankNode(BlankNode::new(s.to_string()).unwrap()),
61            _ => Term::NamedNode(NamedNode::new(s.to_string()).unwrap()),
62        };
63        MyTerm(n)
64    }
65}
66
67fn term_to_python<'a>(
68    py: Python,
69    rdflib: &Bound<'a, PyModule>,
70    node: Term,
71) -> PyResult<Bound<'a, PyAny>> {
72    let dtype: Option<String> = match &node {
73        Term::Literal(lit) => {
74            let mut s = lit.datatype().to_string();
75            s.remove(0);
76            s.remove(s.len() - 1);
77            Some(s)
78        }
79        _ => None,
80    };
81    let lang: Option<&str> = match &node {
82        Term::Literal(lit) => lit.language(),
83        _ => None,
84    };
85
86    let res: Bound<'_, PyAny> = match &node {
87        Term::NamedNode(uri) => {
88            let mut uri = uri.to_string();
89            uri.remove(0);
90            uri.remove(uri.len() - 1);
91            rdflib.getattr("URIRef")?.call1((uri,))?
92        }
93        Term::Literal(literal) => {
94            match (dtype, lang) {
95                // prioritize 'lang' -> it implies String
96                (_, Some(lang)) => {
97                    rdflib
98                        .getattr("Literal")?
99                        .call1((literal.value(), lang, py.None()))?
100                }
101                (Some(dtype), None) => {
102                    rdflib
103                        .getattr("Literal")?
104                        .call1((literal.value(), py.None(), dtype))?
105                }
106                (None, None) => rdflib.getattr("Literal")?.call1((literal.value(),))?,
107            }
108        }
109        Term::BlankNode(id) => rdflib
110            .getattr("BNode")?
111            .call1((id.clone().into_string(),))?,
112        Term::Triple(_) => {
113            return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
114                "Triples are not supported",
115            ))
116        }
117    };
118    Ok(res)
119}
120
121#[pyclass]
122#[derive(Clone)]
123struct Config {
124    cfg: config::Config,
125}
126
127#[pymethods]
128impl Config {
129    #[new]
130    #[pyo3(signature = (search_directories=None, require_ontology_names=false, strict=false, offline=false, resolution_policy="default".to_owned(), root=".".to_owned(), includes=None, excludes=None, temporary=false, no_search=false))]
131    fn new(
132        search_directories: Option<Vec<String>>,
133        require_ontology_names: bool,
134        strict: bool,
135        offline: bool,
136        resolution_policy: String,
137        root: String,
138        includes: Option<Vec<String>>,
139        excludes: Option<Vec<String>>,
140        temporary: bool,
141        no_search: bool,
142    ) -> PyResult<Self> {
143        let mut builder = config::Config::builder()
144            .root(root.into())
145            .require_ontology_names(require_ontology_names)
146            .strict(strict)
147            .offline(offline)
148            .resolution_policy(resolution_policy)
149            .temporary(temporary)
150            .no_search(no_search);
151
152        if let Some(dirs) = search_directories {
153            let paths = dirs.into_iter().map(PathBuf::from).collect();
154            builder = builder.locations(paths);
155        }
156
157        if let Some(includes) = includes {
158            builder = builder.includes(includes);
159        }
160
161        if let Some(excludes) = excludes {
162            builder = builder.excludes(excludes);
163        }
164
165        let cfg = builder
166            .build()
167            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
168
169        Ok(Config { cfg })
170    }
171}
172
173
174#[pyclass(name = "Ontology")]
175#[derive(Clone)]
176struct PyOntology {
177    inner: OntologyRs,
178}
179
180#[pymethods]
181impl PyOntology {
182    #[getter]
183    fn id(&self) -> PyResult<String> {
184        Ok(self.inner.id().to_uri_string())
185    }
186
187    #[getter]
188    fn name(&self) -> PyResult<String> {
189        Ok(self.inner.name().to_uri_string())
190    }
191
192    #[getter]
193    fn imports(&self) -> PyResult<Vec<String>> {
194        Ok(self
195            .inner
196            .imports
197            .iter()
198            .map(|i| i.to_uri_string())
199            .collect())
200    }
201
202    #[getter]
203    fn location(&self) -> PyResult<Option<String>> {
204        Ok(self.inner.location().map(|l| l.to_string()))
205    }
206
207    #[getter]
208    fn last_updated(&self) -> PyResult<Option<String>> {
209        Ok(self.inner.last_updated.map(|dt| dt.to_rfc3339()))
210    }
211
212    #[getter]
213    fn version_properties(&self) -> PyResult<HashMap<String, String>> {
214        Ok(self
215            .inner
216            .version_properties()
217            .iter()
218            .map(|(k, v)| (k.to_uri_string(), v.clone()))
219            .collect())
220    }
221
222    #[getter]
223    fn namespace_map(&self) -> PyResult<HashMap<String, String>> {
224        Ok(self.inner.namespace_map().clone())
225    }
226
227    fn __repr__(&self) -> PyResult<String> {
228        Ok(format!(
229            "<Ontology: {}>",
230            self.inner.name().to_uri_string()
231        ))
232    }
233}
234
235#[pyclass]
236struct OntoEnv {
237    inner: Arc<Mutex<Option<OntoEnvRs>>>,
238}
239
240#[pymethods]
241impl OntoEnv {
242    #[new]
243    #[pyo3(signature = (config=None, path=None, recreate=false, read_only=false))]
244    fn new(
245        _py: Python,
246        config: Option<Config>,
247        path: Option<PathBuf>,
248        recreate: bool,
249        read_only: bool,
250    ) -> PyResult<Self> {
251        // wrap env_logger::init() in a Once to ensure it's only called once. This can
252        // happen if a user script creates multiple OntoEnv instances
253        INIT.call_once(|| {
254            env_logger::init();
255        });
256
257        let env = if let Some(c) = config {
258            let config_path = path.unwrap_or_else(|| PathBuf::from("."));
259            // if temporary is true, create a new OntoEnv
260            if c.cfg.temporary {
261                OntoEnvRs::init(c.cfg, recreate).map_err(anyhow_to_pyerr)
262            } else if !recreate && config_path.join(".ontoenv").exists() {
263                // if temporary is false, load from the directory
264                OntoEnvRs::load_from_directory(config_path, read_only).map_err(anyhow_to_pyerr)
265            } else {
266                // if temporary is false and recreate is true or the directory doesn't exist, create a new OntoEnv
267                OntoEnvRs::init(c.cfg, recreate).map_err(anyhow_to_pyerr)
268            }
269        } else if let Some(p) = path {
270            if !recreate {
271                if let Some(root) = ::ontoenv::api::find_ontoenv_root_from(&p) {
272                    OntoEnvRs::load_from_directory(root, read_only).map_err(anyhow_to_pyerr)
273                } else {
274                    let cfg = config::Config::default(p).map_err(|e| {
275                        PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string())
276                    })?;
277                    OntoEnvRs::init(cfg, false).map_err(anyhow_to_pyerr)
278                }
279            } else {
280                let cfg = config::Config::default(p).map_err(|e| {
281                    PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string())
282                })?;
283                OntoEnvRs::init(cfg, true).map_err(anyhow_to_pyerr)
284            }
285        } else {
286            OntoEnvRs::new_offline().map_err(anyhow_to_pyerr)
287        }?;
288
289        let inner = Arc::new(Mutex::new(Some(env)));
290
291        Ok(OntoEnv {
292            inner: inner.clone(),
293        })
294    }
295
296    fn update(&self) -> PyResult<()> {
297        let inner = self.inner.clone();
298        let mut guard = inner.lock().unwrap();
299        if let Some(env) = guard.as_mut() {
300            env.update().map_err(anyhow_to_pyerr)?;
301            env.save_to_directory().map_err(anyhow_to_pyerr)
302        } else {
303            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
304                "OntoEnv is closed",
305            ))
306        }
307    }
308
309    // fn is_read_only(&self) -> PyResult<bool> {
310    //     let inner = self.inner.clone();
311    //     let env = inner.lock().unwrap();
312    //     Ok(env.is_read_only())
313    // }
314
315    fn __repr__(&self) -> PyResult<String> {
316        let inner = self.inner.clone();
317        let guard = inner.lock().unwrap();
318        if let Some(env) = guard.as_ref() {
319            let stats = env.stats().map_err(anyhow_to_pyerr)?;
320            Ok(format!(
321                "<OntoEnv: {} ontologies, {} graphs, {} triples>",
322                stats.num_ontologies, stats.num_graphs, stats.num_triples,
323            ))
324        } else {
325            Ok("<OntoEnv: closed>".to_string())
326        }
327    }
328
329    // The following methods will now access the inner OntoEnv in a thread-safe manner:
330
331    fn import_graph(
332        &self,
333        py: Python,
334        destination_graph: &Bound<'_, PyAny>,
335        uri: &str,
336    ) -> PyResult<()> {
337        let inner = self.inner.clone();
338        let mut guard = inner.lock().unwrap();
339        let env = guard.as_mut().ok_or_else(|| {
340            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
341        })?;
342        let rdflib = py.import("rdflib")?;
343        let iri = NamedNode::new(uri)
344            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
345        let graphid = env
346            .resolve(ResolveTarget::Graph(iri.clone()))
347            .ok_or_else(|| {
348                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
349                    "Failed to resolve graph for URI: {uri}"
350                ))
351            })?;
352        let mut graph = env.get_graph(&graphid).map_err(anyhow_to_pyerr)?;
353
354        let uriref_constructor = rdflib.getattr("URIRef")?;
355        let type_uri = uriref_constructor.call1((TYPE.as_str(),))?;
356        let ontology_uri = uriref_constructor.call1((ONTOLOGY.as_str(),))?;
357        let kwargs = [("predicate", type_uri), ("object", ontology_uri)].into_py_dict(py)?;
358        let result = destination_graph.call_method("value", (), Some(&kwargs))?;
359        if !result.is_none() {
360            let ontology = NamedNode::new(result.extract::<String>()?)
361                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
362            let base_ontology: SubjectRef = SubjectRef::NamedNode(ontology.as_ref());
363
364            transform::rewrite_sh_prefixes_graph(&mut graph, base_ontology);
365            transform::remove_ontology_declarations_graph(&mut graph, base_ontology);
366        }
367        // remove the owl:import statement for the 'uri' ontology
368        transform::remove_owl_imports_graph(&mut graph, Some(&[iri.as_ref()]));
369
370        Python::with_gil(|_py| {
371            for triple in graph.into_iter() {
372                let s: Term = triple.subject.into();
373                let p: Term = triple.predicate.into();
374                let o: Term = triple.object.into();
375
376                let t = PyTuple::new(
377                    py,
378                    &[
379                        term_to_python(py, &rdflib, s)?,
380                        term_to_python(py, &rdflib, p)?,
381                        term_to_python(py, &rdflib, o)?,
382                    ],
383                )?;
384
385                destination_graph.getattr("add")?.call1((t,))?;
386            }
387            Ok::<(), PyErr>(())
388        })?;
389        Ok(())
390    }
391
392    /// List the ontologies in the imports closure of the given ontology
393    #[pyo3(signature = (uri, recursion_depth = -1))]
394    fn list_closure(&self, _py: Python, uri: &str, recursion_depth: i32) -> PyResult<Vec<String>> {
395        let iri = NamedNode::new(uri)
396            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
397        let inner = self.inner.clone();
398        let mut guard = inner.lock().unwrap();
399        let env = guard.as_mut().ok_or_else(|| {
400            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
401        })?;
402        let graphid = env
403            .resolve(ResolveTarget::Graph(iri.clone()))
404            .ok_or_else(|| {
405                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
406                    "Failed to resolve graph for URI: {uri}"
407                ))
408            })?;
409        let ont = env.ontologies().get(&graphid).ok_or_else(|| {
410            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
411        })?;
412        let closure = env
413            .get_closure(ont.id(), recursion_depth)
414            .map_err(anyhow_to_pyerr)?;
415        let names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
416        Ok(names)
417    }
418
419    /// Merge all graphs in the imports closure of the given ontology into a single graph. If
420    /// destination_graph is provided, add the merged graph to the destination_graph. If not,
421    /// return the merged graph.
422    #[pyo3(signature = (uri, destination_graph=None, rewrite_sh_prefixes=true, remove_owl_imports=true, recursion_depth=-1))]
423    fn get_closure<'a>(
424        &self,
425        py: Python<'a>,
426        uri: &str,
427        destination_graph: Option<&Bound<'a, PyAny>>,
428        rewrite_sh_prefixes: bool,
429        remove_owl_imports: bool,
430        recursion_depth: i32,
431    ) -> PyResult<(Bound<'a, PyAny>, Vec<String>)> {
432        let rdflib = py.import("rdflib")?;
433        let iri = NamedNode::new(uri)
434            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
435        let inner = self.inner.clone();
436        let mut guard = inner.lock().unwrap();
437        let env = guard.as_mut().ok_or_else(|| {
438            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
439        })?;
440        let graphid = env
441            .resolve(ResolveTarget::Graph(iri.clone()))
442            .ok_or_else(|| {
443                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
444                    "No graph with URI: {uri}"
445                ))
446            })?;
447        let ont = env.ontologies().get(&graphid).ok_or_else(|| {
448            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
449        })?;
450        let closure = env
451            .get_closure(ont.id(), recursion_depth)
452            .map_err(anyhow_to_pyerr)?;
453        let closure_names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
454        // if destination_graph is null, create a new rdflib.Graph()
455        let destination_graph = match destination_graph {
456            Some(g) => g.clone(),
457            None => rdflib.getattr("Graph")?.call0()?,
458        };
459        let union = env
460            .get_union_graph(
461                &closure,
462                Some(rewrite_sh_prefixes),
463                Some(remove_owl_imports),
464            )
465            .map_err(anyhow_to_pyerr)?;
466        for triple in union.dataset.into_iter() {
467            let s: Term = triple.subject.into();
468            let p: Term = triple.predicate.into();
469            let o: Term = triple.object.into();
470            let t = PyTuple::new(
471                py,
472                &[
473                    term_to_python(py, &rdflib, s)?,
474                    term_to_python(py, &rdflib, p)?,
475                    term_to_python(py, &rdflib, o)?,
476                ],
477            )?;
478            destination_graph.getattr("add")?.call1((t,))?;
479        }
480
481        // Remove each successful_imports url in the closure from the destination_graph
482        if remove_owl_imports {
483            for graphid in union.graph_ids {
484                let iri = term_to_python(py, &rdflib, Term::NamedNode(graphid.into()))?;
485                let pred = term_to_python(py, &rdflib, IMPORTS.into())?;
486                // remove triples with (None, pred, iri)
487                let remove_tuple = PyTuple::new(py, &[py.None(), pred.into(), iri.into()])?;
488                destination_graph
489                    .getattr("remove")?
490                    .call1((remove_tuple,))?;
491            }
492        }
493        Ok((destination_graph, closure_names))
494    }
495
496    /// Print the contents of the OntoEnv
497    #[pyo3(signature = (includes=None))]
498    fn dump(&self, _py: Python, includes: Option<String>) -> PyResult<()> {
499        let inner = self.inner.clone();
500        let guard = inner.lock().unwrap();
501        if let Some(env) = guard.as_ref() {
502            env.dump(includes.as_deref());
503            Ok(())
504        } else {
505            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
506                "OntoEnv is closed",
507            ))
508        }
509    }
510
511    /// Import the dependencies of the given graph into the graph. Removes the owl:imports
512    /// of all imported ontologies.
513    #[pyo3(signature = (graph, recursion_depth=-1, fetch_missing=false))]
514    fn import_dependencies<'a>(
515        &self,
516        py: Python<'a>,
517        graph: &Bound<'a, PyAny>,
518        recursion_depth: i32,
519        fetch_missing: bool,
520    ) -> PyResult<Vec<String>> {
521        let rdflib = py.import("rdflib")?;
522        let py_imports_pred = term_to_python(py, &rdflib, Term::NamedNode(IMPORTS.into()))?;
523
524        let kwargs = [("predicate", py_imports_pred)].into_py_dict(py)?;
525        let objects_iter = graph.call_method("objects", (), Some(&kwargs))?;
526        let builtins = py.import("builtins")?;
527        let objects_list = builtins.getattr("list")?.call1((objects_iter,))?;
528        let imports: Vec<String> = objects_list.extract()?;
529
530        if imports.is_empty() {
531            return Ok(Vec::new());
532        }
533
534        let inner = self.inner.clone();
535        let mut guard = inner.lock().unwrap();
536        let env = guard.as_mut().ok_or_else(|| {
537            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
538        })?;
539
540        let is_strict = env.is_strict();
541        let mut all_ontologies = HashSet::new();
542        let mut all_closure_names: Vec<String> = Vec::new();
543
544        for uri in &imports {
545            let iri = NamedNode::new(uri.as_str())
546                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
547
548            let mut graphid = env.resolve(ResolveTarget::Graph(iri.clone()));
549
550            if graphid.is_none() && fetch_missing {
551                let location =
552                    OntologyLocation::from_str(uri.as_str()).map_err(anyhow_to_pyerr)?;
553                match env.add(location, false) {
554                    Ok(new_id) => {
555                        graphid = Some(new_id);
556                    }
557                    Err(e) => {
558                        if is_strict {
559                            return Err(anyhow_to_pyerr(e));
560                        }
561                        println!("Failed to fetch {uri}: {e}");
562                    }
563                }
564            }
565
566            let graphid = match graphid {
567                Some(id) => id,
568                None => {
569                    if is_strict {
570                        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
571                            "Failed to resolve graph for URI: {}",
572                            uri
573                        )));
574                    }
575                    println!("could not find {uri:?}");
576                    continue;
577                }
578            };
579
580            let ont = env.ontologies().get(&graphid).ok_or_else(|| {
581                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
582                    "Ontology {} not found",
583                    uri
584                ))
585            })?;
586
587            let closure = env
588                .get_closure(ont.id(), recursion_depth)
589                .map_err(anyhow_to_pyerr)?;
590            for c_ont in closure {
591                all_closure_names.push(c_ont.to_uri_string());
592                all_ontologies.insert(c_ont.clone());
593            }
594        }
595
596        if all_ontologies.is_empty() {
597            return Ok(Vec::new());
598        }
599
600        let union = env
601            .get_union_graph(&all_ontologies, Some(true), Some(true))
602            .map_err(anyhow_to_pyerr)?;
603
604        for triple in union.dataset.into_iter() {
605            let s: Term = triple.subject.into();
606            let p: Term = triple.predicate.into();
607            let o: Term = triple.object.into();
608            let t = PyTuple::new(
609                py,
610                &[
611                    term_to_python(py, &rdflib, s)?,
612                    term_to_python(py, &rdflib, p)?,
613                    term_to_python(py, &rdflib, o)?,
614                ],
615            )?;
616            graph.getattr("add")?.call1((t,))?;
617        }
618
619        // Remove all owl:imports from the original graph
620        let py_imports_pred_for_remove = term_to_python(py, &rdflib, IMPORTS.into())?;
621        let remove_tuple =
622            PyTuple::new(py, &[py.None(), py_imports_pred_for_remove.into(), py.None()])?;
623        graph.getattr("remove")?.call1((remove_tuple,))?;
624
625        all_closure_names.sort();
626        all_closure_names.dedup();
627
628        Ok(all_closure_names)
629    }
630
631    /// Add a new ontology to the OntoEnv
632    #[pyo3(signature = (location, overwrite = false, fetch_imports = true))]
633    fn add(
634        &self,
635        location: &Bound<'_, PyAny>,
636        overwrite: bool,
637        fetch_imports: bool,
638    ) -> PyResult<String> {
639        let inner = self.inner.clone();
640        let mut guard = inner.lock().unwrap();
641        let env = guard.as_mut().ok_or_else(|| {
642            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
643        })?;
644
645        let location =
646            OntologyLocation::from_str(&location.to_string()).map_err(anyhow_to_pyerr)?;
647        let graph_id = if fetch_imports {
648            env.add(location, overwrite)
649        } else {
650            env.add_no_imports(location, overwrite)
651        }
652        .map_err(anyhow_to_pyerr)?;
653        Ok(graph_id.to_uri_string())
654    }
655
656    /// Add a new ontology to the OntoEnv without exploring owl:imports.
657    #[pyo3(signature = (location, overwrite = false))]
658    fn add_no_imports(&self, location: &Bound<'_, PyAny>, overwrite: bool) -> PyResult<String> {
659        let inner = self.inner.clone();
660        let mut guard = inner.lock().unwrap();
661        let env = guard.as_mut().ok_or_else(|| {
662            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
663        })?;
664        let location =
665            OntologyLocation::from_str(&location.to_string()).map_err(anyhow_to_pyerr)?;
666        let graph_id = env
667            .add_no_imports(location, overwrite)
668            .map_err(anyhow_to_pyerr)?;
669        Ok(graph_id.to_uri_string())
670    }
671
672
673    /// Get the names of all ontologies that import the given ontology
674    fn get_importers(&self, uri: &str) -> PyResult<Vec<String>> {
675        let iri = NamedNode::new(uri)
676            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
677        let inner = self.inner.clone();
678        let guard = inner.lock().unwrap();
679        let env = guard.as_ref().ok_or_else(|| {
680            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
681        })?;
682        let importers = env.get_importers(&iri).map_err(anyhow_to_pyerr)?;
683        let names: Vec<String> = importers.iter().map(|ont| ont.to_uri_string()).collect();
684        Ok(names)
685    }
686
687    /// Get the ontology metadata with the given URI
688    fn get_ontology(&self, uri: &str) -> PyResult<PyOntology> {
689        let iri = NamedNode::new(uri)
690            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
691        let inner = self.inner.clone();
692        let guard = inner.lock().unwrap();
693        let env = guard.as_ref().ok_or_else(|| {
694            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
695        })?;
696        let graphid = env
697            .resolve(ResolveTarget::Graph(iri.clone()))
698            .ok_or_else(|| {
699                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
700                    "Failed to resolve graph for URI: {uri}"
701                ))
702            })?;
703        let ont = env.get_ontology(&graphid).map_err(anyhow_to_pyerr)?;
704        Ok(PyOntology { inner: ont })
705    }
706
707    /// Get the graph with the given URI as an rdflib.Graph
708    fn get_graph(&self, py: Python, uri: &Bound<'_, PyString>) -> PyResult<Py<PyAny>> {
709        let rdflib = py.import("rdflib")?;
710        let iri = NamedNode::new(uri.to_string())
711            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
712        let graph = {
713            let inner = self.inner.clone();
714            let guard = inner.lock().unwrap();
715            let env = guard.as_ref().ok_or_else(|| {
716                PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
717            })?;
718            let graphid = env
719                .resolve(ResolveTarget::Graph(iri))
720                .ok_or_else(|| {
721                    PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
722                        "Failed to resolve graph for URI: {uri}"
723                    ))
724                })?;
725
726            env.get_graph(&graphid).map_err(anyhow_to_pyerr)?
727        };
728        let res = rdflib.getattr("Graph")?.call0()?;
729        for triple in graph.into_iter() {
730            let s: Term = triple.subject.into();
731            let p: Term = triple.predicate.into();
732            let o: Term = triple.object.into();
733
734            let t = PyTuple::new(
735                py,
736                &[
737                    term_to_python(py, &rdflib, s)?,
738                    term_to_python(py, &rdflib, p)?,
739                    term_to_python(py, &rdflib, o)?,
740                ],
741            )?;
742
743            res.getattr("add")?.call1((t,))?;
744        }
745        Ok(res.into())
746    }
747
748    /// Get the names of all ontologies in the OntoEnv
749    fn get_ontology_names(&self) -> PyResult<Vec<String>> {
750        let inner = self.inner.clone();
751        let guard = inner.lock().unwrap();
752        let env = guard.as_ref().ok_or_else(|| {
753            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
754        })?;
755        let names: Vec<String> = env
756            .ontologies()
757            .keys()
758            .map(|k| k.to_uri_string())
759            .collect();
760        Ok(names)
761    }
762
763    /// Convert the OntoEnv to an rdflib.Dataset
764    fn to_rdflib_dataset(&self, py: Python) -> PyResult<Py<PyAny>> {
765        // rdflib.ConjunctiveGraph(store="Oxigraph")
766        let inner = self.inner.clone();
767        let guard = inner.lock().unwrap();
768        let env = guard.as_ref().ok_or_else(|| {
769            PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
770        })?;
771        let rdflib = py.import("rdflib")?;
772        let dataset = rdflib.getattr("Dataset")?;
773
774        // call Dataset(store="Oxigraph")
775        let kwargs = [("store", "Oxigraph")].into_py_dict(py)?;
776        let store = dataset.call((), Some(&kwargs))?;
777        let path = env.store_path().unwrap();
778        store.getattr("open")?.call1((path,))?;
779        Ok(store.into())
780    }
781
782    // Config accessors
783    fn is_offline(&self) -> PyResult<bool> {
784        let inner = self.inner.clone();
785        let guard = inner.lock().unwrap();
786        if let Some(env) = guard.as_ref() {
787            Ok(env.is_offline())
788        } else {
789            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
790                "OntoEnv is closed",
791            ))
792        }
793    }
794
795    fn set_offline(&mut self, offline: bool) -> PyResult<()> {
796        let inner = self.inner.clone();
797        let mut guard = inner.lock().unwrap();
798        if let Some(env) = guard.as_mut() {
799            env.set_offline(offline);
800            env.save_to_directory().map_err(anyhow_to_pyerr)
801        } else {
802            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
803                "OntoEnv is closed",
804            ))
805        }
806    }
807
808    fn is_strict(&self) -> PyResult<bool> {
809        let inner = self.inner.clone();
810        let guard = inner.lock().unwrap();
811        if let Some(env) = guard.as_ref() {
812            Ok(env.is_strict())
813        } else {
814            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
815                "OntoEnv is closed",
816            ))
817        }
818    }
819
820    fn set_strict(&mut self, strict: bool) -> PyResult<()> {
821        let inner = self.inner.clone();
822        let mut guard = inner.lock().unwrap();
823        if let Some(env) = guard.as_mut() {
824            env.set_strict(strict);
825            env.save_to_directory().map_err(anyhow_to_pyerr)
826        } else {
827            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
828                "OntoEnv is closed",
829            ))
830        }
831    }
832
833    fn requires_ontology_names(&self) -> PyResult<bool> {
834        let inner = self.inner.clone();
835        let guard = inner.lock().unwrap();
836        if let Some(env) = guard.as_ref() {
837            Ok(env.requires_ontology_names())
838        } else {
839            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
840                "OntoEnv is closed",
841            ))
842        }
843    }
844
845    fn set_require_ontology_names(&mut self, require: bool) -> PyResult<()> {
846        let inner = self.inner.clone();
847        let mut guard = inner.lock().unwrap();
848        if let Some(env) = guard.as_mut() {
849            env.set_require_ontology_names(require);
850            env.save_to_directory().map_err(anyhow_to_pyerr)
851        } else {
852            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
853                "OntoEnv is closed",
854            ))
855        }
856    }
857
858    fn no_search(&self) -> PyResult<bool> {
859        let inner = self.inner.clone();
860        let guard = inner.lock().unwrap();
861        if let Some(env) = guard.as_ref() {
862            Ok(env.no_search())
863        } else {
864            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
865                "OntoEnv is closed",
866            ))
867        }
868    }
869
870    fn set_no_search(&mut self, no_search: bool) -> PyResult<()> {
871        let inner = self.inner.clone();
872        let mut guard = inner.lock().unwrap();
873        if let Some(env) = guard.as_mut() {
874            env.set_no_search(no_search);
875            env.save_to_directory().map_err(anyhow_to_pyerr)
876        } else {
877            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
878                "OntoEnv is closed",
879            ))
880        }
881    }
882
883    fn resolution_policy(&self) -> PyResult<String> {
884        let inner = self.inner.clone();
885        let guard = inner.lock().unwrap();
886        if let Some(env) = guard.as_ref() {
887            Ok(env.resolution_policy().to_string())
888        } else {
889            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
890                "OntoEnv is closed",
891            ))
892        }
893    }
894
895    fn set_resolution_policy(&mut self, policy: String) -> PyResult<()> {
896        let inner = self.inner.clone();
897        let mut guard = inner.lock().unwrap();
898        if let Some(env) = guard.as_mut() {
899            env.set_resolution_policy(policy);
900            env.save_to_directory().map_err(anyhow_to_pyerr)
901        } else {
902            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
903                "OntoEnv is closed",
904            ))
905        }
906    }
907
908    pub fn store_path(&self) -> PyResult<Option<String>> {
909        let inner = self.inner.clone();
910        let guard = inner.lock().unwrap();
911        if let Some(env) = guard.as_ref() {
912            match env.store_path() {
913                Some(path) => Ok(Some(path.to_string_lossy().to_string())),
914                None => Ok(None), // Return None if the path doesn't exist (e.g., temporary env)
915            }
916        } else {
917            Ok(None)
918        }
919    }
920
921    // Wrapper method to raise error if store_path is None, matching previous panic behavior
922    // but providing a Python-level error. Or tests can check for None.
923    // Let's keep the Option return type for flexibility and adjust tests.
924
925    pub fn close(&mut self, py: Python<'_>) -> PyResult<()> {
926        py.allow_threads(|| {
927            let inner = self.inner.clone();
928            let mut guard = inner.lock().unwrap();
929            if let Some(env) = guard.as_mut() {
930                env.save_to_directory().map_err(anyhow_to_pyerr)?;
931                env.flush().map_err(anyhow_to_pyerr)?;
932            }
933            *guard = None;
934            Ok(())
935        })
936    }
937
938    pub fn flush(&mut self, py: Python<'_>) -> PyResult<()> {
939        py.allow_threads(|| {
940            let inner = self.inner.clone();
941            let mut guard = inner.lock().unwrap();
942            if let Some(env) = guard.as_mut() {
943                env.flush().map_err(anyhow_to_pyerr)
944            } else {
945                Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
946                    "OntoEnv is closed",
947                ))
948            }
949        })
950    }
951}
952
953#[pymodule]
954fn ontoenv(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
955    m.add_class::<Config>()?;
956    m.add_class::<OntoEnv>()?;
957    m.add_class::<PyOntology>()?;
958    // add version attribute
959    m.add("version", env!("CARGO_PKG_VERSION"))?;
960    Ok(())
961}