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