ontoenv_python/
lib.rs

1use ::ontoenv::api::{find_ontoenv_root_from, OntoEnv as OntoEnvRs, ResolveTarget};
2use ::ontoenv::config;
3use ::ontoenv::consts::{IMPORTS, ONTOLOGY, TYPE};
4use ::ontoenv::ontology::{Ontology as OntologyRs, OntologyLocation};
5use ::ontoenv::options::{CacheMode, Overwrite, RefreshStrategy};
6use ::ontoenv::ToUriString;
7use anyhow::Error;
8#[cfg(feature = "cli")]
9use ontoenv_cli;
10use oxigraph::model::{BlankNode, Literal, NamedNode, Term};
11#[cfg(not(feature = "cli"))]
12use pyo3::exceptions::PyRuntimeError;
13use pyo3::{
14    prelude::*,
15    types::{IntoPyDict, PyIterator, PyString, PyStringMethods, PyTuple},
16};
17use rand::random;
18use std::borrow::Borrow;
19use std::collections::{HashMap, HashSet};
20use std::ffi::OsStr;
21use std::path::{Path, PathBuf};
22use std::sync::{Arc, Mutex};
23
24fn anyhow_to_pyerr(e: Error) -> PyErr {
25    PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string())
26}
27
28struct ResolvedLocation {
29    location: OntologyLocation,
30    preferred_name: Option<String>,
31}
32
33fn ontology_location_from_py(location: &Bound<'_, PyAny>) -> PyResult<ResolvedLocation> {
34    let ontology_subject = extract_ontology_subject(location)?;
35
36    // Direct string extraction covers `str`, `Path`, `pathlib.Path`, etc.
37    if let Ok(path_like) = location.extract::<PathBuf>() {
38        return OntologyLocation::from_str(path_like.to_string_lossy().as_ref())
39            .map(|loc| ResolvedLocation {
40                location: loc,
41                preferred_name: ontology_subject,
42            })
43            .map_err(anyhow_to_pyerr);
44    }
45
46    if let Ok(fspath_obj) = location.call_method0("__fspath__") {
47        if let Ok(path_like) = fspath_obj.extract::<PathBuf>() {
48            return OntologyLocation::from_str(path_like.to_string_lossy().as_ref())
49                .map(|loc| ResolvedLocation {
50                    location: loc,
51                    preferred_name: ontology_subject,
52                })
53                .map_err(anyhow_to_pyerr);
54        }
55        let fspath: String = bound_pystring_to_string(fspath_obj.str()?)?;
56        return OntologyLocation::from_str(&fspath)
57            .map(|loc| ResolvedLocation {
58                location: loc,
59                preferred_name: ontology_subject,
60            })
61            .map_err(anyhow_to_pyerr);
62    }
63
64    if let Ok(base_attr) = location.getattr("base") {
65        if !base_attr.is_none() {
66            let base: String = bound_pystring_to_string(base_attr.str()?)?;
67            if !base.is_empty() {
68                if let Ok(loc) = OntologyLocation::from_str(&base) {
69                    return Ok(ResolvedLocation {
70                        location: loc,
71                        preferred_name: ontology_subject,
72                    });
73                }
74            }
75        }
76    }
77
78    if let Ok(identifier_attr) = location.getattr("identifier") {
79        if !identifier_attr.is_none() {
80            let identifier_str: String = bound_pystring_to_string(identifier_attr.str()?)?;
81            if !identifier_str.is_empty()
82                && (identifier_str.starts_with("file:") || Path::new(&identifier_str).exists())
83            {
84                if let Ok(loc) = OntologyLocation::from_str(&identifier_str) {
85                    return Ok(ResolvedLocation {
86                        location: loc,
87                        preferred_name: ontology_subject,
88                    });
89                }
90            }
91        }
92    }
93
94    if location.hasattr("serialize")? {
95        let identifier = ontology_subject
96            .clone()
97            .unwrap_or_else(generate_rdflib_graph_identifier);
98        return Ok(ResolvedLocation {
99            location: OntologyLocation::InMemory { identifier },
100            preferred_name: ontology_subject,
101        });
102    }
103
104    let as_string: String = bound_pystring_to_string(location.str()?)?;
105
106    if as_string.starts_with("file:") || Path::new(&as_string).exists() {
107        return OntologyLocation::from_str(&as_string)
108            .map(|loc| ResolvedLocation {
109                location: loc,
110                preferred_name: ontology_subject,
111            })
112            .map_err(anyhow_to_pyerr);
113    }
114
115    Ok(ResolvedLocation {
116        location: OntologyLocation::Url(generate_rdflib_graph_identifier()),
117        preferred_name: ontology_subject,
118    })
119}
120
121fn generate_rdflib_graph_identifier() -> String {
122    format!("rdflib:graph-{}", random_hex_suffix())
123}
124
125fn random_hex_suffix() -> String {
126    format!("{:08x}", random::<u32>())
127}
128
129fn extract_ontology_subject(graph: &Bound<'_, PyAny>) -> PyResult<Option<String>> {
130    if !graph.hasattr("subjects")? {
131        return Ok(None);
132    }
133
134    let py = graph.py();
135    let namespace = PyModule::import(py, "rdflib.namespace")?;
136    let rdf = namespace.getattr("RDF")?;
137    let rdf_type = rdf.getattr("type")?;
138    let owl = namespace.getattr("OWL")?;
139    let ontology_term = match owl.getattr("Ontology") {
140        Ok(term) => term,
141        Err(_) => owl.call_method1("__getitem__", ("Ontology",))?,
142    };
143
144    let subjects_iter = graph.call_method1("subjects", (rdf_type, ontology_term))?;
145    let mut iterator = PyIterator::from_object(&subjects_iter)?;
146
147    if let Some(first_res) = iterator.next() {
148        let first = first_res?;
149        let subject_str = bound_pystring_to_string(first.str()?)?;
150        if !subject_str.is_empty() {
151            return Ok(Some(subject_str));
152        }
153    }
154
155    Ok(None)
156}
157
158// Helper function to format paths with forward slashes for cross-platform error messages
159#[allow(dead_code)]
160struct MyTerm(Term);
161impl From<Result<Bound<'_, PyAny>, pyo3::PyErr>> for MyTerm {
162    fn from(s: Result<Bound<'_, PyAny>, pyo3::PyErr>) -> Self {
163        let s = s.unwrap();
164        let typestr = s.get_type().name().unwrap();
165        let typestr = typestr.to_string();
166        let data_type: Option<NamedNode> = match s.getattr("datatype") {
167            Ok(dt) => {
168                if dt.is_none() {
169                    None
170                } else {
171                    Some(NamedNode::new(dt.to_string()).unwrap())
172                }
173            }
174            Err(_) => None,
175        };
176        let lang: Option<String> = match s.getattr("language") {
177            Ok(l) => {
178                if l.is_none() {
179                    None
180                } else {
181                    Some(l.to_string())
182                }
183            }
184            Err(_) => None,
185        };
186        let n: Term = match typestr.borrow() {
187            "URIRef" => Term::NamedNode(NamedNode::new(s.to_string()).unwrap()),
188            "Literal" => match (data_type, lang) {
189                (Some(dt), None) => Term::Literal(Literal::new_typed_literal(s.to_string(), dt)),
190                (None, Some(l)) => {
191                    Term::Literal(Literal::new_language_tagged_literal(s.to_string(), l).unwrap())
192                }
193                (_, _) => Term::Literal(Literal::new_simple_literal(s.to_string())),
194            },
195            "BNode" => Term::BlankNode(BlankNode::new(s.to_string()).unwrap()),
196            _ => Term::NamedNode(NamedNode::new(s.to_string()).unwrap()),
197        };
198        MyTerm(n)
199    }
200}
201
202fn term_to_python<'a>(
203    py: Python,
204    rdflib: &Bound<'a, PyModule>,
205    node: Term,
206) -> PyResult<Bound<'a, PyAny>> {
207    let dtype: Option<String> = match &node {
208        Term::Literal(lit) => {
209            let mut s = lit.datatype().to_string();
210            s.remove(0);
211            s.remove(s.len() - 1);
212            Some(s)
213        }
214        _ => None,
215    };
216    let lang: Option<&str> = match &node {
217        Term::Literal(lit) => lit.language(),
218        _ => None,
219    };
220
221    let res: Bound<'_, PyAny> = match &node {
222        Term::NamedNode(uri) => {
223            let mut uri = uri.to_string();
224            uri.remove(0);
225            uri.remove(uri.len() - 1);
226            rdflib.getattr("URIRef")?.call1((uri,))?
227        }
228        Term::Literal(literal) => {
229            match (dtype, lang) {
230                // prioritize 'lang' -> it implies String
231                (_, Some(lang)) => {
232                    rdflib
233                        .getattr("Literal")?
234                        .call1((literal.value(), lang, py.None()))?
235                }
236                (Some(dtype), None) => {
237                    rdflib
238                        .getattr("Literal")?
239                        .call1((literal.value(), py.None(), dtype))?
240                }
241                (None, None) => rdflib.getattr("Literal")?.call1((literal.value(),))?,
242            }
243        }
244        Term::BlankNode(id) => rdflib
245            .getattr("BNode")?
246            .call1((id.clone().into_string(),))?,
247    };
248    Ok(res)
249}
250
251fn bound_pystring_to_string(py_str: Bound<'_, PyString>) -> PyResult<String> {
252    Ok(py_str.to_cow()?.into_owned())
253}
254
255/// Run the Rust CLI implementation and return its process-style exit code.
256#[pyfunction]
257#[cfg(feature = "cli")]
258fn run_cli(py: Python<'_>, args: Option<Vec<String>>) -> PyResult<i32> {
259    let argv = args.unwrap_or_else(|| std::env::args().collect());
260    let code = py.detach(move || match ontoenv_cli::run_from_args(argv) {
261        Ok(()) => 0,
262        Err(err) => {
263            eprintln!("{err}");
264            1
265        }
266    });
267    Ok(code)
268}
269
270/// Fallback stub when the CLI feature is disabled at compile time.
271#[pyfunction]
272#[cfg(not(feature = "cli"))]
273#[allow(unused_variables)]
274fn run_cli(_py: Python<'_>, _args: Option<Vec<String>>) -> PyResult<i32> {
275    Err(PyErr::new::<PyRuntimeError, _>(
276        "ontoenv was built without CLI support; rebuild with the 'cli' feature",
277    ))
278}
279
280#[pyclass(name = "Ontology")]
281#[derive(Clone)]
282struct PyOntology {
283    inner: OntologyRs,
284}
285
286#[pymethods]
287impl PyOntology {
288    #[getter]
289    fn id(&self) -> PyResult<String> {
290        Ok(self.inner.id().to_uri_string())
291    }
292
293    #[getter]
294    fn name(&self) -> PyResult<String> {
295        Ok(self.inner.name().to_uri_string())
296    }
297
298    #[getter]
299    fn imports(&self) -> PyResult<Vec<String>> {
300        Ok(self
301            .inner
302            .imports
303            .iter()
304            .map(|i| i.to_uri_string())
305            .collect())
306    }
307
308    #[getter]
309    fn location(&self) -> PyResult<Option<String>> {
310        Ok(self.inner.location().map(|l| l.to_string()))
311    }
312
313    #[getter]
314    fn last_updated(&self) -> PyResult<Option<String>> {
315        Ok(self.inner.last_updated.map(|dt| dt.to_rfc3339()))
316    }
317
318    #[getter]
319    fn version_properties(&self) -> PyResult<HashMap<String, String>> {
320        Ok(self
321            .inner
322            .version_properties()
323            .iter()
324            .map(|(k, v)| (k.to_uri_string(), v.clone()))
325            .collect())
326    }
327
328    #[getter]
329    fn namespace_map(&self) -> PyResult<HashMap<String, String>> {
330        Ok(self.inner.namespace_map().clone())
331    }
332
333    fn __repr__(&self) -> PyResult<String> {
334        Ok(format!("<Ontology: {}>", self.inner.name().to_uri_string()))
335    }
336}
337
338#[pyclass]
339struct OntoEnv {
340    inner: Arc<Mutex<Option<OntoEnvRs>>>,
341}
342
343#[pymethods]
344impl OntoEnv {
345    #[new]
346    #[pyo3(signature = (path=None, recreate=false, create_or_use_cached=false, read_only=false, search_directories=None, require_ontology_names=false, strict=false, offline=false, use_cached_ontologies=false, resolution_policy="default".to_owned(), root=".".to_owned(), includes=None, excludes=None, include_ontologies=None, exclude_ontologies=None, temporary=false, no_search=false))]
347    fn new(
348        _py: Python,
349        path: Option<PathBuf>,
350        recreate: bool,
351        create_or_use_cached: bool,
352        read_only: bool,
353        search_directories: Option<Vec<String>>,
354        require_ontology_names: bool,
355        strict: bool,
356        offline: bool,
357        use_cached_ontologies: bool,
358        resolution_policy: String,
359        root: String,
360        includes: Option<Vec<String>>,
361        excludes: Option<Vec<String>>,
362        include_ontologies: Option<Vec<String>>,
363        exclude_ontologies: Option<Vec<String>>,
364        temporary: bool,
365        no_search: bool,
366    ) -> PyResult<Self> {
367        let mut root_path = path.clone().unwrap_or_else(|| PathBuf::from(root));
368        // If the provided path points to a '.ontoenv' directory, treat its parent as the root
369        if root_path
370            .file_name()
371            .map(|n| n == OsStr::new(".ontoenv"))
372            .unwrap_or(false)
373        {
374            if let Some(parent) = root_path.parent() {
375                root_path = parent.to_path_buf();
376            }
377        }
378
379        // Strict Git-like behavior:
380        // - temporary=True: create a temporary (in-memory) env
381        // - recreate=True: create (or overwrite) an env at root_path
382        // - create_or_use_cached=True: create if missing, otherwise load
383        // - otherwise: discover upward; if not found, error
384
385        let mut builder = config::Config::builder()
386            .root(root_path.clone())
387            .require_ontology_names(require_ontology_names)
388            .strict(strict)
389            .offline(offline)
390            .use_cached_ontologies(CacheMode::from(use_cached_ontologies))
391            .resolution_policy(resolution_policy)
392            .temporary(temporary)
393            .no_search(no_search);
394
395        if let Some(dirs) = search_directories {
396            let paths = dirs.into_iter().map(PathBuf::from).collect();
397            builder = builder.locations(paths);
398        }
399        if let Some(incl) = includes {
400            builder = builder.includes(incl);
401        }
402        if let Some(excl) = excludes {
403            builder = builder.excludes(excl);
404        }
405        if let Some(incl_o) = include_ontologies {
406            builder = builder.include_ontologies(incl_o);
407        }
408        if let Some(excl_o) = exclude_ontologies {
409            builder = builder.exclude_ontologies(excl_o);
410        }
411
412        let cfg = builder
413            .build()
414            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
415
416        let root_for_lookup = cfg.root.clone();
417        let env = if cfg.temporary {
418            OntoEnvRs::init(cfg, false).map_err(anyhow_to_pyerr)?
419        } else if recreate {
420            OntoEnvRs::init(cfg, true).map_err(anyhow_to_pyerr)?
421        } else if create_or_use_cached {
422            OntoEnvRs::open_or_init(cfg, read_only).map_err(anyhow_to_pyerr)?
423        } else {
424            let load_root = if let Some(found_root) =
425                find_ontoenv_root_from(root_for_lookup.as_path())
426            {
427                found_root
428            } else {
429                let ontoenv_dir = root_for_lookup.join(".ontoenv");
430                if ontoenv_dir.exists() {
431                    root_for_lookup.clone()
432                } else {
433                    return Err(PyErr::new::<pyo3::exceptions::PyFileNotFoundError, _>(
434                        format!(
435                            "OntoEnv directory not found at {} (set create_or_use_cached=True to initialize a new environment)",
436                            ontoenv_dir.display()
437                        ),
438                    ));
439                }
440            };
441            OntoEnvRs::load_from_directory(load_root, read_only).map_err(anyhow_to_pyerr)?
442        };
443
444        let inner = Arc::new(Mutex::new(Some(env)));
445
446        Ok(OntoEnv {
447            inner: inner.clone(),
448        })
449    }
450
451    #[pyo3(signature = (all=false))]
452    fn update(&self, all: bool) -> PyResult<()> {
453        let inner = self.inner.clone();
454        let mut guard = inner.lock().unwrap();
455        if let Some(env) = guard.as_mut() {
456            env.update_all(all).map_err(anyhow_to_pyerr)?;
457            env.save_to_directory().map_err(anyhow_to_pyerr)
458        } else {
459            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
460                "OntoEnv is closed",
461            ))
462        }
463    }
464
465    // fn is_read_only(&self) -> PyResult<bool> {
466    //     let inner = self.inner.clone();
467    //     let env = inner.lock().unwrap();
468    //     Ok(env.is_read_only())
469    // }
470
471    fn __repr__(&self) -> PyResult<String> {
472        let inner = self.inner.clone();
473        let guard = inner.lock().unwrap();
474        if let Some(env) = guard.as_ref() {
475            let stats = env.stats().map_err(anyhow_to_pyerr)?;
476            Ok(format!(
477                "<OntoEnv: {} ontologies, {} graphs, {} triples>",
478                stats.num_ontologies, stats.num_graphs, stats.num_triples,
479            ))
480        } else {
481            Ok("<OntoEnv: closed>".to_string())
482        }
483    }
484
485    // The following methods will now access the inner OntoEnv in a thread-safe manner:
486
487    #[pyo3(signature = (destination_graph, uri, recursion_depth = -1))]
488    fn import_graph(
489        &self,
490        py: Python,
491        destination_graph: &Bound<'_, PyAny>,
492        uri: &str,
493        recursion_depth: i32,
494    ) -> PyResult<()> {
495        let inner = self.inner.clone();
496        let mut guard = inner.lock().unwrap();
497        let env = guard
498            .as_mut()
499            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
500        let rdflib = py.import("rdflib")?;
501        let iri = NamedNode::new(uri)
502            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
503        let graphid = env
504            .resolve(ResolveTarget::Graph(iri.clone()))
505            .ok_or_else(|| {
506                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
507                    "Failed to resolve graph for URI: {uri}"
508                ))
509            })?;
510
511        // Compute closure starting from this ontology, honoring recursion depth and deduping loops.
512        let closure = env
513            .get_closure(&graphid, recursion_depth)
514            .map_err(anyhow_to_pyerr)?;
515
516        // Determine root ontology: prefer an existing ontology in the destination graph; else use the
517        // imported ontology name.
518        let uriref_constructor = rdflib.getattr("URIRef")?;
519        let type_uri = uriref_constructor.call1((TYPE.as_str(),))?;
520        let ontology_uri = uriref_constructor.call1((ONTOLOGY.as_str(),))?;
521        let kwargs = [("predicate", type_uri), ("object", ontology_uri)].into_py_dict(py)?;
522        let existing_root = destination_graph.call_method("value", (), Some(&kwargs))?;
523        let root_node_owned: oxigraph::model::NamedNode = if existing_root.is_none() {
524            graphid.name().into_owned()
525        } else {
526            NamedNode::new(existing_root.extract::<String>()?)
527                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?
528                .to_owned()
529        };
530        let root_node = root_node_owned.as_ref();
531
532        // Remove owl:imports in the destination graph only for ontologies that will be rewritten.
533        let imports_uri = uriref_constructor.call1((IMPORTS.as_str(),))?;
534        let closure_set: std::collections::HashSet<String> =
535            closure.iter().map(|c| c.to_uri_string()).collect();
536        let triples_to_remove_imports = destination_graph.call_method(
537            "triples",
538            ((py.None(), imports_uri, py.None()),),
539            None,
540        )?;
541        for triple in triples_to_remove_imports.try_iter()? {
542            let t = triple?;
543            let obj: Bound<'_, PyAny> = t.get_item(2)?;
544            if let Ok(s) = obj.str() {
545                if closure_set.contains(s.to_str()?) {
546                    destination_graph.getattr("remove")?.call1((t,))?;
547                }
548            }
549        }
550
551        // Remove any ontology declarations in the destination that are not the chosen root.
552        let triples_to_remove = destination_graph.call_method(
553            "triples",
554            ((
555                py.None(),
556                uriref_constructor.call1((TYPE.as_str(),))?,
557                uriref_constructor.call1((ONTOLOGY.as_str(),))?,
558            ),),
559            None,
560        )?;
561        for triple in triples_to_remove.try_iter()? {
562            let t = triple?;
563            let subj: Bound<'_, PyAny> = t.get_item(0)?;
564            if subj.str()?.to_str()? != root_node.as_str() {
565                destination_graph.getattr("remove")?.call1((t,))?;
566            }
567        }
568
569        // Merge closure graphs via the Rust API, choosing the caller's root.
570        let merged = env
571            .import_graph_with_root(&graphid, recursion_depth, root_node)
572            .map_err(anyhow_to_pyerr)?;
573
574        // Flatten triples into the destination graph.
575        for triple in merged.into_iter() {
576            let s: Term = triple.subject.into();
577            let p: Term = triple.predicate.into();
578            let o: Term = triple.object.into();
579            let t = PyTuple::new(
580                py,
581                &[
582                    term_to_python(py, &rdflib, s)?,
583                    term_to_python(py, &rdflib, p)?,
584                    term_to_python(py, &rdflib, o)?,
585                ],
586            )?;
587            destination_graph.getattr("add")?.call1((t,))?;
588        }
589        // Re-attach imports from the original closure onto the root in the destination graph.
590        for dep in closure.iter().skip(1) {
591            let dep_uri = dep.to_uri_string();
592            let t = PyTuple::new(
593                py,
594                &[
595                    uriref_constructor.call1((root_node.as_str(),))?,
596                    uriref_constructor.call1((IMPORTS.as_str(),))?,
597                    uriref_constructor.call1((dep_uri.as_str(),))?,
598                ],
599            )?;
600            destination_graph.getattr("add")?.call1((t,))?;
601        }
602        Ok(())
603    }
604
605    /// List the ontologies in the imports closure of the given ontology
606    #[pyo3(signature = (uri, recursion_depth = -1))]
607    fn list_closure(&self, _py: Python, uri: &str, recursion_depth: i32) -> PyResult<Vec<String>> {
608        let iri = NamedNode::new(uri)
609            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
610        let inner = self.inner.clone();
611        let mut guard = inner.lock().unwrap();
612        let env = guard
613            .as_mut()
614            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
615        let graphid = env
616            .resolve(ResolveTarget::Graph(iri.clone()))
617            .ok_or_else(|| {
618                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
619                    "Failed to resolve graph for URI: {uri}"
620                ))
621            })?;
622        let ont = env.ontologies().get(&graphid).ok_or_else(|| {
623            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
624        })?;
625        let closure = env
626            .get_closure(ont.id(), recursion_depth)
627            .map_err(anyhow_to_pyerr)?;
628        let names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
629        Ok(names)
630    }
631
632    /// Merge the imports closure of `uri` into a single graph and return it alongside the closure list.
633    ///
634    /// The first element of the returned tuple is either the provided `destination_graph` (after
635    /// mutation) or a brand-new `rdflib.Graph`. The second element is an ordered list of ontology
636    /// IRIs in the resolved closure starting with `uri`. Set `rewrite_sh_prefixes` or
637    /// `remove_owl_imports` to control post-processing of the merged triples.
638    #[pyo3(signature = (uri, destination_graph=None, rewrite_sh_prefixes=true, remove_owl_imports=true, recursion_depth=-1))]
639    fn get_closure<'a>(
640        &self,
641        py: Python<'a>,
642        uri: &str,
643        destination_graph: Option<&Bound<'a, PyAny>>,
644        rewrite_sh_prefixes: bool,
645        remove_owl_imports: bool,
646        recursion_depth: i32,
647    ) -> PyResult<(Bound<'a, PyAny>, Vec<String>)> {
648        let rdflib = py.import("rdflib")?;
649        let iri = NamedNode::new(uri)
650            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
651        let inner = self.inner.clone();
652        let mut guard = inner.lock().unwrap();
653        let env = guard
654            .as_mut()
655            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
656        let graphid = env
657            .resolve(ResolveTarget::Graph(iri.clone()))
658            .ok_or_else(|| {
659                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("No graph with URI: {uri}"))
660            })?;
661        let ont = env.ontologies().get(&graphid).ok_or_else(|| {
662            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
663        })?;
664        let closure = env
665            .get_closure(ont.id(), recursion_depth)
666            .map_err(anyhow_to_pyerr)?;
667        let closure_names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
668        // if destination_graph is null, create a new rdflib.Graph()
669        let destination_graph = match destination_graph {
670            Some(g) => g.clone(),
671            None => rdflib.getattr("Graph")?.call0()?,
672        };
673        let union = env
674            .get_union_graph(
675                &closure,
676                Some(rewrite_sh_prefixes),
677                Some(remove_owl_imports),
678            )
679            .map_err(anyhow_to_pyerr)?;
680        for triple in union.dataset.into_iter() {
681            let s: Term = triple.subject.into();
682            let p: Term = triple.predicate.into();
683            let o: Term = triple.object.into();
684            let t = PyTuple::new(
685                py,
686                &[
687                    term_to_python(py, &rdflib, s)?,
688                    term_to_python(py, &rdflib, p)?,
689                    term_to_python(py, &rdflib, o)?,
690                ],
691            )?;
692            destination_graph.getattr("add")?.call1((t,))?;
693        }
694
695        // Remove each successful_imports url in the closure from the destination_graph
696        if remove_owl_imports {
697            for graphid in union.graph_ids {
698                let iri = term_to_python(py, &rdflib, Term::NamedNode(graphid.into()))?;
699                let pred = term_to_python(py, &rdflib, IMPORTS.into())?;
700                // remove triples with (None, pred, iri)
701                let remove_tuple = PyTuple::new(py, &[py.None(), pred.into(), iri.into()])?;
702                destination_graph
703                    .getattr("remove")?
704                    .call1((remove_tuple,))?;
705            }
706        }
707        Ok((destination_graph, closure_names))
708    }
709
710    /// Print the contents of the OntoEnv
711    #[pyo3(signature = (includes=None))]
712    fn dump(&self, _py: Python, includes: Option<String>) -> PyResult<()> {
713        let inner = self.inner.clone();
714        let guard = inner.lock().unwrap();
715        if let Some(env) = guard.as_ref() {
716            env.dump(includes.as_deref());
717            Ok(())
718        } else {
719            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
720                "OntoEnv is closed",
721            ))
722        }
723    }
724
725    /// Import the dependencies referenced by `owl:imports` triples in `graph`.
726    ///
727    /// When `fetch_missing` is true, the environment attempts to download unresolved imports
728    /// before computing the closure. After merging the closure triples into `graph`, all
729    /// `owl:imports` statements are removed. The returned list contains the deduplicated ontology
730    /// IRIs that were successfully imported.
731    #[pyo3(signature = (graph, recursion_depth=-1, fetch_missing=false))]
732    fn import_dependencies<'a>(
733        &self,
734        py: Python<'a>,
735        graph: &Bound<'a, PyAny>,
736        recursion_depth: i32,
737        fetch_missing: bool,
738    ) -> PyResult<Vec<String>> {
739        let rdflib = py.import("rdflib")?;
740        let py_imports_pred = term_to_python(py, &rdflib, Term::NamedNode(IMPORTS.into()))?;
741
742        let kwargs = [("predicate", py_imports_pred)].into_py_dict(py)?;
743        let objects_iter = graph.call_method("objects", (), Some(&kwargs))?;
744        let builtins = py.import("builtins")?;
745        let objects_list = builtins.getattr("list")?.call1((objects_iter,))?;
746        let imports: Vec<String> = objects_list.extract()?;
747
748        if imports.is_empty() {
749            return Ok(Vec::new());
750        }
751
752        let inner = self.inner.clone();
753        let mut guard = inner.lock().unwrap();
754        let env = guard
755            .as_mut()
756            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
757
758        let is_strict = env.is_strict();
759        let mut all_ontologies = HashSet::new();
760        let mut all_closure_names: Vec<String> = Vec::new();
761
762        for uri in &imports {
763            let iri = NamedNode::new(uri.as_str())
764                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
765
766            let mut graphid = env.resolve(ResolveTarget::Graph(iri.clone()));
767
768            if graphid.is_none() && fetch_missing {
769                let location = OntologyLocation::from_str(uri.as_str()).map_err(anyhow_to_pyerr)?;
770                match env.add(location, Overwrite::Preserve, RefreshStrategy::UseCache) {
771                    Ok(new_id) => {
772                        graphid = Some(new_id);
773                    }
774                    Err(e) => {
775                        if is_strict {
776                            return Err(anyhow_to_pyerr(e));
777                        }
778                        println!("Failed to fetch {uri}: {e}");
779                    }
780                }
781            }
782
783            let graphid = match graphid {
784                Some(id) => id,
785                None => {
786                    if is_strict {
787                        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
788                            "Failed to resolve graph for URI: {}",
789                            uri
790                        )));
791                    }
792                    println!("Could not find {uri:?}");
793                    continue;
794                }
795            };
796
797            let ont = env.ontologies().get(&graphid).ok_or_else(|| {
798                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
799                    "Ontology {} not found",
800                    uri
801                ))
802            })?;
803
804            let closure = env
805                .get_closure(ont.id(), recursion_depth)
806                .map_err(anyhow_to_pyerr)?;
807            for c_ont in closure {
808                all_closure_names.push(c_ont.to_uri_string());
809                all_ontologies.insert(c_ont.clone());
810            }
811        }
812
813        if all_ontologies.is_empty() {
814            return Ok(Vec::new());
815        }
816
817        let union = env
818            .get_union_graph(&all_ontologies, Some(true), Some(true))
819            .map_err(anyhow_to_pyerr)?;
820
821        for triple in union.dataset.into_iter() {
822            let s: Term = triple.subject.into();
823            let p: Term = triple.predicate.into();
824            let o: Term = triple.object.into();
825            let t = PyTuple::new(
826                py,
827                &[
828                    term_to_python(py, &rdflib, s)?,
829                    term_to_python(py, &rdflib, p)?,
830                    term_to_python(py, &rdflib, o)?,
831                ],
832            )?;
833            graph.getattr("add")?.call1((t,))?;
834        }
835
836        // Remove all owl:imports from the original graph
837        let py_imports_pred_for_remove = term_to_python(py, &rdflib, IMPORTS.into())?;
838        let remove_tuple = PyTuple::new(
839            py,
840            &[py.None(), py_imports_pred_for_remove.into(), py.None()],
841        )?;
842        graph.getattr("remove")?.call1((remove_tuple,))?;
843
844        all_closure_names.sort();
845        all_closure_names.dedup();
846
847        Ok(all_closure_names)
848    }
849
850    /// Get the dependency closure of a given graph and return it as a new graph.
851    ///
852    /// This method will look for `owl:imports` statements in the provided `graph`,
853    /// then find those ontologies within the `OntoEnv` and compute the full
854    /// dependency closure. The triples of all ontologies in the closure are
855    /// returned as a new graph. The original `graph` is left untouched unless you also
856    /// supply it as the `destination_graph`.
857    ///
858    /// Args:
859    ///     graph (rdflib.Graph): The graph to find dependencies for.
860    ///     destination_graph (Optional[rdflib.Graph]): If provided, the dependency graph will be added to this
861    ///         graph instead of creating a new one.
862    ///     recursion_depth (int): The maximum depth for recursive import resolution. A
863    ///         negative value (default) means no limit.
864    ///     fetch_missing (bool): If True, will fetch ontologies that are not in the environment.
865    ///     rewrite_sh_prefixes (bool): If True, will rewrite SHACL prefixes to be unique.
866    ///     remove_owl_imports (bool): If True, will remove `owl:imports` statements from the
867    ///         returned graph.
868    ///
869    /// Returns:
870    ///     tuple[rdflib.Graph, list[str]]: A tuple containing the populated dependency graph and the sorted list of
871    ///     imported ontology IRIs.
872    #[pyo3(signature = (graph, destination_graph=None, recursion_depth=-1, fetch_missing=false, rewrite_sh_prefixes=true, remove_owl_imports=true))]
873    fn get_dependencies_graph<'a>(
874        &self,
875        py: Python<'a>,
876        graph: &Bound<'a, PyAny>,
877        destination_graph: Option<&Bound<'a, PyAny>>,
878        recursion_depth: i32,
879        fetch_missing: bool,
880        rewrite_sh_prefixes: bool,
881        remove_owl_imports: bool,
882    ) -> PyResult<(Bound<'a, PyAny>, Vec<String>)> {
883        let rdflib = py.import("rdflib")?;
884        let py_imports_pred = term_to_python(py, &rdflib, Term::NamedNode(IMPORTS.into()))?;
885
886        let kwargs = [("predicate", py_imports_pred)].into_py_dict(py)?;
887        let objects_iter = graph.call_method("objects", (), Some(&kwargs))?;
888        let builtins = py.import("builtins")?;
889        let objects_list = builtins.getattr("list")?.call1((objects_iter,))?;
890        let imports: Vec<String> = objects_list.extract()?;
891
892        let destination_graph = match destination_graph {
893            Some(g) => g.clone(),
894            None => rdflib.getattr("Graph")?.call0()?,
895        };
896
897        if imports.is_empty() {
898            return Ok((destination_graph, Vec::new()));
899        }
900
901        let inner = self.inner.clone();
902        let mut guard = inner.lock().unwrap();
903        let env = guard
904            .as_mut()
905            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
906
907        let is_strict = env.is_strict();
908        let mut all_ontologies = HashSet::new();
909        let mut all_closure_names: Vec<String> = Vec::new();
910
911        for uri in &imports {
912            let iri = NamedNode::new(uri.as_str())
913                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
914
915            let mut graphid = env.resolve(ResolveTarget::Graph(iri.clone()));
916
917            if graphid.is_none() && fetch_missing {
918                let location = OntologyLocation::from_str(uri.as_str()).map_err(anyhow_to_pyerr)?;
919                match env.add(location, Overwrite::Preserve, RefreshStrategy::UseCache) {
920                    Ok(new_id) => {
921                        graphid = Some(new_id);
922                    }
923                    Err(e) => {
924                        if is_strict {
925                            return Err(anyhow_to_pyerr(e));
926                        }
927                        println!("Failed to fetch {uri}: {e}");
928                    }
929                }
930            }
931
932            let graphid = match graphid {
933                Some(id) => id,
934                None => {
935                    if is_strict {
936                        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
937                            "Failed to resolve graph for URI: {}",
938                            uri
939                        )));
940                    }
941                    println!("Could not find {uri:?}");
942                    continue;
943                }
944            };
945
946            let ont = env.ontologies().get(&graphid).ok_or_else(|| {
947                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
948                    "Ontology {} not found",
949                    uri
950                ))
951            })?;
952
953            let closure = env
954                .get_closure(ont.id(), recursion_depth)
955                .map_err(anyhow_to_pyerr)?;
956            for c_ont in closure {
957                all_closure_names.push(c_ont.to_uri_string());
958                all_ontologies.insert(c_ont.clone());
959            }
960        }
961
962        if all_ontologies.is_empty() {
963            return Ok((destination_graph, Vec::new()));
964        }
965
966        let union = env
967            .get_union_graph(
968                &all_ontologies,
969                Some(rewrite_sh_prefixes),
970                Some(remove_owl_imports),
971            )
972            .map_err(anyhow_to_pyerr)?;
973
974        for triple in union.dataset.into_iter() {
975            let s: Term = triple.subject.into();
976            let p: Term = triple.predicate.into();
977            let o: Term = triple.object.into();
978            let t = PyTuple::new(
979                py,
980                &[
981                    term_to_python(py, &rdflib, s)?,
982                    term_to_python(py, &rdflib, p)?,
983                    term_to_python(py, &rdflib, o)?,
984                ],
985            )?;
986            destination_graph.getattr("add")?.call1((t,))?;
987        }
988
989        if remove_owl_imports {
990            for graphid in union.graph_ids {
991                let iri = term_to_python(py, &rdflib, Term::NamedNode(graphid.into()))?;
992                let pred = term_to_python(py, &rdflib, IMPORTS.into())?;
993                let remove_tuple = PyTuple::new(py, &[py.None(), pred.into(), iri.into()])?;
994                destination_graph
995                    .getattr("remove")?
996                    .call1((remove_tuple,))?;
997            }
998        }
999
1000        all_closure_names.sort();
1001        all_closure_names.dedup();
1002
1003        Ok((destination_graph, all_closure_names))
1004    }
1005
1006    /// Add a new ontology to the OntoEnv
1007    #[pyo3(signature = (location, overwrite = false, fetch_imports = true, force = false))]
1008    fn add(
1009        &self,
1010        location: &Bound<'_, PyAny>,
1011        overwrite: bool,
1012        fetch_imports: bool,
1013        force: bool,
1014    ) -> PyResult<String> {
1015        let inner = self.inner.clone();
1016        let mut guard = inner.lock().unwrap();
1017        let env = guard
1018            .as_mut()
1019            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1020
1021        let resolved = ontology_location_from_py(location)?;
1022        if matches!(resolved.location, OntologyLocation::InMemory { .. }) {
1023            return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1024                "In-memory rdflib graphs cannot be added to the environment",
1025            ));
1026        }
1027        let preferred_name = resolved.preferred_name.clone();
1028        let location = resolved.location;
1029        let overwrite_flag: Overwrite = overwrite.into();
1030        let refresh: RefreshStrategy = force.into();
1031        let graph_id = if fetch_imports {
1032            env.add(location, overwrite_flag, refresh)
1033        } else {
1034            env.add_no_imports(location, overwrite_flag, refresh)
1035        }
1036        .map_err(anyhow_to_pyerr)?;
1037        let actual_name = graph_id.to_uri_string();
1038        if let Some(pref) = preferred_name {
1039            if let Ok(candidate) = NamedNode::new(pref.clone()) {
1040                if env.resolve(ResolveTarget::Graph(candidate)).is_some() {
1041                    return Ok(pref);
1042                }
1043            }
1044        }
1045        Ok(actual_name)
1046    }
1047
1048    /// Add a new ontology to the OntoEnv without exploring owl:imports.
1049    #[pyo3(signature = (location, overwrite = false, force = false))]
1050    fn add_no_imports(
1051        &self,
1052        location: &Bound<'_, PyAny>,
1053        overwrite: bool,
1054        force: bool,
1055    ) -> PyResult<String> {
1056        let inner = self.inner.clone();
1057        let mut guard = inner.lock().unwrap();
1058        let env = guard
1059            .as_mut()
1060            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1061        let resolved = ontology_location_from_py(location)?;
1062        if matches!(resolved.location, OntologyLocation::InMemory { .. }) {
1063            return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1064                "In-memory rdflib graphs cannot be added to the environment",
1065            ));
1066        }
1067        let preferred_name = resolved.preferred_name.clone();
1068        let location = resolved.location;
1069        let overwrite_flag: Overwrite = overwrite.into();
1070        let refresh: RefreshStrategy = force.into();
1071        let graph_id = env
1072            .add_no_imports(location, overwrite_flag, refresh)
1073            .map_err(anyhow_to_pyerr)?;
1074        let actual_name = graph_id.to_uri_string();
1075        if let Some(pref) = preferred_name {
1076            if let Ok(candidate) = NamedNode::new(pref.clone()) {
1077                if env.resolve(ResolveTarget::Graph(candidate)).is_some() {
1078                    return Ok(pref);
1079                }
1080            }
1081        }
1082        Ok(actual_name)
1083    }
1084
1085    /// Get the names of all ontologies that import the given ontology
1086    fn get_importers(&self, uri: &str) -> PyResult<Vec<String>> {
1087        let iri = NamedNode::new(uri)
1088            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1089        let inner = self.inner.clone();
1090        let guard = inner.lock().unwrap();
1091        let env = guard
1092            .as_ref()
1093            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1094        let importers = env.get_importers(&iri).map_err(anyhow_to_pyerr)?;
1095        let names: Vec<String> = importers.iter().map(|ont| ont.to_uri_string()).collect();
1096        Ok(names)
1097    }
1098
1099    /// Get the ontology metadata with the given URI
1100    fn get_ontology(&self, uri: &str) -> PyResult<PyOntology> {
1101        let iri = NamedNode::new(uri)
1102            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1103        let inner = self.inner.clone();
1104        let guard = inner.lock().unwrap();
1105        let env = guard
1106            .as_ref()
1107            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1108        let graphid = env
1109            .resolve(ResolveTarget::Graph(iri.clone()))
1110            .ok_or_else(|| {
1111                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
1112                    "Failed to resolve graph for URI: {uri}"
1113                ))
1114            })?;
1115        let ont = env.get_ontology(&graphid).map_err(anyhow_to_pyerr)?;
1116        Ok(PyOntology { inner: ont })
1117    }
1118
1119    /// Get the graph with the given URI as an rdflib.Graph
1120    fn get_graph(&self, py: Python, uri: &Bound<'_, PyString>) -> PyResult<Py<PyAny>> {
1121        let rdflib = py.import("rdflib")?;
1122        let iri = NamedNode::new(uri.to_string())
1123            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1124        let graph = {
1125            let inner = self.inner.clone();
1126            let guard = inner.lock().unwrap();
1127            let env = guard.as_ref().ok_or_else(|| {
1128                PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
1129            })?;
1130            let graphid = env.resolve(ResolveTarget::Graph(iri)).ok_or_else(|| {
1131                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
1132                    "Failed to resolve graph for URI: {uri}"
1133                ))
1134            })?;
1135
1136            env.get_graph(&graphid).map_err(anyhow_to_pyerr)?
1137        };
1138        let res = rdflib.getattr("Graph")?.call0()?;
1139        for triple in graph.into_iter() {
1140            let s: Term = triple.subject.into();
1141            let p: Term = triple.predicate.into();
1142            let o: Term = triple.object.into();
1143
1144            let t = PyTuple::new(
1145                py,
1146                &[
1147                    term_to_python(py, &rdflib, s)?,
1148                    term_to_python(py, &rdflib, p)?,
1149                    term_to_python(py, &rdflib, o)?,
1150                ],
1151            )?;
1152
1153            res.getattr("add")?.call1((t,))?;
1154        }
1155        Ok(res.into())
1156    }
1157
1158    /// Get the names of all ontologies in the OntoEnv
1159    fn get_ontology_names(&self) -> PyResult<Vec<String>> {
1160        let inner = self.inner.clone();
1161        let guard = inner.lock().unwrap();
1162        let env = guard
1163            .as_ref()
1164            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1165        let names: Vec<String> = env.ontologies().keys().map(|k| k.to_uri_string()).collect();
1166        Ok(names)
1167    }
1168
1169    /// Convert the OntoEnv to an in-memory rdflib.Dataset populated with all named graphs
1170    fn to_rdflib_dataset(&self, py: Python) -> PyResult<Py<PyAny>> {
1171        let inner = self.inner.clone();
1172        let guard = inner.lock().unwrap();
1173        let env = guard
1174            .as_ref()
1175            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1176        let rdflib = py.import("rdflib")?;
1177        let dataset_cls = rdflib.getattr("Dataset")?;
1178        let ds = dataset_cls.call0()?;
1179        let uriref = rdflib.getattr("URIRef")?;
1180
1181        for (_gid, ont) in env.ontologies().iter() {
1182            let id_str = ont.id().name().as_str();
1183            let id_py = uriref.call1((id_str,))?;
1184            let kwargs = [("identifier", id_py.clone())].into_py_dict(py)?;
1185            let ctx = ds.getattr("graph")?.call((), Some(&kwargs))?;
1186
1187            let graph = env.get_graph(ont.id()).map_err(anyhow_to_pyerr)?;
1188            for t in graph.iter() {
1189                let s: Term = t.subject.into();
1190                let p: Term = t.predicate.into();
1191                let o: Term = t.object.into();
1192                let triple = PyTuple::new(
1193                    py,
1194                    &[
1195                        term_to_python(py, &rdflib, s)?,
1196                        term_to_python(py, &rdflib, p)?,
1197                        term_to_python(py, &rdflib, o)?,
1198                    ],
1199                )?;
1200                ctx.getattr("add")?.call1((triple,))?;
1201            }
1202        }
1203
1204        Ok(ds.into())
1205    }
1206
1207    // Config accessors
1208    fn is_offline(&self) -> PyResult<bool> {
1209        let inner = self.inner.clone();
1210        let guard = inner.lock().unwrap();
1211        if let Some(env) = guard.as_ref() {
1212            Ok(env.is_offline())
1213        } else {
1214            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1215                "OntoEnv is closed",
1216            ))
1217        }
1218    }
1219
1220    fn set_offline(&mut self, offline: bool) -> PyResult<()> {
1221        let inner = self.inner.clone();
1222        let mut guard = inner.lock().unwrap();
1223        if let Some(env) = guard.as_mut() {
1224            env.set_offline(offline);
1225            env.save_to_directory().map_err(anyhow_to_pyerr)
1226        } else {
1227            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1228                "OntoEnv is closed",
1229            ))
1230        }
1231    }
1232
1233    fn is_strict(&self) -> PyResult<bool> {
1234        let inner = self.inner.clone();
1235        let guard = inner.lock().unwrap();
1236        if let Some(env) = guard.as_ref() {
1237            Ok(env.is_strict())
1238        } else {
1239            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1240                "OntoEnv is closed",
1241            ))
1242        }
1243    }
1244
1245    fn set_strict(&mut self, strict: bool) -> PyResult<()> {
1246        let inner = self.inner.clone();
1247        let mut guard = inner.lock().unwrap();
1248        if let Some(env) = guard.as_mut() {
1249            env.set_strict(strict);
1250            env.save_to_directory().map_err(anyhow_to_pyerr)
1251        } else {
1252            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1253                "OntoEnv is closed",
1254            ))
1255        }
1256    }
1257
1258    fn requires_ontology_names(&self) -> PyResult<bool> {
1259        let inner = self.inner.clone();
1260        let guard = inner.lock().unwrap();
1261        if let Some(env) = guard.as_ref() {
1262            Ok(env.requires_ontology_names())
1263        } else {
1264            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1265                "OntoEnv is closed",
1266            ))
1267        }
1268    }
1269
1270    fn set_require_ontology_names(&mut self, require: bool) -> PyResult<()> {
1271        let inner = self.inner.clone();
1272        let mut guard = inner.lock().unwrap();
1273        if let Some(env) = guard.as_mut() {
1274            env.set_require_ontology_names(require);
1275            env.save_to_directory().map_err(anyhow_to_pyerr)
1276        } else {
1277            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1278                "OntoEnv is closed",
1279            ))
1280        }
1281    }
1282
1283    fn no_search(&self) -> PyResult<bool> {
1284        let inner = self.inner.clone();
1285        let guard = inner.lock().unwrap();
1286        if let Some(env) = guard.as_ref() {
1287            Ok(env.no_search())
1288        } else {
1289            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1290                "OntoEnv is closed",
1291            ))
1292        }
1293    }
1294
1295    fn set_no_search(&mut self, no_search: bool) -> PyResult<()> {
1296        let inner = self.inner.clone();
1297        let mut guard = inner.lock().unwrap();
1298        if let Some(env) = guard.as_mut() {
1299            env.set_no_search(no_search);
1300            env.save_to_directory().map_err(anyhow_to_pyerr)
1301        } else {
1302            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1303                "OntoEnv is closed",
1304            ))
1305        }
1306    }
1307
1308    fn resolution_policy(&self) -> PyResult<String> {
1309        let inner = self.inner.clone();
1310        let guard = inner.lock().unwrap();
1311        if let Some(env) = guard.as_ref() {
1312            Ok(env.resolution_policy().to_string())
1313        } else {
1314            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1315                "OntoEnv is closed",
1316            ))
1317        }
1318    }
1319
1320    fn set_resolution_policy(&mut self, policy: String) -> PyResult<()> {
1321        let inner = self.inner.clone();
1322        let mut guard = inner.lock().unwrap();
1323        if let Some(env) = guard.as_mut() {
1324            env.set_resolution_policy(policy);
1325            env.save_to_directory().map_err(anyhow_to_pyerr)
1326        } else {
1327            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1328                "OntoEnv is closed",
1329            ))
1330        }
1331    }
1332
1333    pub fn store_path(&self) -> PyResult<Option<String>> {
1334        let inner = self.inner.clone();
1335        let guard = inner.lock().unwrap();
1336        if let Some(env) = guard.as_ref() {
1337            match env.store_path() {
1338                Some(path) => {
1339                    let dir = path.parent().unwrap_or(path);
1340                    Ok(Some(dir.to_string_lossy().to_string()))
1341                }
1342                None => Ok(None), // Return None if the path doesn't exist (e.g., temporary env)
1343            }
1344        } else {
1345            Ok(None)
1346        }
1347    }
1348
1349    // Wrapper method to raise error if store_path is None, matching previous panic behavior
1350    // but providing a Python-level error. Or tests can check for None.
1351    // Let's keep the Option return type for flexibility and adjust tests.
1352
1353    pub fn close(&mut self, py: Python<'_>) -> PyResult<()> {
1354        py.detach(|| {
1355            let inner = self.inner.clone();
1356            let mut guard = inner.lock().unwrap();
1357            if let Some(env) = guard.as_mut() {
1358                env.save_to_directory().map_err(anyhow_to_pyerr)?;
1359                env.flush().map_err(anyhow_to_pyerr)?;
1360            }
1361            *guard = None;
1362            Ok(())
1363        })
1364    }
1365
1366    pub fn flush(&mut self, py: Python<'_>) -> PyResult<()> {
1367        py.detach(|| {
1368            let inner = self.inner.clone();
1369            let mut guard = inner.lock().unwrap();
1370            if let Some(env) = guard.as_mut() {
1371                env.flush().map_err(anyhow_to_pyerr)
1372            } else {
1373                Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1374                    "OntoEnv is closed",
1375                ))
1376            }
1377        })
1378    }
1379}
1380
1381#[pymodule]
1382fn _native(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
1383    // Initialize logging when the python module is loaded.
1384    ::ontoenv::api::init_logging();
1385    // Use try_init to avoid panic if logging is already initialized.
1386    let _ = env_logger::try_init();
1387
1388    m.add_class::<OntoEnv>()?;
1389    m.add_class::<PyOntology>()?;
1390    m.add_function(wrap_pyfunction!(run_cli, m)?)?;
1391    // add version attribute
1392    m.add("version", env!("CARGO_PKG_VERSION"))?;
1393    Ok(())
1394}