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 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#[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 (_, 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#[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#[pyfunction]
272#[cfg(not(feature = "cli"))]
273#[allow(unused_variables)]
274fn run_cli(_py: Python<'_>, _args: Option<Vec<String>>) -> PyResult<i32> {
275 Err(PyErr::new::<PyRuntimeError, _>(
276 "ontoenv was built without CLI support; rebuild with the 'cli' feature",
277 ))
278}
279
280#[pyclass(name = "Ontology")]
281#[derive(Clone)]
282struct PyOntology {
283 inner: OntologyRs,
284}
285
286#[pymethods]
287impl PyOntology {
288 #[getter]
289 fn id(&self) -> PyResult<String> {
290 Ok(self.inner.id().to_uri_string())
291 }
292
293 #[getter]
294 fn name(&self) -> PyResult<String> {
295 Ok(self.inner.name().to_uri_string())
296 }
297
298 #[getter]
299 fn imports(&self) -> PyResult<Vec<String>> {
300 Ok(self
301 .inner
302 .imports
303 .iter()
304 .map(|i| i.to_uri_string())
305 .collect())
306 }
307
308 #[getter]
309 fn location(&self) -> PyResult<Option<String>> {
310 Ok(self.inner.location().map(|l| l.to_string()))
311 }
312
313 #[getter]
314 fn last_updated(&self) -> PyResult<Option<String>> {
315 Ok(self.inner.last_updated.map(|dt| dt.to_rfc3339()))
316 }
317
318 #[getter]
319 fn version_properties(&self) -> PyResult<HashMap<String, String>> {
320 Ok(self
321 .inner
322 .version_properties()
323 .iter()
324 .map(|(k, v)| (k.to_uri_string(), v.clone()))
325 .collect())
326 }
327
328 #[getter]
329 fn namespace_map(&self) -> PyResult<HashMap<String, String>> {
330 Ok(self.inner.namespace_map().clone())
331 }
332
333 fn __repr__(&self) -> PyResult<String> {
334 Ok(format!("<Ontology: {}>", self.inner.name().to_uri_string()))
335 }
336}
337
338#[pyclass]
339struct OntoEnv {
340 inner: Arc<Mutex<Option<OntoEnvRs>>>,
341}
342
343#[pymethods]
344impl OntoEnv {
345 #[new]
346 #[pyo3(signature = (path=None, recreate=false, create_or_use_cached=false, read_only=false, search_directories=None, require_ontology_names=false, strict=false, offline=false, use_cached_ontologies=false, resolution_policy="default".to_owned(), root=".".to_owned(), includes=None, excludes=None, include_ontologies=None, exclude_ontologies=None, temporary=false, no_search=false))]
347 fn new(
348 _py: Python,
349 path: Option<PathBuf>,
350 recreate: bool,
351 create_or_use_cached: bool,
352 read_only: bool,
353 search_directories: Option<Vec<String>>,
354 require_ontology_names: bool,
355 strict: bool,
356 offline: bool,
357 use_cached_ontologies: bool,
358 resolution_policy: String,
359 root: String,
360 includes: Option<Vec<String>>,
361 excludes: Option<Vec<String>>,
362 include_ontologies: Option<Vec<String>>,
363 exclude_ontologies: Option<Vec<String>>,
364 temporary: bool,
365 no_search: bool,
366 ) -> PyResult<Self> {
367 let mut root_path = path.clone().unwrap_or_else(|| PathBuf::from(root));
368 if 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 let mut builder = config::Config::builder()
386 .root(root_path.clone())
387 .require_ontology_names(require_ontology_names)
388 .strict(strict)
389 .offline(offline)
390 .use_cached_ontologies(CacheMode::from(use_cached_ontologies))
391 .resolution_policy(resolution_policy)
392 .temporary(temporary)
393 .no_search(no_search);
394
395 if let Some(dirs) = search_directories {
396 let paths = dirs.into_iter().map(PathBuf::from).collect();
397 builder = builder.locations(paths);
398 }
399 if let Some(incl) = includes {
400 builder = builder.includes(incl);
401 }
402 if let Some(excl) = excludes {
403 builder = builder.excludes(excl);
404 }
405 if let Some(incl_o) = include_ontologies {
406 builder = builder.include_ontologies(incl_o);
407 }
408 if let Some(excl_o) = exclude_ontologies {
409 builder = builder.exclude_ontologies(excl_o);
410 }
411
412 let cfg = builder
413 .build()
414 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
415
416 let root_for_lookup = cfg.root.clone();
417 let env = if cfg.temporary {
418 OntoEnvRs::init(cfg, false).map_err(anyhow_to_pyerr)?
419 } else if recreate {
420 OntoEnvRs::init(cfg, true).map_err(anyhow_to_pyerr)?
421 } else if create_or_use_cached {
422 OntoEnvRs::open_or_init(cfg, read_only).map_err(anyhow_to_pyerr)?
423 } else {
424 let load_root = if let Some(found_root) =
425 find_ontoenv_root_from(root_for_lookup.as_path())
426 {
427 found_root
428 } else {
429 let ontoenv_dir = root_for_lookup.join(".ontoenv");
430 if ontoenv_dir.exists() {
431 root_for_lookup.clone()
432 } else {
433 return Err(PyErr::new::<pyo3::exceptions::PyFileNotFoundError, _>(
434 format!(
435 "OntoEnv directory not found at {} (set create_or_use_cached=True to initialize a new environment)",
436 ontoenv_dir.display()
437 ),
438 ));
439 }
440 };
441 OntoEnvRs::load_from_directory(load_root, read_only).map_err(anyhow_to_pyerr)?
442 };
443
444 let inner = Arc::new(Mutex::new(Some(env)));
445
446 Ok(OntoEnv {
447 inner: inner.clone(),
448 })
449 }
450
451 #[pyo3(signature = (all=false))]
452 fn update(&self, all: bool) -> PyResult<()> {
453 let inner = self.inner.clone();
454 let mut guard = inner.lock().unwrap();
455 if let Some(env) = guard.as_mut() {
456 env.update_all(all).map_err(anyhow_to_pyerr)?;
457 env.save_to_directory().map_err(anyhow_to_pyerr)
458 } else {
459 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
460 "OntoEnv is closed",
461 ))
462 }
463 }
464
465 fn __repr__(&self) -> PyResult<String> {
472 let inner = self.inner.clone();
473 let guard = inner.lock().unwrap();
474 if let Some(env) = guard.as_ref() {
475 let stats = env.stats().map_err(anyhow_to_pyerr)?;
476 Ok(format!(
477 "<OntoEnv: {} ontologies, {} graphs, {} triples>",
478 stats.num_ontologies, stats.num_graphs, stats.num_triples,
479 ))
480 } else {
481 Ok("<OntoEnv: closed>".to_string())
482 }
483 }
484
485 #[pyo3(signature = (destination_graph, uri, recursion_depth = -1))]
488 fn import_graph(
489 &self,
490 py: Python,
491 destination_graph: &Bound<'_, PyAny>,
492 uri: &str,
493 recursion_depth: i32,
494 ) -> PyResult<()> {
495 let inner = self.inner.clone();
496 let mut guard = inner.lock().unwrap();
497 let env = guard
498 .as_mut()
499 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
500 let rdflib = py.import("rdflib")?;
501 let iri = NamedNode::new(uri)
502 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
503 let graphid = env
504 .resolve(ResolveTarget::Graph(iri.clone()))
505 .ok_or_else(|| {
506 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
507 "Failed to resolve graph for URI: {uri}"
508 ))
509 })?;
510
511 let closure = env
513 .get_closure(&graphid, recursion_depth)
514 .map_err(anyhow_to_pyerr)?;
515
516 let uriref_constructor = rdflib.getattr("URIRef")?;
519 let type_uri = uriref_constructor.call1((TYPE.as_str(),))?;
520 let ontology_uri = uriref_constructor.call1((ONTOLOGY.as_str(),))?;
521 let kwargs = [("predicate", type_uri), ("object", ontology_uri)].into_py_dict(py)?;
522 let existing_root = destination_graph.call_method("value", (), Some(&kwargs))?;
523 let root_node_owned: oxigraph::model::NamedNode = if existing_root.is_none() {
524 graphid.name().into_owned()
525 } else {
526 NamedNode::new(existing_root.extract::<String>()?)
527 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?
528 .to_owned()
529 };
530 let root_node = root_node_owned.as_ref();
531
532 let imports_uri = uriref_constructor.call1((IMPORTS.as_str(),))?;
534 let closure_set: std::collections::HashSet<String> =
535 closure.iter().map(|c| c.to_uri_string()).collect();
536 let triples_to_remove_imports = destination_graph.call_method(
537 "triples",
538 ((py.None(), imports_uri, py.None()),),
539 None,
540 )?;
541 for triple in triples_to_remove_imports.try_iter()? {
542 let t = triple?;
543 let obj: Bound<'_, PyAny> = t.get_item(2)?;
544 if let Ok(s) = obj.str() {
545 if closure_set.contains(s.to_str()?) {
546 destination_graph.getattr("remove")?.call1((t,))?;
547 }
548 }
549 }
550
551 let triples_to_remove = destination_graph.call_method(
553 "triples",
554 ((
555 py.None(),
556 uriref_constructor.call1((TYPE.as_str(),))?,
557 uriref_constructor.call1((ONTOLOGY.as_str(),))?,
558 ),),
559 None,
560 )?;
561 for triple in triples_to_remove.try_iter()? {
562 let t = triple?;
563 let subj: Bound<'_, PyAny> = t.get_item(0)?;
564 if subj.str()?.to_str()? != root_node.as_str() {
565 destination_graph.getattr("remove")?.call1((t,))?;
566 }
567 }
568
569 let merged = env
571 .import_graph_with_root(&graphid, recursion_depth, root_node)
572 .map_err(anyhow_to_pyerr)?;
573
574 for triple in merged.into_iter() {
576 let s: Term = triple.subject.into();
577 let p: Term = triple.predicate.into();
578 let o: Term = triple.object.into();
579 let t = PyTuple::new(
580 py,
581 &[
582 term_to_python(py, &rdflib, s)?,
583 term_to_python(py, &rdflib, p)?,
584 term_to_python(py, &rdflib, o)?,
585 ],
586 )?;
587 destination_graph.getattr("add")?.call1((t,))?;
588 }
589 for dep in closure.iter().skip(1) {
591 let dep_uri = dep.to_uri_string();
592 let t = PyTuple::new(
593 py,
594 &[
595 uriref_constructor.call1((root_node.as_str(),))?,
596 uriref_constructor.call1((IMPORTS.as_str(),))?,
597 uriref_constructor.call1((dep_uri.as_str(),))?,
598 ],
599 )?;
600 destination_graph.getattr("add")?.call1((t,))?;
601 }
602 Ok(())
603 }
604
605 #[pyo3(signature = (uri, recursion_depth = -1))]
607 fn list_closure(&self, _py: Python, uri: &str, recursion_depth: i32) -> PyResult<Vec<String>> {
608 let iri = NamedNode::new(uri)
609 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
610 let inner = self.inner.clone();
611 let mut guard = inner.lock().unwrap();
612 let env = guard
613 .as_mut()
614 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
615 let graphid = env
616 .resolve(ResolveTarget::Graph(iri.clone()))
617 .ok_or_else(|| {
618 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
619 "Failed to resolve graph for URI: {uri}"
620 ))
621 })?;
622 let ont = env.ontologies().get(&graphid).ok_or_else(|| {
623 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
624 })?;
625 let closure = env
626 .get_closure(ont.id(), recursion_depth)
627 .map_err(anyhow_to_pyerr)?;
628 let names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
629 Ok(names)
630 }
631
632 #[pyo3(signature = (uri, destination_graph=None, rewrite_sh_prefixes=true, remove_owl_imports=true, recursion_depth=-1))]
639 fn get_closure<'a>(
640 &self,
641 py: Python<'a>,
642 uri: &str,
643 destination_graph: Option<&Bound<'a, PyAny>>,
644 rewrite_sh_prefixes: bool,
645 remove_owl_imports: bool,
646 recursion_depth: i32,
647 ) -> PyResult<(Bound<'a, PyAny>, Vec<String>)> {
648 let rdflib = py.import("rdflib")?;
649 let iri = NamedNode::new(uri)
650 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
651 let inner = self.inner.clone();
652 let mut guard = inner.lock().unwrap();
653 let env = guard
654 .as_mut()
655 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
656 let graphid = env
657 .resolve(ResolveTarget::Graph(iri.clone()))
658 .ok_or_else(|| {
659 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("No graph with URI: {uri}"))
660 })?;
661 let ont = env.ontologies().get(&graphid).ok_or_else(|| {
662 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!("Ontology {iri} not found"))
663 })?;
664 let closure = env
665 .get_closure(ont.id(), recursion_depth)
666 .map_err(anyhow_to_pyerr)?;
667 let closure_names: Vec<String> = closure.iter().map(|ont| ont.to_uri_string()).collect();
668 let destination_graph = match destination_graph {
670 Some(g) => g.clone(),
671 None => rdflib.getattr("Graph")?.call0()?,
672 };
673 let union = env
674 .get_union_graph(
675 &closure,
676 Some(rewrite_sh_prefixes),
677 Some(remove_owl_imports),
678 )
679 .map_err(anyhow_to_pyerr)?;
680 for triple in union.dataset.into_iter() {
681 let s: Term = triple.subject.into();
682 let p: Term = triple.predicate.into();
683 let o: Term = triple.object.into();
684 let t = PyTuple::new(
685 py,
686 &[
687 term_to_python(py, &rdflib, s)?,
688 term_to_python(py, &rdflib, p)?,
689 term_to_python(py, &rdflib, o)?,
690 ],
691 )?;
692 destination_graph.getattr("add")?.call1((t,))?;
693 }
694
695 if remove_owl_imports {
697 for graphid in union.graph_ids {
698 let iri = term_to_python(py, &rdflib, Term::NamedNode(graphid.into()))?;
699 let pred = term_to_python(py, &rdflib, IMPORTS.into())?;
700 let remove_tuple = PyTuple::new(py, &[py.None(), pred.into(), iri.into()])?;
702 destination_graph
703 .getattr("remove")?
704 .call1((remove_tuple,))?;
705 }
706 }
707 Ok((destination_graph, closure_names))
708 }
709
710 #[pyo3(signature = (includes=None))]
712 fn dump(&self, _py: Python, includes: Option<String>) -> PyResult<()> {
713 let inner = self.inner.clone();
714 let guard = inner.lock().unwrap();
715 if let Some(env) = guard.as_ref() {
716 env.dump(includes.as_deref());
717 Ok(())
718 } else {
719 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
720 "OntoEnv is closed",
721 ))
722 }
723 }
724
725 #[pyo3(signature = (graph, recursion_depth=-1, fetch_missing=false))]
732 fn import_dependencies<'a>(
733 &self,
734 py: Python<'a>,
735 graph: &Bound<'a, PyAny>,
736 recursion_depth: i32,
737 fetch_missing: bool,
738 ) -> PyResult<Vec<String>> {
739 let rdflib = py.import("rdflib")?;
740 let py_imports_pred = term_to_python(py, &rdflib, Term::NamedNode(IMPORTS.into()))?;
741
742 let kwargs = [("predicate", py_imports_pred)].into_py_dict(py)?;
743 let objects_iter = graph.call_method("objects", (), Some(&kwargs))?;
744 let builtins = py.import("builtins")?;
745 let objects_list = builtins.getattr("list")?.call1((objects_iter,))?;
746 let imports: Vec<String> = objects_list.extract()?;
747
748 if imports.is_empty() {
749 return Ok(Vec::new());
750 }
751
752 let inner = self.inner.clone();
753 let mut guard = inner.lock().unwrap();
754 let env = guard
755 .as_mut()
756 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
757
758 let is_strict = env.is_strict();
759 let mut all_ontologies = HashSet::new();
760 let mut all_closure_names: Vec<String> = Vec::new();
761
762 for uri in &imports {
763 let iri = NamedNode::new(uri.as_str())
764 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
765
766 let mut graphid = env.resolve(ResolveTarget::Graph(iri.clone()));
767
768 if graphid.is_none() && fetch_missing {
769 let location = OntologyLocation::from_str(uri.as_str()).map_err(anyhow_to_pyerr)?;
770 match env.add(location, Overwrite::Preserve, RefreshStrategy::UseCache) {
771 Ok(new_id) => {
772 graphid = Some(new_id);
773 }
774 Err(e) => {
775 if is_strict {
776 return Err(anyhow_to_pyerr(e));
777 }
778 println!("Failed to fetch {uri}: {e}");
779 }
780 }
781 }
782
783 let graphid = match graphid {
784 Some(id) => id,
785 None => {
786 if is_strict {
787 return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
788 "Failed to resolve graph for URI: {}",
789 uri
790 )));
791 }
792 println!("Could not find {uri:?}");
793 continue;
794 }
795 };
796
797 let ont = env.ontologies().get(&graphid).ok_or_else(|| {
798 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
799 "Ontology {} not found",
800 uri
801 ))
802 })?;
803
804 let closure = env
805 .get_closure(ont.id(), recursion_depth)
806 .map_err(anyhow_to_pyerr)?;
807 for c_ont in closure {
808 all_closure_names.push(c_ont.to_uri_string());
809 all_ontologies.insert(c_ont.clone());
810 }
811 }
812
813 if all_ontologies.is_empty() {
814 return Ok(Vec::new());
815 }
816
817 let union = env
818 .get_union_graph(&all_ontologies, Some(true), Some(true))
819 .map_err(anyhow_to_pyerr)?;
820
821 for triple in union.dataset.into_iter() {
822 let s: Term = triple.subject.into();
823 let p: Term = triple.predicate.into();
824 let o: Term = triple.object.into();
825 let t = PyTuple::new(
826 py,
827 &[
828 term_to_python(py, &rdflib, s)?,
829 term_to_python(py, &rdflib, p)?,
830 term_to_python(py, &rdflib, o)?,
831 ],
832 )?;
833 graph.getattr("add")?.call1((t,))?;
834 }
835
836 let py_imports_pred_for_remove = term_to_python(py, &rdflib, IMPORTS.into())?;
838 let remove_tuple = PyTuple::new(
839 py,
840 &[py.None(), py_imports_pred_for_remove.into(), py.None()],
841 )?;
842 graph.getattr("remove")?.call1((remove_tuple,))?;
843
844 all_closure_names.sort();
845 all_closure_names.dedup();
846
847 Ok(all_closure_names)
848 }
849
850 #[pyo3(signature = (graph, destination_graph=None, recursion_depth=-1, fetch_missing=false, rewrite_sh_prefixes=true, remove_owl_imports=true))]
873 fn get_dependencies_graph<'a>(
874 &self,
875 py: Python<'a>,
876 graph: &Bound<'a, PyAny>,
877 destination_graph: Option<&Bound<'a, PyAny>>,
878 recursion_depth: i32,
879 fetch_missing: bool,
880 rewrite_sh_prefixes: bool,
881 remove_owl_imports: bool,
882 ) -> PyResult<(Bound<'a, PyAny>, Vec<String>)> {
883 let rdflib = py.import("rdflib")?;
884 let py_imports_pred = term_to_python(py, &rdflib, Term::NamedNode(IMPORTS.into()))?;
885
886 let kwargs = [("predicate", py_imports_pred)].into_py_dict(py)?;
887 let objects_iter = graph.call_method("objects", (), Some(&kwargs))?;
888 let builtins = py.import("builtins")?;
889 let objects_list = builtins.getattr("list")?.call1((objects_iter,))?;
890 let imports: Vec<String> = objects_list.extract()?;
891
892 let destination_graph = match destination_graph {
893 Some(g) => g.clone(),
894 None => rdflib.getattr("Graph")?.call0()?,
895 };
896
897 if imports.is_empty() {
898 return Ok((destination_graph, Vec::new()));
899 }
900
901 let inner = self.inner.clone();
902 let mut guard = inner.lock().unwrap();
903 let env = guard
904 .as_mut()
905 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
906
907 let is_strict = env.is_strict();
908 let mut all_ontologies = HashSet::new();
909 let mut all_closure_names: Vec<String> = Vec::new();
910
911 for uri in &imports {
912 let iri = NamedNode::new(uri.as_str())
913 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
914
915 let mut graphid = env.resolve(ResolveTarget::Graph(iri.clone()));
916
917 if graphid.is_none() && fetch_missing {
918 let location = OntologyLocation::from_str(uri.as_str()).map_err(anyhow_to_pyerr)?;
919 match env.add(location, Overwrite::Preserve, RefreshStrategy::UseCache) {
920 Ok(new_id) => {
921 graphid = Some(new_id);
922 }
923 Err(e) => {
924 if is_strict {
925 return Err(anyhow_to_pyerr(e));
926 }
927 println!("Failed to fetch {uri}: {e}");
928 }
929 }
930 }
931
932 let graphid = match graphid {
933 Some(id) => id,
934 None => {
935 if is_strict {
936 return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
937 "Failed to resolve graph for URI: {}",
938 uri
939 )));
940 }
941 println!("Could not find {uri:?}");
942 continue;
943 }
944 };
945
946 let ont = env.ontologies().get(&graphid).ok_or_else(|| {
947 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
948 "Ontology {} not found",
949 uri
950 ))
951 })?;
952
953 let closure = env
954 .get_closure(ont.id(), recursion_depth)
955 .map_err(anyhow_to_pyerr)?;
956 for c_ont in closure {
957 all_closure_names.push(c_ont.to_uri_string());
958 all_ontologies.insert(c_ont.clone());
959 }
960 }
961
962 if all_ontologies.is_empty() {
963 return Ok((destination_graph, Vec::new()));
964 }
965
966 let union = env
967 .get_union_graph(
968 &all_ontologies,
969 Some(rewrite_sh_prefixes),
970 Some(remove_owl_imports),
971 )
972 .map_err(anyhow_to_pyerr)?;
973
974 for triple in union.dataset.into_iter() {
975 let s: Term = triple.subject.into();
976 let p: Term = triple.predicate.into();
977 let o: Term = triple.object.into();
978 let t = PyTuple::new(
979 py,
980 &[
981 term_to_python(py, &rdflib, s)?,
982 term_to_python(py, &rdflib, p)?,
983 term_to_python(py, &rdflib, o)?,
984 ],
985 )?;
986 destination_graph.getattr("add")?.call1((t,))?;
987 }
988
989 if remove_owl_imports {
990 for graphid in union.graph_ids {
991 let iri = term_to_python(py, &rdflib, Term::NamedNode(graphid.into()))?;
992 let pred = term_to_python(py, &rdflib, IMPORTS.into())?;
993 let remove_tuple = PyTuple::new(py, &[py.None(), pred.into(), iri.into()])?;
994 destination_graph
995 .getattr("remove")?
996 .call1((remove_tuple,))?;
997 }
998 }
999
1000 all_closure_names.sort();
1001 all_closure_names.dedup();
1002
1003 Ok((destination_graph, all_closure_names))
1004 }
1005
1006 #[pyo3(signature = (location, overwrite = false, fetch_imports = true, force = false))]
1008 fn add(
1009 &self,
1010 location: &Bound<'_, PyAny>,
1011 overwrite: bool,
1012 fetch_imports: bool,
1013 force: bool,
1014 ) -> PyResult<String> {
1015 let inner = self.inner.clone();
1016 let mut guard = inner.lock().unwrap();
1017 let env = guard
1018 .as_mut()
1019 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1020
1021 let resolved = ontology_location_from_py(location)?;
1022 if matches!(resolved.location, OntologyLocation::InMemory { .. }) {
1023 return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1024 "In-memory rdflib graphs cannot be added to the environment",
1025 ));
1026 }
1027 let preferred_name = resolved.preferred_name.clone();
1028 let location = resolved.location;
1029 let overwrite_flag: Overwrite = overwrite.into();
1030 let refresh: RefreshStrategy = force.into();
1031 let graph_id = if fetch_imports {
1032 env.add(location, overwrite_flag, refresh)
1033 } else {
1034 env.add_no_imports(location, overwrite_flag, refresh)
1035 }
1036 .map_err(anyhow_to_pyerr)?;
1037 let actual_name = graph_id.to_uri_string();
1038 if let Some(pref) = preferred_name {
1039 if let Ok(candidate) = NamedNode::new(pref.clone()) {
1040 if env.resolve(ResolveTarget::Graph(candidate)).is_some() {
1041 return Ok(pref);
1042 }
1043 }
1044 }
1045 Ok(actual_name)
1046 }
1047
1048 #[pyo3(signature = (location, overwrite = false, force = false))]
1050 fn add_no_imports(
1051 &self,
1052 location: &Bound<'_, PyAny>,
1053 overwrite: bool,
1054 force: bool,
1055 ) -> PyResult<String> {
1056 let inner = self.inner.clone();
1057 let mut guard = inner.lock().unwrap();
1058 let env = guard
1059 .as_mut()
1060 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1061 let resolved = ontology_location_from_py(location)?;
1062 if matches!(resolved.location, OntologyLocation::InMemory { .. }) {
1063 return Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
1064 "In-memory rdflib graphs cannot be added to the environment",
1065 ));
1066 }
1067 let preferred_name = resolved.preferred_name.clone();
1068 let location = resolved.location;
1069 let overwrite_flag: Overwrite = overwrite.into();
1070 let refresh: RefreshStrategy = force.into();
1071 let graph_id = env
1072 .add_no_imports(location, overwrite_flag, refresh)
1073 .map_err(anyhow_to_pyerr)?;
1074 let actual_name = graph_id.to_uri_string();
1075 if let Some(pref) = preferred_name {
1076 if let Ok(candidate) = NamedNode::new(pref.clone()) {
1077 if env.resolve(ResolveTarget::Graph(candidate)).is_some() {
1078 return Ok(pref);
1079 }
1080 }
1081 }
1082 Ok(actual_name)
1083 }
1084
1085 fn get_importers(&self, uri: &str) -> PyResult<Vec<String>> {
1087 let iri = NamedNode::new(uri)
1088 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1089 let inner = self.inner.clone();
1090 let guard = inner.lock().unwrap();
1091 let env = guard
1092 .as_ref()
1093 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1094 let importers = env.get_importers(&iri).map_err(anyhow_to_pyerr)?;
1095 let names: Vec<String> = importers.iter().map(|ont| ont.to_uri_string()).collect();
1096 Ok(names)
1097 }
1098
1099 fn get_ontology(&self, uri: &str) -> PyResult<PyOntology> {
1101 let iri = NamedNode::new(uri)
1102 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1103 let inner = self.inner.clone();
1104 let guard = inner.lock().unwrap();
1105 let env = guard
1106 .as_ref()
1107 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1108 let graphid = env
1109 .resolve(ResolveTarget::Graph(iri.clone()))
1110 .ok_or_else(|| {
1111 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
1112 "Failed to resolve graph for URI: {uri}"
1113 ))
1114 })?;
1115 let ont = env.get_ontology(&graphid).map_err(anyhow_to_pyerr)?;
1116 Ok(PyOntology { inner: ont })
1117 }
1118
1119 fn get_graph(&self, py: Python, uri: &Bound<'_, PyString>) -> PyResult<Py<PyAny>> {
1121 let rdflib = py.import("rdflib")?;
1122 let iri = NamedNode::new(uri.to_string())
1123 .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))?;
1124 let graph = {
1125 let inner = self.inner.clone();
1126 let guard = inner.lock().unwrap();
1127 let env = guard.as_ref().ok_or_else(|| {
1128 PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed")
1129 })?;
1130 let graphid = env.resolve(ResolveTarget::Graph(iri)).ok_or_else(|| {
1131 PyErr::new::<pyo3::exceptions::PyValueError, _>(format!(
1132 "Failed to resolve graph for URI: {uri}"
1133 ))
1134 })?;
1135
1136 env.get_graph(&graphid).map_err(anyhow_to_pyerr)?
1137 };
1138 let res = rdflib.getattr("Graph")?.call0()?;
1139 for triple in graph.into_iter() {
1140 let s: Term = triple.subject.into();
1141 let p: Term = triple.predicate.into();
1142 let o: Term = triple.object.into();
1143
1144 let t = PyTuple::new(
1145 py,
1146 &[
1147 term_to_python(py, &rdflib, s)?,
1148 term_to_python(py, &rdflib, p)?,
1149 term_to_python(py, &rdflib, o)?,
1150 ],
1151 )?;
1152
1153 res.getattr("add")?.call1((t,))?;
1154 }
1155 Ok(res.into())
1156 }
1157
1158 fn get_ontology_names(&self) -> PyResult<Vec<String>> {
1160 let inner = self.inner.clone();
1161 let guard = inner.lock().unwrap();
1162 let env = guard
1163 .as_ref()
1164 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1165 let names: Vec<String> = env.ontologies().keys().map(|k| k.to_uri_string()).collect();
1166 Ok(names)
1167 }
1168
1169 fn to_rdflib_dataset(&self, py: Python) -> PyResult<Py<PyAny>> {
1171 let inner = self.inner.clone();
1172 let guard = inner.lock().unwrap();
1173 let env = guard
1174 .as_ref()
1175 .ok_or_else(|| PyErr::new::<pyo3::exceptions::PyValueError, _>("OntoEnv is closed"))?;
1176 let rdflib = py.import("rdflib")?;
1177 let dataset_cls = rdflib.getattr("Dataset")?;
1178 let ds = dataset_cls.call0()?;
1179 let uriref = rdflib.getattr("URIRef")?;
1180
1181 for (_gid, ont) in env.ontologies().iter() {
1182 let id_str = ont.id().name().as_str();
1183 let id_py = uriref.call1((id_str,))?;
1184 let kwargs = [("identifier", id_py.clone())].into_py_dict(py)?;
1185 let ctx = ds.getattr("graph")?.call((), Some(&kwargs))?;
1186
1187 let graph = env.get_graph(ont.id()).map_err(anyhow_to_pyerr)?;
1188 for t in graph.iter() {
1189 let s: Term = t.subject.into();
1190 let p: Term = t.predicate.into();
1191 let o: Term = t.object.into();
1192 let triple = PyTuple::new(
1193 py,
1194 &[
1195 term_to_python(py, &rdflib, s)?,
1196 term_to_python(py, &rdflib, p)?,
1197 term_to_python(py, &rdflib, o)?,
1198 ],
1199 )?;
1200 ctx.getattr("add")?.call1((triple,))?;
1201 }
1202 }
1203
1204 Ok(ds.into())
1205 }
1206
1207 fn is_offline(&self) -> PyResult<bool> {
1209 let inner = self.inner.clone();
1210 let guard = inner.lock().unwrap();
1211 if let Some(env) = guard.as_ref() {
1212 Ok(env.is_offline())
1213 } else {
1214 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1215 "OntoEnv is closed",
1216 ))
1217 }
1218 }
1219
1220 fn set_offline(&mut self, offline: bool) -> PyResult<()> {
1221 let inner = self.inner.clone();
1222 let mut guard = inner.lock().unwrap();
1223 if let Some(env) = guard.as_mut() {
1224 env.set_offline(offline);
1225 env.save_to_directory().map_err(anyhow_to_pyerr)
1226 } else {
1227 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1228 "OntoEnv is closed",
1229 ))
1230 }
1231 }
1232
1233 fn is_strict(&self) -> PyResult<bool> {
1234 let inner = self.inner.clone();
1235 let guard = inner.lock().unwrap();
1236 if let Some(env) = guard.as_ref() {
1237 Ok(env.is_strict())
1238 } else {
1239 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1240 "OntoEnv is closed",
1241 ))
1242 }
1243 }
1244
1245 fn set_strict(&mut self, strict: bool) -> PyResult<()> {
1246 let inner = self.inner.clone();
1247 let mut guard = inner.lock().unwrap();
1248 if let Some(env) = guard.as_mut() {
1249 env.set_strict(strict);
1250 env.save_to_directory().map_err(anyhow_to_pyerr)
1251 } else {
1252 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1253 "OntoEnv is closed",
1254 ))
1255 }
1256 }
1257
1258 fn requires_ontology_names(&self) -> PyResult<bool> {
1259 let inner = self.inner.clone();
1260 let guard = inner.lock().unwrap();
1261 if let Some(env) = guard.as_ref() {
1262 Ok(env.requires_ontology_names())
1263 } else {
1264 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1265 "OntoEnv is closed",
1266 ))
1267 }
1268 }
1269
1270 fn set_require_ontology_names(&mut self, require: bool) -> PyResult<()> {
1271 let inner = self.inner.clone();
1272 let mut guard = inner.lock().unwrap();
1273 if let Some(env) = guard.as_mut() {
1274 env.set_require_ontology_names(require);
1275 env.save_to_directory().map_err(anyhow_to_pyerr)
1276 } else {
1277 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1278 "OntoEnv is closed",
1279 ))
1280 }
1281 }
1282
1283 fn no_search(&self) -> PyResult<bool> {
1284 let inner = self.inner.clone();
1285 let guard = inner.lock().unwrap();
1286 if let Some(env) = guard.as_ref() {
1287 Ok(env.no_search())
1288 } else {
1289 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1290 "OntoEnv is closed",
1291 ))
1292 }
1293 }
1294
1295 fn set_no_search(&mut self, no_search: bool) -> PyResult<()> {
1296 let inner = self.inner.clone();
1297 let mut guard = inner.lock().unwrap();
1298 if let Some(env) = guard.as_mut() {
1299 env.set_no_search(no_search);
1300 env.save_to_directory().map_err(anyhow_to_pyerr)
1301 } else {
1302 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1303 "OntoEnv is closed",
1304 ))
1305 }
1306 }
1307
1308 fn resolution_policy(&self) -> PyResult<String> {
1309 let inner = self.inner.clone();
1310 let guard = inner.lock().unwrap();
1311 if let Some(env) = guard.as_ref() {
1312 Ok(env.resolution_policy().to_string())
1313 } else {
1314 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1315 "OntoEnv is closed",
1316 ))
1317 }
1318 }
1319
1320 fn set_resolution_policy(&mut self, policy: String) -> PyResult<()> {
1321 let inner = self.inner.clone();
1322 let mut guard = inner.lock().unwrap();
1323 if let Some(env) = guard.as_mut() {
1324 env.set_resolution_policy(policy);
1325 env.save_to_directory().map_err(anyhow_to_pyerr)
1326 } else {
1327 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1328 "OntoEnv is closed",
1329 ))
1330 }
1331 }
1332
1333 pub fn store_path(&self) -> PyResult<Option<String>> {
1334 let inner = self.inner.clone();
1335 let guard = inner.lock().unwrap();
1336 if let Some(env) = guard.as_ref() {
1337 match env.store_path() {
1338 Some(path) => {
1339 let dir = path.parent().unwrap_or(path);
1340 Ok(Some(dir.to_string_lossy().to_string()))
1341 }
1342 None => Ok(None), }
1344 } else {
1345 Ok(None)
1346 }
1347 }
1348
1349 pub fn close(&mut self, py: Python<'_>) -> PyResult<()> {
1354 py.detach(|| {
1355 let inner = self.inner.clone();
1356 let mut guard = inner.lock().unwrap();
1357 if let Some(env) = guard.as_mut() {
1358 env.save_to_directory().map_err(anyhow_to_pyerr)?;
1359 env.flush().map_err(anyhow_to_pyerr)?;
1360 }
1361 *guard = None;
1362 Ok(())
1363 })
1364 }
1365
1366 pub fn flush(&mut self, py: Python<'_>) -> PyResult<()> {
1367 py.detach(|| {
1368 let inner = self.inner.clone();
1369 let mut guard = inner.lock().unwrap();
1370 if let Some(env) = guard.as_mut() {
1371 env.flush().map_err(anyhow_to_pyerr)
1372 } else {
1373 Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
1374 "OntoEnv is closed",
1375 ))
1376 }
1377 })
1378 }
1379}
1380
1381#[pymodule]
1382fn _native(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
1383 ::ontoenv::api::init_logging();
1385 let _ = env_logger::try_init();
1387
1388 m.add_class::<OntoEnv>()?;
1389 m.add_class::<PyOntology>()?;
1390 m.add_function(wrap_pyfunction!(run_cli, m)?)?;
1391 m.add("version", env!("CARGO_PKG_VERSION"))?;
1393 Ok(())
1394}