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