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