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, remote_cache_ttl_secs=None))]
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        remote_cache_ttl_secs: Option<u64>,
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
394        if let Some(dirs) = search_directories {
395            let paths = dirs.into_iter().map(PathBuf::from).collect();
396            builder = builder.locations(paths);
397        }
398        if let Some(incl) = includes {
399            builder = builder.includes(incl);
400        }
401        if let Some(excl) = excludes {
402            builder = builder.excludes(excl);
403        }
404        if let Some(incl_o) = include_ontologies {
405            builder = builder.include_ontologies(incl_o);
406        }
407        if let Some(excl_o) = exclude_ontologies {
408            builder = builder.exclude_ontologies(excl_o);
409        }
410        if let Some(ttl) = remote_cache_ttl_secs {
411            builder = builder.remote_cache_ttl_secs(ttl);
412        }
413
414        let cfg = builder
415            .build()
416            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
417
418        let root_for_lookup = cfg.root.clone();
419        let env = if cfg.temporary {
420            OntoEnvRs::init(cfg, false).map_err(anyhow_to_pyerr)?
421        } else if recreate {
422            OntoEnvRs::init(cfg, true).map_err(anyhow_to_pyerr)?
423        } else if create_or_use_cached {
424            OntoEnvRs::open_or_init(cfg, read_only).map_err(anyhow_to_pyerr)?
425        } else {
426            let load_root = if let Some(found_root) =
427                find_ontoenv_root_from(root_for_lookup.as_path())
428            {
429                found_root
430            } else {
431                let ontoenv_dir = root_for_lookup.join(".ontoenv");
432                if ontoenv_dir.exists() {
433                    root_for_lookup.clone()
434                } else {
435                    return Err(PyErr::new::<pyo3::exceptions::PyFileNotFoundError, _>(
436                        format!(
437                            "OntoEnv directory not found at {} (set create_or_use_cached=True to initialize a new environment)",
438                            ontoenv_dir.display()
439                        ),
440                    ));
441                }
442            };
443            OntoEnvRs::load_from_directory(load_root, read_only).map_err(anyhow_to_pyerr)?
444        };
445
446        let inner = Arc::new(Mutex::new(Some(env)));
447
448        Ok(OntoEnv {
449            inner: inner.clone(),
450        })
451    }
452
453    #[pyo3(signature = (all=false))]
454    fn update(&self, all: bool) -> PyResult<()> {
455        let inner = self.inner.clone();
456        let mut guard = inner.lock().unwrap();
457        if let Some(env) = guard.as_mut() {
458            env.update_all(all).map_err(anyhow_to_pyerr)?;
459            env.save_to_directory().map_err(anyhow_to_pyerr)
460        } else {
461            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
462                "OntoEnv is closed",
463            ))
464        }
465    }
466
467    // fn is_read_only(&self) -> PyResult<bool> {
468    //     let inner = self.inner.clone();
469    //     let env = inner.lock().unwrap();
470    //     Ok(env.is_read_only())
471    // }
472
473    fn __repr__(&self) -> PyResult<String> {
474        let inner = self.inner.clone();
475        let guard = inner.lock().unwrap();
476        if let Some(env) = guard.as_ref() {
477            let stats = env.stats().map_err(anyhow_to_pyerr)?;
478            Ok(format!(
479                "<OntoEnv: {} ontologies, {} graphs, {} triples>",
480                stats.num_ontologies, stats.num_graphs, stats.num_triples,
481            ))
482        } else {
483            Ok("<OntoEnv: closed>".to_string())
484        }
485    }
486
487    // The following methods will now access the inner OntoEnv in a thread-safe manner:
488
489    #[pyo3(signature = (destination_graph, uri, recursion_depth = -1))]
490    fn import_graph(
491        &self,
492        py: Python,
493        destination_graph: &Bound<'_, PyAny>,
494        uri: &str,
495        recursion_depth: i32,
496    ) -> PyResult<()> {
497        let inner = self.inner.clone();
498        let mut guard = inner.lock().unwrap();
499        let env = guard
500            .as_mut()
501            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
502        let rdflib = py.import("rdflib")?;
503        let iri = NamedNode::new(uri)
504            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
505        let graphid = env
506            .resolve(ResolveTarget::Graph(iri.clone()))
507            .ok_or_else(|| {
508                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
509                    "Failed to resolve graph for URI: {uri}"
510                ))
511            })?;
512
513        // Compute closure starting from this ontology, honoring recursion depth and deduping loops.
514        let closure = env
515            .get_closure(&graphid, recursion_depth)
516            .map_err(anyhow_to_pyerr)?;
517
518        // Determine root ontology: prefer an existing ontology in the destination graph; else use the
519        // imported ontology name.
520        let uriref_constructor = rdflib.getattr("URIRef")?;
521        let type_uri = uriref_constructor.call1((TYPE.as_str(),))?;
522        let ontology_uri = uriref_constructor.call1((ONTOLOGY.as_str(),))?;
523        let kwargs = [("predicate", type_uri), ("object", ontology_uri)].into_py_dict(py)?;
524        let existing_root = destination_graph.call_method("value", (), Some(&kwargs))?;
525        let root_node_owned: oxigraph::model::NamedNode = if existing_root.is_none() {
526            graphid.name().into_owned()
527        } else {
528            NamedNode::new(existing_root.extract::<String>()?)
529                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?
530                .to_owned()
531        };
532        let root_node = root_node_owned.as_ref();
533
534        // Remove owl:imports in the destination graph only for ontologies that will be rewritten.
535        let imports_uri = uriref_constructor.call1((IMPORTS.as_str(),))?;
536        let closure_set: std::collections::HashSet<String> =
537            closure.iter().map(|c| c.to_uri_string()).collect();
538        let triples_to_remove_imports = destination_graph.call_method(
539            "triples",
540            ((py.None(), imports_uri, py.None()),),
541            None,
542        )?;
543        for triple in triples_to_remove_imports.try_iter()? {
544            let t = triple?;
545            let obj: Bound<'_, PyAny> = t.get_item(2)?;
546            if let Ok(s) = obj.str() {
547                if closure_set.contains(s.to_str()?) {
548                    destination_graph.getattr("remove")?.call1((t,))?;
549                }
550            }
551        }
552
553        // Remove any ontology declarations in the destination that are not the chosen root.
554        let triples_to_remove = destination_graph.call_method(
555            "triples",
556            ((
557                py.None(),
558                uriref_constructor.call1((TYPE.as_str(),))?,
559                uriref_constructor.call1((ONTOLOGY.as_str(),))?,
560            ),),
561            None,
562        )?;
563        for triple in triples_to_remove.try_iter()? {
564            let t = triple?;
565            let subj: Bound<'_, PyAny> = t.get_item(0)?;
566            if subj.str()?.to_str()? != root_node.as_str() {
567                destination_graph.getattr("remove")?.call1((t,))?;
568            }
569        }
570
571        // Merge closure graphs via the Rust API, choosing the caller's root.
572        let merged = env
573            .import_graph_with_root(&graphid, recursion_depth, root_node)
574            .map_err(anyhow_to_pyerr)?;
575
576        // Flatten triples into the destination graph.
577        for triple in merged.into_iter() {
578            let s: Term = triple.subject.into();
579            let p: Term = triple.predicate.into();
580            let o: Term = triple.object.into();
581            let t = PyTuple::new(
582                py,
583                &[
584                    term_to_python(py, &rdflib, s)?,
585                    term_to_python(py, &rdflib, p)?,
586                    term_to_python(py, &rdflib, o)?,
587                ],
588            )?;
589            destination_graph.getattr("add")?.call1((t,))?;
590        }
591        // Re-attach imports from the original closure onto the root in the destination graph.
592        for dep in closure.iter().skip(1) {
593            let dep_uri = dep.to_uri_string();
594            let t = PyTuple::new(
595                py,
596                &[
597                    uriref_constructor.call1((root_node.as_str(),))?,
598                    uriref_constructor.call1((IMPORTS.as_str(),))?,
599                    uriref_constructor.call1((dep_uri.as_str(),))?,
600                ],
601            )?;
602            destination_graph.getattr("add")?.call1((t,))?;
603        }
604        Ok(())
605    }
606
607    /// List the ontologies in the imports closure of the given ontology
608    #[pyo3(signature = (uri, recursion_depth = -1))]
609    fn list_closure(&self, _py: Python, uri: &str, recursion_depth: i32) -> PyResult<Vec<String>> {
610        let iri = NamedNode::new(uri)
611            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
612        let inner = self.inner.clone();
613        let mut guard = inner.lock().unwrap();
614        let env = guard
615            .as_mut()
616            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
617        let graphid = env
618            .resolve(ResolveTarget::Graph(iri.clone()))
619            .ok_or_else(|| {
620                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
621                    "Failed to resolve graph for URI: {uri}"
622                ))
623            })?;
624        let ont = env.ontologies().get(&graphid).ok_or_else(|| {
625            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
626        })?;
627        let closure = env
628            .get_closure(ont.id(), recursion_depth)
629            .map_err(anyhow_to_pyerr)?;
630        let names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
631        Ok(names)
632    }
633
634    /// Merge the imports closure of `uri` into a single graph and return it alongside the closure list.
635    ///
636    /// The first element of the returned tuple is either the provided `destination_graph` (after
637    /// mutation) or a brand-new `rdflib.Graph`. The second element is an ordered list of ontology
638    /// IRIs in the resolved closure starting with `uri`. Set `rewrite_sh_prefixes` or
639    /// `remove_owl_imports` to control post-processing of the merged triples.
640    #[pyo3(signature = (uri, destination_graph=None, rewrite_sh_prefixes=true, remove_owl_imports=true, recursion_depth=-1))]
641    fn get_closure<'a>(
642        &self,
643        py: Python<'a>,
644        uri: &str,
645        destination_graph: Option<&Bound<'a, PyAny>>,
646        rewrite_sh_prefixes: bool,
647        remove_owl_imports: bool,
648        recursion_depth: i32,
649    ) -> PyResult<(Bound<'a, PyAny>, Vec<String>)> {
650        let rdflib = py.import("rdflib")?;
651        let iri = NamedNode::new(uri)
652            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
653        let inner = self.inner.clone();
654        let mut guard = inner.lock().unwrap();
655        let env = guard
656            .as_mut()
657            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
658        let graphid = env
659            .resolve(ResolveTarget::Graph(iri.clone()))
660            .ok_or_else(|| {
661                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("No graph with URI: {uri}"))
662            })?;
663        let ont = env.ontologies().get(&graphid).ok_or_else(|| {
664            PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
665        })?;
666        let closure = env
667            .get_closure(ont.id(), recursion_depth)
668            .map_err(anyhow_to_pyerr)?;
669        let closure_names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
670        // if destination_graph is null, create a new rdflib.Graph()
671        let destination_graph = match destination_graph {
672            Some(g) => g.clone(),
673            None => rdflib.getattr("Graph")?.call0()?,
674        };
675        let union = env
676            .get_union_graph(
677                &closure,
678                Some(rewrite_sh_prefixes),
679                Some(remove_owl_imports),
680            )
681            .map_err(anyhow_to_pyerr)?;
682        for triple in union.dataset.into_iter() {
683            let s: Term = triple.subject.into();
684            let p: Term = triple.predicate.into();
685            let o: Term = triple.object.into();
686            let t = PyTuple::new(
687                py,
688                &[
689                    term_to_python(py, &rdflib, s)?,
690                    term_to_python(py, &rdflib, p)?,
691                    term_to_python(py, &rdflib, o)?,
692                ],
693            )?;
694            destination_graph.getattr("add")?.call1((t,))?;
695        }
696
697        // Remove each successful_imports url in the closure from the destination_graph
698        if remove_owl_imports {
699            for graphid in union.graph_ids {
700                let iri = term_to_python(py, &rdflib, Term::NamedNode(graphid.into()))?;
701                let pred = term_to_python(py, &rdflib, IMPORTS.into())?;
702                // remove triples with (None, pred, iri)
703                let remove_tuple = PyTuple::new(py, &[py.None(), pred.into(), iri.into()])?;
704                destination_graph
705                    .getattr("remove")?
706                    .call1((remove_tuple,))?;
707            }
708        }
709        Ok((destination_graph, closure_names))
710    }
711
712    /// Print the contents of the OntoEnv
713    #[pyo3(signature = (includes=None))]
714    fn dump(&self, _py: Python, includes: Option<String>) -> PyResult<()> {
715        let inner = self.inner.clone();
716        let guard = inner.lock().unwrap();
717        if let Some(env) = guard.as_ref() {
718            env.dump(includes.as_deref());
719            Ok(())
720        } else {
721            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
722                "OntoEnv is closed",
723            ))
724        }
725    }
726
727    /// Import the dependencies referenced by `owl:imports` triples in `graph`.
728    ///
729    /// When `fetch_missing` is true, the environment attempts to download unresolved imports
730    /// before computing the closure. After merging the closure triples into `graph`, all
731    /// `owl:imports` statements are removed. The returned list contains the deduplicated ontology
732    /// IRIs that were successfully imported.
733    #[pyo3(signature = (graph, recursion_depth=-1, fetch_missing=false))]
734    fn import_dependencies<'a>(
735        &self,
736        py: Python<'a>,
737        graph: &Bound<'a, PyAny>,
738        recursion_depth: i32,
739        fetch_missing: bool,
740    ) -> PyResult<Vec<String>> {
741        let rdflib = py.import("rdflib")?;
742        let py_imports_pred = term_to_python(py, &rdflib, Term::NamedNode(IMPORTS.into()))?;
743
744        let kwargs = [("predicate", py_imports_pred)].into_py_dict(py)?;
745        let objects_iter = graph.call_method("objects", (), Some(&kwargs))?;
746        let builtins = py.import("builtins")?;
747        let objects_list = builtins.getattr("list")?.call1((objects_iter,))?;
748        let imports: Vec<String> = objects_list.extract()?;
749
750        if imports.is_empty() {
751            return Ok(Vec::new());
752        }
753
754        let inner = self.inner.clone();
755        let mut guard = inner.lock().unwrap();
756        let env = guard
757            .as_mut()
758            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
759
760        let is_strict = env.is_strict();
761        let mut all_ontologies = HashSet::new();
762        let mut all_closure_names: Vec<String> = Vec::new();
763
764        for uri in &imports {
765            let iri = NamedNode::new(uri.as_str())
766                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
767
768            let mut graphid = env.resolve(ResolveTarget::Graph(iri.clone()));
769
770            if graphid.is_none() && fetch_missing {
771                let location = OntologyLocation::from_str(uri.as_str()).map_err(anyhow_to_pyerr)?;
772                match env.add(location, Overwrite::Preserve, RefreshStrategy::UseCache) {
773                    Ok(new_id) => {
774                        graphid = Some(new_id);
775                    }
776                    Err(e) => {
777                        if is_strict {
778                            return Err(anyhow_to_pyerr(e));
779                        }
780                        println!("Failed to fetch {uri}: {e}");
781                    }
782                }
783            }
784
785            let graphid = match graphid {
786                Some(id) => id,
787                None => {
788                    if is_strict {
789                        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
790                            "Failed to resolve graph for URI: {}",
791                            uri
792                        )));
793                    }
794                    println!("Could not find {uri:?}");
795                    continue;
796                }
797            };
798
799            let ont = env.ontologies().get(&graphid).ok_or_else(|| {
800                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
801                    "Ontology {} not found",
802                    uri
803                ))
804            })?;
805
806            let closure = env
807                .get_closure(ont.id(), recursion_depth)
808                .map_err(anyhow_to_pyerr)?;
809            for c_ont in closure {
810                all_closure_names.push(c_ont.to_uri_string());
811                all_ontologies.insert(c_ont.clone());
812            }
813        }
814
815        if all_ontologies.is_empty() {
816            return Ok(Vec::new());
817        }
818
819        let union = env
820            .get_union_graph(&all_ontologies, Some(true), Some(true))
821            .map_err(anyhow_to_pyerr)?;
822
823        for triple in union.dataset.into_iter() {
824            let s: Term = triple.subject.into();
825            let p: Term = triple.predicate.into();
826            let o: Term = triple.object.into();
827            let t = PyTuple::new(
828                py,
829                &[
830                    term_to_python(py, &rdflib, s)?,
831                    term_to_python(py, &rdflib, p)?,
832                    term_to_python(py, &rdflib, o)?,
833                ],
834            )?;
835            graph.getattr("add")?.call1((t,))?;
836        }
837
838        // Remove all owl:imports from the original graph
839        let py_imports_pred_for_remove = term_to_python(py, &rdflib, IMPORTS.into())?;
840        let remove_tuple = PyTuple::new(
841            py,
842            &[py.None(), py_imports_pred_for_remove.into(), py.None()],
843        )?;
844        graph.getattr("remove")?.call1((remove_tuple,))?;
845
846        all_closure_names.sort();
847        all_closure_names.dedup();
848
849        Ok(all_closure_names)
850    }
851
852    /// Get the dependency closure of a given graph and return it as a new graph.
853    ///
854    /// This method will look for `owl:imports` statements in the provided `graph`,
855    /// then find those ontologies within the `OntoEnv` and compute the full
856    /// dependency closure. The triples of all ontologies in the closure are
857    /// returned as a new graph. The original `graph` is left untouched unless you also
858    /// supply it as the `destination_graph`.
859    ///
860    /// Args:
861    ///     graph (rdflib.Graph): The graph to find dependencies for.
862    ///     destination_graph (Optional[rdflib.Graph]): If provided, the dependency graph will be added to this
863    ///         graph instead of creating a new one.
864    ///     recursion_depth (int): The maximum depth for recursive import resolution. A
865    ///         negative value (default) means no limit.
866    ///     fetch_missing (bool): If True, will fetch ontologies that are not in the environment.
867    ///     rewrite_sh_prefixes (bool): If True, will rewrite SHACL prefixes to be unique.
868    ///     remove_owl_imports (bool): If True, will remove `owl:imports` statements from the
869    ///         returned graph.
870    ///
871    /// Returns:
872    ///     tuple[rdflib.Graph, list[str]]: A tuple containing the populated dependency graph and the sorted list of
873    ///     imported ontology IRIs.
874    #[pyo3(signature = (graph, destination_graph=None, recursion_depth=-1, fetch_missing=false, rewrite_sh_prefixes=true, remove_owl_imports=true))]
875    fn get_dependencies_graph<'a>(
876        &self,
877        py: Python<'a>,
878        graph: &Bound<'a, PyAny>,
879        destination_graph: Option<&Bound<'a, PyAny>>,
880        recursion_depth: i32,
881        fetch_missing: bool,
882        rewrite_sh_prefixes: bool,
883        remove_owl_imports: bool,
884    ) -> PyResult<(Bound<'a, PyAny>, Vec<String>)> {
885        let rdflib = py.import("rdflib")?;
886        let py_imports_pred = term_to_python(py, &rdflib, Term::NamedNode(IMPORTS.into()))?;
887
888        let kwargs = [("predicate", py_imports_pred)].into_py_dict(py)?;
889        let objects_iter = graph.call_method("objects", (), Some(&kwargs))?;
890        let builtins = py.import("builtins")?;
891        let objects_list = builtins.getattr("list")?.call1((objects_iter,))?;
892        let imports: Vec<String> = objects_list.extract()?;
893
894        let destination_graph = match destination_graph {
895            Some(g) => g.clone(),
896            None => rdflib.getattr("Graph")?.call0()?,
897        };
898
899        if imports.is_empty() {
900            return Ok((destination_graph, Vec::new()));
901        }
902
903        let inner = self.inner.clone();
904        let mut guard = inner.lock().unwrap();
905        let env = guard
906            .as_mut()
907            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
908
909        let is_strict = env.is_strict();
910        let mut all_ontologies = HashSet::new();
911        let mut all_closure_names: Vec<String> = Vec::new();
912
913        for uri in &imports {
914            let iri = NamedNode::new(uri.as_str())
915                .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
916
917            let mut graphid = env.resolve(ResolveTarget::Graph(iri.clone()));
918
919            if graphid.is_none() && fetch_missing {
920                let location = OntologyLocation::from_str(uri.as_str()).map_err(anyhow_to_pyerr)?;
921                match env.add(location, Overwrite::Preserve, RefreshStrategy::UseCache) {
922                    Ok(new_id) => {
923                        graphid = Some(new_id);
924                    }
925                    Err(e) => {
926                        if is_strict {
927                            return Err(anyhow_to_pyerr(e));
928                        }
929                        println!("Failed to fetch {uri}: {e}");
930                    }
931                }
932            }
933
934            let graphid = match graphid {
935                Some(id) => id,
936                None => {
937                    if is_strict {
938                        return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
939                            "Failed to resolve graph for URI: {}",
940                            uri
941                        )));
942                    }
943                    println!("Could not find {uri:?}");
944                    continue;
945                }
946            };
947
948            let ont = env.ontologies().get(&graphid).ok_or_else(|| {
949                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
950                    "Ontology {} not found",
951                    uri
952                ))
953            })?;
954
955            let closure = env
956                .get_closure(ont.id(), recursion_depth)
957                .map_err(anyhow_to_pyerr)?;
958            for c_ont in closure {
959                all_closure_names.push(c_ont.to_uri_string());
960                all_ontologies.insert(c_ont.clone());
961            }
962        }
963
964        if all_ontologies.is_empty() {
965            return Ok((destination_graph, Vec::new()));
966        }
967
968        let union = env
969            .get_union_graph(
970                &all_ontologies,
971                Some(rewrite_sh_prefixes),
972                Some(remove_owl_imports),
973            )
974            .map_err(anyhow_to_pyerr)?;
975
976        for triple in union.dataset.into_iter() {
977            let s: Term = triple.subject.into();
978            let p: Term = triple.predicate.into();
979            let o: Term = triple.object.into();
980            let t = PyTuple::new(
981                py,
982                &[
983                    term_to_python(py, &rdflib, s)?,
984                    term_to_python(py, &rdflib, p)?,
985                    term_to_python(py, &rdflib, o)?,
986                ],
987            )?;
988            destination_graph.getattr("add")?.call1((t,))?;
989        }
990
991        if remove_owl_imports {
992            for graphid in union.graph_ids {
993                let iri = term_to_python(py, &rdflib, Term::NamedNode(graphid.into()))?;
994                let pred = term_to_python(py, &rdflib, IMPORTS.into())?;
995                let remove_tuple = PyTuple::new(py, &[py.None(), pred.into(), iri.into()])?;
996                destination_graph
997                    .getattr("remove")?
998                    .call1((remove_tuple,))?;
999            }
1000        }
1001
1002        all_closure_names.sort();
1003        all_closure_names.dedup();
1004
1005        Ok((destination_graph, all_closure_names))
1006    }
1007
1008    /// Add a new ontology to the OntoEnv
1009    #[pyo3(signature = (location, overwrite = false, fetch_imports = true, force = false))]
1010    fn add(
1011        &self,
1012        location: &Bound<'_, PyAny>,
1013        overwrite: bool,
1014        fetch_imports: bool,
1015        force: bool,
1016    ) -> PyResult<String> {
1017        let inner = self.inner.clone();
1018        let mut guard = inner.lock().unwrap();
1019        let env = guard
1020            .as_mut()
1021            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1022
1023        let resolved = ontology_location_from_py(location)?;
1024        if matches!(resolved.location, OntologyLocation::InMemory { .. }) {
1025            return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1026                "In-memory rdflib graphs cannot be added to the environment",
1027            ));
1028        }
1029        let preferred_name = resolved.preferred_name.clone();
1030        let location = resolved.location;
1031        let overwrite_flag: Overwrite = overwrite.into();
1032        let refresh: RefreshStrategy = force.into();
1033        let graph_id = if fetch_imports {
1034            env.add(location, overwrite_flag, refresh)
1035        } else {
1036            env.add_no_imports(location, overwrite_flag, refresh)
1037        }
1038        .map_err(anyhow_to_pyerr)?;
1039        let actual_name = graph_id.to_uri_string();
1040        if let Some(pref) = preferred_name {
1041            if let Ok(candidate) = NamedNode::new(pref.clone()) {
1042                if env.resolve(ResolveTarget::Graph(candidate)).is_some() {
1043                    return Ok(pref);
1044                }
1045            }
1046        }
1047        Ok(actual_name)
1048    }
1049
1050    /// Add a new ontology to the OntoEnv without exploring owl:imports.
1051    #[pyo3(signature = (location, overwrite = false, force = false))]
1052    fn add_no_imports(
1053        &self,
1054        location: &Bound<'_, PyAny>,
1055        overwrite: bool,
1056        force: bool,
1057    ) -> PyResult<String> {
1058        let inner = self.inner.clone();
1059        let mut guard = inner.lock().unwrap();
1060        let env = guard
1061            .as_mut()
1062            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1063        let resolved = ontology_location_from_py(location)?;
1064        if matches!(resolved.location, OntologyLocation::InMemory { .. }) {
1065            return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1066                "In-memory rdflib graphs cannot be added to the environment",
1067            ));
1068        }
1069        let preferred_name = resolved.preferred_name.clone();
1070        let location = resolved.location;
1071        let overwrite_flag: Overwrite = overwrite.into();
1072        let refresh: RefreshStrategy = force.into();
1073        let graph_id = env
1074            .add_no_imports(location, overwrite_flag, refresh)
1075            .map_err(anyhow_to_pyerr)?;
1076        let actual_name = graph_id.to_uri_string();
1077        if let Some(pref) = preferred_name {
1078            if let Ok(candidate) = NamedNode::new(pref.clone()) {
1079                if env.resolve(ResolveTarget::Graph(candidate)).is_some() {
1080                    return Ok(pref);
1081                }
1082            }
1083        }
1084        Ok(actual_name)
1085    }
1086
1087    /// Get the names of all ontologies that import the given ontology
1088    fn get_importers(&self, uri: &str) -> PyResult<Vec<String>> {
1089        let iri = NamedNode::new(uri)
1090            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1091        let inner = self.inner.clone();
1092        let guard = inner.lock().unwrap();
1093        let env = guard
1094            .as_ref()
1095            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1096        let importers = env.get_importers(&iri).map_err(anyhow_to_pyerr)?;
1097        let names: Vec<String> = importers.iter().map(|ont| ont.to_uri_string()).collect();
1098        Ok(names)
1099    }
1100
1101    /// Get the ontology metadata with the given URI
1102    fn get_ontology(&self, uri: &str) -> PyResult<PyOntology> {
1103        let iri = NamedNode::new(uri)
1104            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1105        let inner = self.inner.clone();
1106        let guard = inner.lock().unwrap();
1107        let env = guard
1108            .as_ref()
1109            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1110        let graphid = env
1111            .resolve(ResolveTarget::Graph(iri.clone()))
1112            .ok_or_else(|| {
1113                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
1114                    "Failed to resolve graph for URI: {uri}"
1115                ))
1116            })?;
1117        let ont = env.get_ontology(&graphid).map_err(anyhow_to_pyerr)?;
1118        Ok(PyOntology { inner: ont })
1119    }
1120
1121    /// Get the graph with the given URI as an rdflib.Graph
1122    fn get_graph(&self, py: Python, uri: &Bound<'_, PyString>) -> PyResult<Py<PyAny>> {
1123        let rdflib = py.import("rdflib")?;
1124        let iri = NamedNode::new(uri.to_string())
1125            .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1126        let graph = {
1127            let inner = self.inner.clone();
1128            let guard = inner.lock().unwrap();
1129            let env = guard.as_ref().ok_or_else(|| {
1130                PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
1131            })?;
1132            let graphid = env.resolve(ResolveTarget::Graph(iri)).ok_or_else(|| {
1133                PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
1134                    "Failed to resolve graph for URI: {uri}"
1135                ))
1136            })?;
1137
1138            env.get_graph(&graphid).map_err(anyhow_to_pyerr)?
1139        };
1140        let res = rdflib.getattr("Graph")?.call0()?;
1141        for triple in graph.into_iter() {
1142            let s: Term = triple.subject.into();
1143            let p: Term = triple.predicate.into();
1144            let o: Term = triple.object.into();
1145
1146            let t = PyTuple::new(
1147                py,
1148                &[
1149                    term_to_python(py, &rdflib, s)?,
1150                    term_to_python(py, &rdflib, p)?,
1151                    term_to_python(py, &rdflib, o)?,
1152                ],
1153            )?;
1154
1155            res.getattr("add")?.call1((t,))?;
1156        }
1157        Ok(res.into())
1158    }
1159
1160    /// Get the names of all ontologies in the OntoEnv
1161    fn get_ontology_names(&self) -> PyResult<Vec<String>> {
1162        let inner = self.inner.clone();
1163        let guard = inner.lock().unwrap();
1164        let env = guard
1165            .as_ref()
1166            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1167        let names: Vec<String> = env.ontologies().keys().map(|k| k.to_uri_string()).collect();
1168        Ok(names)
1169    }
1170
1171    /// Convert the OntoEnv to an in-memory rdflib.Dataset populated with all named graphs
1172    fn to_rdflib_dataset(&self, py: Python) -> PyResult<Py<PyAny>> {
1173        let inner = self.inner.clone();
1174        let guard = inner.lock().unwrap();
1175        let env = guard
1176            .as_ref()
1177            .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1178        let rdflib = py.import("rdflib")?;
1179        let dataset_cls = rdflib.getattr("Dataset")?;
1180        let ds = dataset_cls.call0()?;
1181        let uriref = rdflib.getattr("URIRef")?;
1182
1183        for (_gid, ont) in env.ontologies().iter() {
1184            let id_str = ont.id().name().as_str();
1185            let id_py = uriref.call1((id_str,))?;
1186            let kwargs = [("identifier", id_py.clone())].into_py_dict(py)?;
1187            let ctx = ds.getattr("graph")?.call((), Some(&kwargs))?;
1188
1189            let graph = env.get_graph(ont.id()).map_err(anyhow_to_pyerr)?;
1190            for t in graph.iter() {
1191                let s: Term = t.subject.into();
1192                let p: Term = t.predicate.into();
1193                let o: Term = t.object.into();
1194                let triple = PyTuple::new(
1195                    py,
1196                    &[
1197                        term_to_python(py, &rdflib, s)?,
1198                        term_to_python(py, &rdflib, p)?,
1199                        term_to_python(py, &rdflib, o)?,
1200                    ],
1201                )?;
1202                ctx.getattr("add")?.call1((triple,))?;
1203            }
1204        }
1205
1206        Ok(ds.into())
1207    }
1208
1209    // Config accessors
1210    fn is_offline(&self) -> PyResult<bool> {
1211        let inner = self.inner.clone();
1212        let guard = inner.lock().unwrap();
1213        if let Some(env) = guard.as_ref() {
1214            Ok(env.is_offline())
1215        } else {
1216            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1217                "OntoEnv is closed",
1218            ))
1219        }
1220    }
1221
1222    fn set_offline(&mut self, offline: bool) -> PyResult<()> {
1223        let inner = self.inner.clone();
1224        let mut guard = inner.lock().unwrap();
1225        if let Some(env) = guard.as_mut() {
1226            env.set_offline(offline);
1227            env.save_to_directory().map_err(anyhow_to_pyerr)
1228        } else {
1229            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1230                "OntoEnv is closed",
1231            ))
1232        }
1233    }
1234
1235    fn is_strict(&self) -> PyResult<bool> {
1236        let inner = self.inner.clone();
1237        let guard = inner.lock().unwrap();
1238        if let Some(env) = guard.as_ref() {
1239            Ok(env.is_strict())
1240        } else {
1241            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1242                "OntoEnv is closed",
1243            ))
1244        }
1245    }
1246
1247    fn set_strict(&mut self, strict: bool) -> PyResult<()> {
1248        let inner = self.inner.clone();
1249        let mut guard = inner.lock().unwrap();
1250        if let Some(env) = guard.as_mut() {
1251            env.set_strict(strict);
1252            env.save_to_directory().map_err(anyhow_to_pyerr)
1253        } else {
1254            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1255                "OntoEnv is closed",
1256            ))
1257        }
1258    }
1259
1260    fn requires_ontology_names(&self) -> PyResult<bool> {
1261        let inner = self.inner.clone();
1262        let guard = inner.lock().unwrap();
1263        if let Some(env) = guard.as_ref() {
1264            Ok(env.requires_ontology_names())
1265        } else {
1266            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1267                "OntoEnv is closed",
1268            ))
1269        }
1270    }
1271
1272    fn set_require_ontology_names(&mut self, require: bool) -> PyResult<()> {
1273        let inner = self.inner.clone();
1274        let mut guard = inner.lock().unwrap();
1275        if let Some(env) = guard.as_mut() {
1276            env.set_require_ontology_names(require);
1277            env.save_to_directory().map_err(anyhow_to_pyerr)
1278        } else {
1279            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1280                "OntoEnv is closed",
1281            ))
1282        }
1283    }
1284
1285    fn resolution_policy(&self) -> PyResult<String> {
1286        let inner = self.inner.clone();
1287        let guard = inner.lock().unwrap();
1288        if let Some(env) = guard.as_ref() {
1289            Ok(env.resolution_policy().to_string())
1290        } else {
1291            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1292                "OntoEnv is closed",
1293            ))
1294        }
1295    }
1296
1297    fn set_resolution_policy(&mut self, policy: String) -> PyResult<()> {
1298        let inner = self.inner.clone();
1299        let mut guard = inner.lock().unwrap();
1300        if let Some(env) = guard.as_mut() {
1301            env.set_resolution_policy(policy);
1302            env.save_to_directory().map_err(anyhow_to_pyerr)
1303        } else {
1304            Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1305                "OntoEnv is closed",
1306            ))
1307        }
1308    }
1309
1310    pub fn store_path(&self) -> PyResult<Option<String>> {
1311        let inner = self.inner.clone();
1312        let guard = inner.lock().unwrap();
1313        if let Some(env) = guard.as_ref() {
1314            match env.store_path() {
1315                Some(path) => {
1316                    let dir = path.parent().unwrap_or(path);
1317                    Ok(Some(dir.to_string_lossy().to_string()))
1318                }
1319                None => Ok(None), // Return None if the path doesn't exist (e.g., temporary env)
1320            }
1321        } else {
1322            Ok(None)
1323        }
1324    }
1325
1326    // Wrapper method to raise error if store_path is None, matching previous panic behavior
1327    // but providing a Python-level error. Or tests can check for None.
1328    // Let's keep the Option return type for flexibility and adjust tests.
1329
1330    pub fn close(&mut self, py: Python<'_>) -> PyResult<()> {
1331        py.detach(|| {
1332            let inner = self.inner.clone();
1333            let mut guard = inner.lock().unwrap();
1334            if let Some(env) = guard.as_mut() {
1335                env.save_to_directory().map_err(anyhow_to_pyerr)?;
1336                env.flush().map_err(anyhow_to_pyerr)?;
1337            }
1338            *guard = None;
1339            Ok(())
1340        })
1341    }
1342
1343    pub fn flush(&mut self, py: Python<'_>) -> PyResult<()> {
1344        py.detach(|| {
1345            let inner = self.inner.clone();
1346            let mut guard = inner.lock().unwrap();
1347            if let Some(env) = guard.as_mut() {
1348                env.flush().map_err(anyhow_to_pyerr)
1349            } else {
1350                Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1351                    "OntoEnv is closed",
1352                ))
1353            }
1354        })
1355    }
1356}
1357
1358#[pymodule]
1359fn _native(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
1360    // Initialize logging when the python module is loaded.
1361    ::ontoenv::api::init_logging();
1362    // Use try_init to avoid panic if logging is already initialized.
1363    let _ = env_logger::try_init();
1364
1365    m.add_class::<OntoEnv>()?;
1366    m.add_class::<PyOntology>()?;
1367    m.add_function(wrap_pyfunction!(run_cli, m)?)?;
1368    // add version attribute
1369    m.add("version", env!("CARGO_PKG_VERSION"))?;
1370    Ok(())
1371}