feat(ontology): Implement OntologyIngestor and update exports

- Added OntologyIngestor in semantica/ingest/ontology_ingestor.py
- Updated semantica/ontology/__init__.py to export OntologyIngestor
- Updated semantica/ingest/methods.py to use OntologyIngestor
- Added tests for ontology ingestion
- Cleaned up temporary files
This commit is contained in:
KaifAhmad1
2026-01-21 13:46:46 +05:30
parent fa8544c6d6
commit b96e71ae72
11 changed files with 976 additions and 4 deletions
+14
View File
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- **Ontology Ingestion Module**:
- Implemented `OntologyIngestor` in `semantica.ingest` for parsing RDF/OWL files (Turtle, RDF/XML, JSON-LD, N3) into standardized `OntologyData` objects.
- Added `ingest_ontology` convenience function and integrated it into the unified `ingest(source_type="ontology")` interface.
- Added recursive directory scanning support for batch ontology ingestion.
- Exposed ingestion tools in `semantica.ontology` for better discoverability.
- Added `OntologyData` dataclass for consistent metadata handling (source path, format, timestamps).
- **Documentation**:
- **Ontology Usage Guide**: Updated `ontology_usage.md` with comprehensive examples for single-file and directory ingestion.
- **API Reference**: Updated `ontology.md` with `OntologyIngestor` class documentation and method details.
- **Tests**:
- **Comprehensive Test Suite**: Added `tests/ingest/test_ontology_ingestor.py` covering all supported formats, error handling, and unified interface integration.
- **Demo Script**: Added `examples/demo_ontology_ingest.py` for end-to-end usage demonstration.
## [0.2.3] - 2026-01-20
### Fixed
+34
View File
@@ -63,6 +63,29 @@ The module uses several inference algorithms:
---
## Ontology Ingestion
Ingest existing ontology files directly into usable data structures using `OntologyIngestor`.
**Function:** `ingest_ontology(source, method="file")`
| Argument | Description |
|----------|-------------|
| `source` | File path, directory path, or list of paths |
| `method` | Ingestion method (default: "file") |
**Example:**
```python
from semantica.ontology import ingest_ontology
# Ingest file
data = ingest_ontology("ontology.ttl")
# Ingest directory
dataset = ingest_ontology("ontologies/")
```
## Main Classes
### OntologyEngine
@@ -170,6 +193,17 @@ Manages external dependencies.
| `import_external_ontology(uri, ontology)` | Load and merge external ontology |
| `evaluate_alignment(uri, ontology)` | Assess alignment and compatibility |
### OntologyIngestor
Handles ingestion of existing ontologies from files and directories.
**Methods:**
| Method | Description |
|--------|-------------|
| `ingest_ontology(file_path)` | Ingest a single ontology file |
| `ingest_directory(directory_path)` | Recursively ingest ontology files from a directory |
---
## Unified Engine Examples
+68
View File
@@ -0,0 +1,68 @@
import os
import shutil
import tempfile
from pathlib import Path
from semantica.ingest import ingest, ingest_ontology, OntologyData
def demo_ontology_ingestion():
print("=== Ontology Ingestion Demo ===")
# Create a sample ontology file
sample_ttl = """
@prefix : <http://example.org/demo/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<http://example.org/demo/> rdf:type owl:Ontology ;
rdfs:label "Demo Ontology" ;
rdfs:comment "A simple ontology for demonstration." .
:DemoClass rdf:type owl:Class ;
rdfs:label "Demo Class" .
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".ttl", mode="w") as tmp:
tmp.write(sample_ttl)
tmp_path = tmp.name
print(f"\nCreated temporary ontology file: {tmp_path}")
try:
# 1. Use ingest_ontology convenience function
print("\n--- Method 1: ingest_ontology() ---")
result = ingest_ontology(tmp_path)
if isinstance(result, OntologyData):
print(f"Success! Ingested ontology: {result.data.get('name')}")
print(f"Format: {result.metadata.get('format')}")
print(f"Classes found: {len(result.data.get('classes', []))}")
for cls in result.data.get('classes', []):
print(f" - {cls.get('name')} ({cls.get('uri')})")
else:
print("Unexpected result type:", type(result))
# 2. Use unified ingest function
print("\n--- Method 2: Unified ingest() ---")
# Explicitly setting source_type="ontology" ensures it uses OntologyIngestor
unified_result = ingest(tmp_path, source_type="ontology")
if "ontology" in unified_result:
ont_data = unified_result["ontology"]
if isinstance(ont_data, OntologyData):
print(f"Success! Ingested via unified interface.")
print(f"Ontology Name: {ont_data.data.get('name')}")
else:
print(f"Got 'ontology' key but value is {type(ont_data)}")
else:
print("Unified ingest result keys:", unified_result.keys())
except Exception as e:
print(f"An error occurred: {e}")
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
print(f"\nCleaned up temporary file.")
if __name__ == "__main__":
demo_ontology_ingestion()
+9
View File
@@ -86,6 +86,7 @@ Main Classes:
- RepoIngestor: Git repository processing
- EmailIngestor: Email protocol handling
- DBIngestor: Database export handling
- OntologyIngestor: Ontology file processing
- MethodRegistry: Registry for custom ingestion methods
- IngestConfig: Configuration manager for ingest module
@@ -98,6 +99,7 @@ Convenience Functions:
- ingest_repository: Repository ingestion wrapper
- ingest_email: Email ingestion wrapper
- ingest_database: Database ingestion wrapper
- ingest_ontology: Ontology ingestion wrapper
Example Usage:
@@ -134,6 +136,7 @@ from .methods import (
ingest_feed,
ingest_file,
ingest_mcp,
ingest_ontology,
ingest_repository,
ingest_stream,
ingest_web,
@@ -166,6 +169,8 @@ from .web_ingestor import (
WebIngestor,
)
from .ontology_ingestor import OntologyData, OntologyIngestor
__all__ = [
# File ingestion
"FileIngestor",
@@ -216,6 +221,9 @@ __all__ = [
"MCPClient",
"MCPResource",
"MCPTool",
# Ontology ingestion
"OntologyIngestor",
"OntologyData",
# Registry and Methods
"MethodRegistry",
"method_registry",
@@ -227,6 +235,7 @@ __all__ = [
"ingest_repository",
"ingest_email",
"ingest_database",
"ingest_ontology",
"ingest_mcp",
"get_ingest_method",
"list_available_methods",
+69
View File
@@ -150,6 +150,7 @@ from .email_ingestor import EmailData, EmailIngestor
from .feed_ingestor import FeedData, FeedIngestor
from .file_ingestor import FileIngestor, FileObject
from .mcp_ingestor import MCPData, MCPIngestor
from .ontology_ingestor import OntologyData, OntologyIngestor
from .registry import method_registry
from .repo_ingestor import RepoIngestor
from .stream_ingestor import StreamIngestor, StreamProcessor
@@ -537,6 +538,66 @@ def ingest_email(
raise
def ingest_ontology(
source: Union[str, Path, List[Union[str, Path]]], method: str = "file", **kwargs
) -> Union[OntologyData, List[OntologyData]]:
"""
Ingest ontology from source (convenience function).
This is a user-friendly wrapper that ingests ontologies using the specified method.
Args:
source: Ontology file path, directory path, or list of paths
method: Ingestion method (default: "file")
- "file": Single file ingestion
- "directory": Directory ingestion with recursive scanning
**kwargs: Additional options passed to OntologyIngestor
Returns:
OntologyData, List[OntologyData] with ingestion results
Examples:
>>> from semantica.ingest.methods import ingest_ontology
>>> ontology = ingest_ontology("ontology.ttl")
>>> ontologies = ingest_ontology("./ontologies", method="directory")
"""
# Check for custom method in registry
custom_method = method_registry.get("ontology", method)
if custom_method and custom_method != ingest_ontology:
try:
return custom_method(source, **kwargs)
except Exception as e:
logger.warning(
f"Custom method {method} failed: {e}, falling back to default"
)
try:
# Get config
config = ingest_config.get_method_config("ontology")
config.update(kwargs)
ingestor = OntologyIngestor(**config)
source_path = str(source) if isinstance(source, (str, Path)) else None
if method == "file" and source_path:
if isinstance(source, list):
return [ingestor.ingest_ontology(str(s), **kwargs) for s in source]
return ingestor.ingest_ontology(source_path, **kwargs)
elif method == "directory" and source_path:
recursive = kwargs.get("recursive", ingest_config.get("recursive", True))
return ingestor.ingest_directory(source_path, recursive=recursive, **kwargs)
else:
# Default: try as file
if isinstance(source, list):
return [ingestor.ingest_ontology(str(s), **kwargs) for s in source]
return ingestor.ingest_ontology(str(source), **kwargs)
except Exception as e:
logger.error(f"Failed to ingest ontology: {e}")
raise
def ingest_database(
source: Union[str, Dict[str, Any]], method: Optional[str] = None, **kwargs
) -> Union[TableData, List[TableData], Dict[str, Any]]:
@@ -769,6 +830,7 @@ def ingest(
- "repo": Repository ingestion
- "email": Email ingestion
- "db": Database ingestion
- "ontology": Ontology ingestion
method: Optional specific ingestion method
**kwargs: Additional options passed to ingestor
@@ -802,6 +864,8 @@ def ingest(
("git@", "https://github.com", "https://gitlab.com")
):
source_type = "repo"
elif source_str.endswith((".ttl", ".owl", ".rdf", ".jsonld", ".n3", ".nt")):
source_type = "ontology"
else:
source_type = "file"
else:
@@ -830,6 +894,8 @@ def ingest(
raise ProcessingError("Email ingestion requires configuration dictionary")
elif source_type == "db":
return {"data": ingest_database(sources, method=method, **kwargs)}
elif source_type == "ontology":
return {"ontology": ingest_ontology(sources, method=method or "file", **kwargs)}
elif source_type == "mcp":
return {"data": ingest_mcp(sources, method=method or "resources", **kwargs)}
else:
@@ -909,5 +975,8 @@ method_registry.register("mcp", "default", ingest_mcp)
method_registry.register("mcp", "resources", ingest_mcp)
method_registry.register("mcp", "tools", ingest_mcp)
method_registry.register("mcp", "all", ingest_mcp)
method_registry.register("ontology", "default", ingest_ontology)
method_registry.register("ontology", "file", ingest_ontology)
method_registry.register("ontology", "directory", ingest_ontology)
method_registry.register("ingest", "default", ingest)
method_registry.register("ingest", "unified", ingest)
+392
View File
@@ -0,0 +1,392 @@
"""
Ontology Ingestion Module
This module provides capabilities to ingest external ontologies from files (OWL, RDF, TTL, etc.)
and convert them into Semantica's internal ontology dictionary format.
Supported Formats:
- Turtle (.ttl): Terse RDF Triple Language. A concise, human-readable
format for representing RDF graphs. Commonly used for writing
ontologies by hand.
- RDF/XML (.rdf, .owl): The XML serialization of RDF. The standard
format for OWL (Web Ontology Language) ontologies and often used
for data interchange.
- JSON-LD (.jsonld): JSON for Linked Data. A lightweight Linked Data
format that is easy for humans to read and for machines to parse
and generate. Ideal for web-based applications.
- N-Triples (.nt): A line-based, plain text format for encoding an
RDF graph. Each line represents a single triple. Very simple to
parse but verbose.
- Notation3 (.n3): A superset of Turtle that adds features like logic
and rules.
Key Features:
- Support for multiple RDF formats (Turtle, RDF/XML, JSON-LD, N3, NT)
- Automatic parsing using rdflib
- Conversion to Semantica ontology structure
- Batch processing of ontology files
- Extraction of classes, properties, and metadata
Example Usage:
>>> from semantica.ingest import OntologyIngestor
>>> ingestor = OntologyIngestor()
>>> ontology = ingestor.ingest_ontology("my_ontology.ttl")
"""
import os
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
import rdflib
from rdflib import RDF, RDFS, OWL, Graph
from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker
@dataclass
class OntologyData:
"""Ontology data representation."""
data: Dict[str, Any]
source_path: str
format: str
metadata: Dict[str, Any] = field(default_factory=dict)
ingested_at: datetime = field(default_factory=datetime.now)
class OntologyIngestor:
"""
Ontology ingestion handler.
This class parses OWL/RDF files and converts them to Semantica's ontology dictionary format.
"""
def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
"""
Initialize ontology ingestor.
Args:
config: Optional configuration dictionary
**kwargs: Additional configuration parameters
"""
self.logger = get_logger("ontology_ingestor")
self.progress = get_progress_tracker()
self.config = config or {}
self.config.update(kwargs)
def ingest_ontology(self, file_path: Union[str, Path], format: Optional[str] = None, **kwargs) -> OntologyData:
"""
Ingest an ontology file.
Args:
file_path: Path to the ontology file (string or Path object)
format: Optional format hint (e.g., 'turtle', 'xml'). If None, rdflib guesses.
**kwargs: Additional arguments for rdflib parsing
Returns:
OntologyData object containing the parsed ontology and metadata
"""
file_path = Path(file_path)
# Track file ingestion
tracking_id = self.progress.start_tracking(
file=str(file_path),
module="ingest",
submodule="OntologyIngestor",
message=f"Ontology: {file_path.name}",
)
try:
# Validate file exists
if not file_path.exists():
raise ValidationError(f"File not found: {file_path}")
self.progress.update_tracking(tracking_id, message="Parsing RDF graph...")
g = Graph()
# Use provided format or let rdflib guess based on extension
parse_kwargs = kwargs.copy()
if format:
parse_kwargs['format'] = format
try:
g.parse(file_path, **parse_kwargs)
except Exception as e:
# Fallback: try to guess format from extension if not provided and initial parse failed
if not format:
ext = os.path.splitext(file_path)[1].lower()
fmt_map = {
'.ttl': 'turtle',
'.owl': 'xml', # OWL is often XML
'.rdf': 'xml',
'.jsonld': 'json-ld',
'.n3': 'n3',
'.nt': 'nt'
}
guessed_fmt = fmt_map.get(ext)
if guessed_fmt:
self.logger.info(f"Retrying with guessed format: {guessed_fmt}")
g.parse(file_path, format=guessed_fmt, **kwargs)
else:
raise e
else:
raise e
self.progress.update_tracking(tracking_id, message="Converting to internal format...")
# Determine format for metadata
used_format = format
if not used_format:
ext = os.path.splitext(file_path)[1].lower()
fmt_map = {
'.ttl': 'turtle',
'.owl': 'xml',
'.rdf': 'xml',
'.jsonld': 'json-ld',
'.n3': 'n3',
'.nt': 'nt'
}
used_format = fmt_map.get(ext, 'unknown')
ontology_dict = self._convert_to_dict(g, source_path=str(file_path), format=used_format)
ontology_data = OntologyData(
data=ontology_dict,
source_path=str(file_path),
format=used_format,
metadata=ontology_dict.get("metadata", {}).copy()
)
self.progress.stop_tracking(
tracking_id,
status="completed",
message=f"Successfully ingested ontology from {file_path}",
)
return ontology_data
except Exception as e:
self.logger.error(f"Failed to ingest ontology: {str(e)}")
self.progress.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise ProcessingError(f"Failed to ingest ontology: {str(e)}") from e
def ingest_directory(self, directory_path: Union[str, Path], recursive: bool = True, **kwargs) -> List[OntologyData]:
"""
Ingest all ontology files in a directory.
Args:
directory_path: Path to the directory (string or Path object)
recursive: Whether to search recursively
**kwargs: Additional arguments
Returns:
List of OntologyData objects
"""
directory_path = Path(directory_path)
ontologies = []
extensions = {'.ttl', '.owl', '.rdf', '.jsonld', '.n3', '.nt'}
# Track directory ingestion
tracking_id = self.progress.start_tracking(
file=str(directory_path),
module="ingest",
submodule="OntologyIngestor",
message=f"Directory: {directory_path.name}",
)
try:
if not directory_path.exists():
raise ValidationError(f"Directory not found: {directory_path}")
if not directory_path.is_dir():
raise ValidationError(f"Path is not a directory: {directory_path}")
files_to_process = []
for root, _, files in os.walk(directory_path):
for file in files:
ext = os.path.splitext(file)[1].lower()
if ext in extensions:
files_to_process.append(os.path.join(root, file))
if not recursive:
break
total_files = len(files_to_process)
self.progress.update_tracking(
tracking_id, message=f"Processing {total_files} ontology files"
)
for idx, file_path in enumerate(files_to_process, 1):
try:
ont_data = self.ingest_ontology(file_path, **kwargs)
ontologies.append(ont_data)
self.progress.update_progress(
tracking_id,
processed=idx,
total=total_files,
message=f"Processing {idx}/{total_files}: {Path(file_path).name}"
)
except Exception as e:
self.logger.warning(f"Skipping {file_path}: {e}")
self.progress.stop_tracking(
tracking_id,
status="completed",
message=f"Ingested {len(ontologies)} ontologies",
)
return ontologies
except Exception as e:
self.progress.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise
def _convert_to_dict(self, graph: Graph, source_path: str, format: str = "unknown") -> Dict[str, Any]:
"""
Convert rdflib Graph to Semantica ontology dictionary.
Args:
graph: Parsed rdflib Graph
source_path: Source file path
format: Format of the ontology file
Returns:
Ontology dictionary
"""
ontology = {
"uri": "",
"name": os.path.basename(source_path),
"version": "1.0",
"classes": [],
"properties": [],
"metadata": {
"source_path": source_path,
"ingested_at": datetime.now().isoformat(),
"format": format
}
}
# 1. Extract Ontology Metadata
for s, p, o in graph.triples((None, RDF.type, OWL.Ontology)):
ontology["uri"] = str(s)
# Try to find label/comment/versionInfo
for _, _, label in graph.triples((s, RDFS.label, None)):
ontology["name"] = str(label)
for _, _, comment in graph.triples((s, RDFS.comment, None)):
ontology["description"] = str(comment)
for _, _, version in graph.triples((s, OWL.versionInfo, None)):
ontology["version"] = str(version)
# Break after first ontology definition found (usually only one per file)
break
# 2. Extract Classes
classes = {}
# Union of owl:Class and rdfs:Class
class_types = [OWL.Class, RDFS.Class]
for c_type in class_types:
for s, p, o in graph.triples((None, RDF.type, c_type)):
if isinstance(s, rdflib.BNode):
continue # Skip blank nodes for now
uri = str(s)
if uri not in classes:
cls_def = {
"uri": uri,
"name": self._get_local_name(uri),
"type": "class"
}
# Add label/comment
label = graph.value(s, RDFS.label)
if label:
cls_def["label"] = str(label)
cls_def["name"] = str(label) # Prefer label as name if available? Or keep URI fragment?
# Keeping local name from URI is safer for internal IDs, label for display.
# But Semantica seems to use "name" for the identifier in some examples.
# Let's keep name as local name or label if simple.
comment = graph.value(s, RDFS.comment)
if comment:
cls_def["description"] = str(comment)
# Superclasses
parents = []
for _, _, parent in graph.triples((s, RDFS.subClassOf, None)):
if isinstance(parent, rdflib.URIRef):
parents.append(str(parent))
if parents:
cls_def["parents"] = parents
classes[uri] = cls_def
ontology["classes"] = list(classes.values())
# 3. Extract Properties
properties = {}
# Object Properties
for s, p, o in graph.triples((None, RDF.type, OWL.ObjectProperty)):
self._add_property(graph, s, "object", properties)
# Datatype Properties
for s, p, o in graph.triples((None, RDF.type, OWL.DatatypeProperty)):
self._add_property(graph, s, "data", properties)
# RDF Properties (generic)
for s, p, o in graph.triples((None, RDF.type, RDF.Property)):
if str(s) not in properties: # Don't overwrite if already found as specific type
self._add_property(graph, s, "annotation", properties) # Default to annotation or generic
ontology["properties"] = list(properties.values())
return ontology
def _add_property(self, graph: Graph, subject: rdflib.term.Node, prop_type: str, properties_dict: Dict):
if isinstance(subject, rdflib.BNode):
return
uri = str(subject)
if uri in properties_dict:
return
prop_def = {
"uri": uri,
"name": self._get_local_name(uri),
"type": prop_type
}
label = graph.value(subject, RDFS.label)
if label:
prop_def["label"] = str(label)
comment = graph.value(subject, RDFS.comment)
if comment:
prop_def["description"] = str(comment)
# Domain and Range
domain = graph.value(subject, RDFS.domain)
if domain and isinstance(domain, rdflib.URIRef):
prop_def["domain"] = str(domain)
range_val = graph.value(subject, RDFS.range)
if range_val and isinstance(range_val, rdflib.URIRef):
prop_def["range"] = str(range_val)
properties_dict[uri] = prop_def
def _get_local_name(self, uri: str) -> str:
"""Extract local name from URI."""
if '#' in uri:
return uri.split('#')[-1]
return uri.split('/')[-1]
+8 -1
View File
@@ -109,12 +109,14 @@ Convenience Functions:
- create_associative_class: Associative class creation wrapper
- get_ontology_method: Get ontology method by name
- list_available_methods: List registered methods
- ingest_ontology: Ingest ontology from file or directory
Example Usage:
>>> from semantica.ontology import generate_ontology, infer_classes, OntologyGenerator
>>> from semantica.ontology import generate_ontology, infer_classes, OntologyGenerator, ingest_ontology
>>> # Using convenience functions
>>> ontology = generate_ontology({"entities": [...], "relationships": [...]}, method="default")
>>> classes = infer_classes(entities, method="default")
>>> data = ingest_ontology("ontology.ttl")
>>> # Using classes directly
>>> from semantica.ontology import OntologyGenerator, ClassInferrer, PropertyGenerator
>>> generator = OntologyGenerator(base_uri="https://example.org/ontology/")
@@ -155,6 +157,8 @@ from .registry import MethodRegistry, method_registry
from .requirements_spec import RequirementsSpec, RequirementsSpecManager
from .reuse_manager import ReuseDecision, ReuseManager
from .version_manager import OntologyVersion, VersionManager
from semantica.ingest import OntologyData, OntologyIngestor
from .methods import ingest_ontology
__all__ = [
# Main generators
@@ -200,4 +204,7 @@ __all__ = [
# Configuration
"OntologyConfig",
"ontology_config",
"ingest_ontology",
"OntologyData",
"OntologyIngestor",
]
+26 -3
View File
@@ -111,15 +111,19 @@ Main Functions:
- create_associative_class: Associative class creation wrapper
- get_ontology_method: Get ontology method by name
- list_available_methods: List registered methods
- ingest_ontology: Ingest ontology from file or directory (via semantica.ingest)
Example Usage:
>>> from semantica.ontology.methods import generate_ontology, infer_classes
>>> from semantica.ontology.methods import generate_ontology, infer_classes, ingest_ontology
>>> ontology = generate_ontology({"entities": [...], "relationships": [...]}, method="default")
>>> classes = infer_classes(entities, method="default")
>>> data = ingest_ontology("ontology.ttl")
"""
from typing import Any, Callable, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional, Union
from pathlib import Path
from semantica.ingest import ingest_ontology as _ingest_ontology, OntologyData
from .registry import method_registry
@@ -172,4 +176,23 @@ def list_available_methods(task: Optional[str] = None) -> Dict[str, List[str]]:
return method_registry.list_all(task)
pass
def ingest_ontology(
source: Union[str, Path, List[Union[str, Path]]],
method: str = "file",
**kwargs
) -> Union[OntologyData, List[OntologyData]]:
"""
Ingest ontology from source.
This is a convenience wrapper around semantica.ingest.ingest_ontology.
Args:
source: Ontology file path, directory path, or list of paths
method: Ingestion method (default: "file")
**kwargs: Additional options
Returns:
OntologyData or List[OntologyData]
"""
return _ingest_ontology(source, method=method, **kwargs)
+55
View File
@@ -42,6 +42,18 @@ classes = inferrer.infer_classes(entities, build_hierarchy=True)
properties = prop_gen.infer_properties(entities, relationships, classes)
```
### Ingesting Ontologies
```python
from semantica.ingest import OntologyIngestor
# Create ingestor
ingestor = OntologyIngestor()
# Ingest ontology
ontology_data = ingestor.ingest_ontology("ontology.ttl")
```
## Ontology Generation
### Basic Ontology Generation
@@ -115,6 +127,49 @@ ontology = engine.from_data(
)
```
## Ontology Ingestion
### Basic Ingestion
Ingest existing ontologies from files (Turtle, RDF/XML, JSON-LD, etc.) into `OntologyData` objects.
```python
from semantica.ontology import ingest_ontology
# Ingest a single file
ontology_data = ingest_ontology("path/to/ontology.ttl")
print(f"Source: {ontology_data.source_path}")
print(f"Format: {ontology_data.format}")
print(f"Data keys: {ontology_data.data.keys()}")
```
### Ingesting Directories
Ingest all ontology files in a directory recursively.
```python
from semantica.ontology import ingest_ontology
# Ingest a directory
ontologies = ingest_ontology("path/to/ontologies_dir/")
for ont in ontologies:
print(f"Ingested: {ont.source_path} ({ont.format})")
```
### Unified Ingestion Interface
You can also use the unified `semantica.ingest` interface.
```python
from semantica.ingest import ingest
# Ingest as "ontology" source type
result = ingest("path/to/ontology.ttl", source_type="ontology")
ontology_data = result["ontology"]
```
## Class Inference
### Basic Class Inference
+87
View File
@@ -363,3 +363,90 @@ class ReuseManager:
def list_known_ontologies(self) -> List[str]:
"""List known ontology URIs."""
return list(self.known_ontologies.keys())
def merge_ontology_data(
self, target: Dict[str, Any], source: Dict[str, Any], **options
) -> Dict[str, Any]:
"""
Merge source ontology data into target ontology.
Merges classes, properties, and metadata from source to target.
Handles deduplication based on URI and name.
Args:
target: Target ontology dictionary (modified in-place)
source: Source ontology dictionary
**options: Merge options:
- overwrite: Whether to overwrite existing elements (default: False)
- merge_metadata: Whether to merge metadata (default: True)
Returns:
Merged target ontology
"""
tracking_id = self.progress_tracker.start_tracking(
module="ontology",
submodule="ReuseManager",
message=f"Merging ontology {source.get('name', 'unknown')} into {target.get('name', 'unknown')}",
)
try:
overwrite = options.get("overwrite", False)
# Helper to merge lists of dicts (classes/properties)
def merge_lists(target_list, source_list, key_field="uri"):
existing_keys = {item.get(key_field): i for i, item in enumerate(target_list) if item.get(key_field)}
for item in source_list:
key = item.get(key_field)
if not key:
# Fallback to name if URI missing
key = item.get("name")
if key in existing_keys:
if overwrite:
target_list[existing_keys[key]] = item
else:
target_list.append(item)
if key:
existing_keys[key] = len(target_list) - 1
# Merge Classes
if "classes" in source:
if "classes" not in target:
target["classes"] = []
merge_lists(target["classes"], source["classes"])
# Merge Properties
if "properties" in source:
if "properties" not in target:
target["properties"] = []
merge_lists(target["properties"], source["properties"])
# Merge Metadata
if options.get("merge_metadata", True) and "metadata" in source:
if "metadata" not in target:
target["metadata"] = {}
# Update with source metadata, preserving target's specific fields if needed
# Here we just update
target["metadata"].update(source["metadata"])
# Merge Imports
if "imports" in source:
if "imports" not in target:
target["imports"] = []
for imp in source["imports"]:
if imp not in target["imports"]:
target["imports"].append(imp)
self.progress_tracker.stop_tracking(
tracking_id,
status="completed",
message=f"Merged ontology data successfully",
)
return target
except Exception as e:
self.progress_tracker.stop_tracking(
tracking_id, status="failed", message=str(e)
)
raise
+214
View File
@@ -0,0 +1,214 @@
import os
import shutil
import tempfile
import pytest
from pathlib import Path
from semantica.ingest import OntologyIngestor, ingest, ingest_ontology, OntologyData
class TestOntologyIngestor:
@pytest.fixture
def sample_ttl_content(self):
return """
@prefix : <http://example.org/ontology/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://example.org/ontology/> rdf:type owl:Ontology ;
rdfs:label "Test Ontology" .
:Person rdf:type owl:Class ;
rdfs:label "Person" .
:hasName rdf:type owl:DatatypeProperty ;
rdfs:domain :Person ;
rdfs:range xsd:string .
"""
def test_ingest_single_file(self, sample_ttl_content):
with tempfile.NamedTemporaryFile(delete=False, suffix=".ttl", mode="w") as tmp:
tmp.write(sample_ttl_content)
tmp_path = tmp.name
try:
ingestor = OntologyIngestor()
result = ingestor.ingest_ontology(tmp_path)
assert isinstance(result, OntologyData)
assert result.data["name"] == "Test Ontology" or result.data["name"] == os.path.basename(tmp_path)
assert any(cls["name"] == "Person" for cls in result.data["classes"])
assert any(prop["name"] == "hasName" for prop in result.data["properties"])
assert result.metadata["format"] == "ttl" or result.metadata["format"] == "turtle"
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_ingest_directory(self, sample_ttl_content):
with tempfile.TemporaryDirectory() as tmp_dir:
# Create two ontology files
file1 = os.path.join(tmp_dir, "ont1.ttl")
file2 = os.path.join(tmp_dir, "ont2.rdf")
with open(file1, "w") as f:
f.write(sample_ttl_content)
# Simple RDF/XML content for the second file
rdf_content = """
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:owl="http://www.w3.org/2002/07/owl#">
<owl:Ontology rdf:about="http://example.org/ont2"/>
<owl:Class rdf:about="http://example.org/ont2/Animal"/>
</rdf:RDF>
"""
with open(file2, "w") as f:
f.write(rdf_content)
ingestor = OntologyIngestor()
results = ingestor.ingest_directory(tmp_dir)
assert len(results) == 2
assert all(isinstance(r, OntologyData) for r in results)
# Verify results contain expected classes
classes = [cls["name"] for res in results for cls in res.data["classes"]]
assert "Person" in classes
assert "Animal" in classes
def test_unified_ingest_function(self, sample_ttl_content):
with tempfile.NamedTemporaryFile(delete=False, suffix=".ttl", mode="w") as tmp:
tmp.write(sample_ttl_content)
tmp_path = tmp.name
try:
# Test auto-detection via unified ingest
result = ingest(tmp_path)
assert "ontology" in result
assert isinstance(result["ontology"], OntologyData)
assert len(result["ontology"].data["classes"]) > 0
# Test explicit source type
result_explicit = ingest(tmp_path, source_type="ontology")
assert "ontology" in result_explicit
assert result_explicit["ontology"].metadata["source_path"] == tmp_path
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_convenience_function(self, sample_ttl_content):
with tempfile.NamedTemporaryFile(delete=False, suffix=".n3", mode="w") as tmp:
tmp.write(sample_ttl_content)
tmp_path = tmp.name
try:
result = ingest_ontology(tmp_path)
assert isinstance(result, OntologyData)
assert len(result.data["classes"]) > 0
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_ingest_formats(self):
"""Test ingestion of all supported formats."""
ingestor = OntologyIngestor()
# 1. JSON-LD
jsonld_content = """
{
"@context": {
"owl": "http://www.w3.org/2002/07/owl#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
},
"@id": "http://example.org/jsonld",
"@type": "owl:Ontology",
"rdfs:label": "JSON-LD Ontology",
"owl:versionInfo": "1.0"
}
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonld", mode="w") as tmp:
tmp.write(jsonld_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
assert result.data["name"] == "JSON-LD Ontology"
assert result.metadata["format"] == "json-ld"
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
# 2. N-Triples
nt_content = '<http://example.org/nt/Class> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Class> .\n'
with tempfile.NamedTemporaryFile(delete=False, suffix=".nt", mode="w") as tmp:
tmp.write(nt_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
# N-Triples often doesn't have ontology metadata, so name might default to basename
assert result.data["name"] == os.path.basename(tmp_path)
assert any(cls["uri"] == "http://example.org/nt/Class" for cls in result.data["classes"])
assert result.metadata["format"] == "nt"
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
# 3. Notation3
n3_content = """
@prefix : <http://example.org/n3/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
:N3Class a owl:Class .
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".n3", mode="w") as tmp:
tmp.write(n3_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
assert any(cls["uri"] == "http://example.org/n3/N3Class" for cls in result.data["classes"])
# format might be 'n3' or 'turtle' depending on rdflib detection as they are similar
assert result.metadata["format"] in ["n3", "turtle"]
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
# 4. RDF/XML (.owl)
owl_content = """
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:owl="http://www.w3.org/2002/07/owl#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#">
<owl:Ontology rdf:about="http://example.org/owl"/>
<owl:Class rdf:about="http://example.org/owl/OwlClass">
<rdfs:label>OwlClass</rdfs:label>
</owl:Class>
</rdf:RDF>
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".owl", mode="w") as tmp:
tmp.write(owl_content)
tmp_path = tmp.name
try:
result = ingestor.ingest_ontology(tmp_path)
assert any(cls["name"] == "OwlClass" for cls in result.data["classes"])
assert result.metadata["format"] in ["xml", "rdf", "owl"]
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def test_error_handling(self):
ingestor = OntologyIngestor()
with pytest.raises(Exception): # Specific exception type depends on implementation, likely ValidationError or FileNotFoundError
ingestor.ingest_ontology("non_existent_file.ttl")
def test_invalid_content(self):
with tempfile.NamedTemporaryFile(delete=False, suffix=".ttl", mode="w") as tmp:
tmp.write("This is not valid turtle content")
tmp_path = tmp.name
try:
ingestor = OntologyIngestor()
# Depending on implementation, this might raise an exception or return partial/empty result with error in metadata
# Given current implementation uses g.parse(), it likely raises an exception which is caught or propagated
# If propagated:
with pytest.raises(Exception):
ingestor.ingest_ontology(tmp_path)
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)