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