rdf-construct 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rdf_construct/__init__.py +1 -1
- rdf_construct/cli.py +127 -0
- rdf_construct/describe/__init__.py +93 -0
- rdf_construct/describe/analyzer.py +176 -0
- rdf_construct/describe/documentation.py +146 -0
- rdf_construct/describe/formatters/__init__.py +47 -0
- rdf_construct/describe/formatters/json.py +65 -0
- rdf_construct/describe/formatters/markdown.py +275 -0
- rdf_construct/describe/formatters/text.py +315 -0
- rdf_construct/describe/hierarchy.py +232 -0
- rdf_construct/describe/imports.py +213 -0
- rdf_construct/describe/metadata.py +187 -0
- rdf_construct/describe/metrics.py +145 -0
- rdf_construct/describe/models.py +552 -0
- rdf_construct/describe/namespaces.py +180 -0
- rdf_construct/describe/profiles.py +415 -0
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/METADATA +28 -6
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/RECORD +21 -7
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/WHEEL +0 -0
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/entry_points.txt +0 -0
- {rdf_construct-0.3.0.dist-info → rdf_construct-0.4.0.dist-info}/licenses/LICENSE +0 -0
rdf_construct/__init__.py
CHANGED
rdf_construct/cli.py
CHANGED
|
@@ -1810,6 +1810,133 @@ def stats(
|
|
|
1810
1810
|
sys.exit(1)
|
|
1811
1811
|
|
|
1812
1812
|
|
|
1813
|
+
@cli.command()
|
|
1814
|
+
@click.argument("file", type=click.Path(exists=True, path_type=Path))
|
|
1815
|
+
@click.option(
|
|
1816
|
+
"--output",
|
|
1817
|
+
"-o",
|
|
1818
|
+
type=click.Path(path_type=Path),
|
|
1819
|
+
help="Write output to file instead of stdout",
|
|
1820
|
+
)
|
|
1821
|
+
@click.option(
|
|
1822
|
+
"--format",
|
|
1823
|
+
"-f",
|
|
1824
|
+
"output_format",
|
|
1825
|
+
type=click.Choice(["text", "json", "markdown", "md"], case_sensitive=False),
|
|
1826
|
+
default="text",
|
|
1827
|
+
help="Output format (default: text)",
|
|
1828
|
+
)
|
|
1829
|
+
@click.option(
|
|
1830
|
+
"--brief",
|
|
1831
|
+
is_flag=True,
|
|
1832
|
+
help="Show brief summary only (metadata, metrics, profile)",
|
|
1833
|
+
)
|
|
1834
|
+
@click.option(
|
|
1835
|
+
"--no-resolve",
|
|
1836
|
+
is_flag=True,
|
|
1837
|
+
help="Skip import resolution checks",
|
|
1838
|
+
)
|
|
1839
|
+
@click.option(
|
|
1840
|
+
"--reasoning",
|
|
1841
|
+
is_flag=True,
|
|
1842
|
+
help="Include reasoning analysis",
|
|
1843
|
+
)
|
|
1844
|
+
@click.option(
|
|
1845
|
+
"--no-colour",
|
|
1846
|
+
"--no-color",
|
|
1847
|
+
is_flag=True,
|
|
1848
|
+
help="Disable coloured output (text format only)",
|
|
1849
|
+
)
|
|
1850
|
+
def describe(
|
|
1851
|
+
file: Path,
|
|
1852
|
+
output: Path | None,
|
|
1853
|
+
output_format: str,
|
|
1854
|
+
brief: bool,
|
|
1855
|
+
no_resolve: bool,
|
|
1856
|
+
reasoning: bool,
|
|
1857
|
+
no_colour: bool,
|
|
1858
|
+
):
|
|
1859
|
+
"""Describe an ontology: profile, metrics, imports, and structure.
|
|
1860
|
+
|
|
1861
|
+
Provides a comprehensive analysis of an RDF ontology file, including:
|
|
1862
|
+
- Profile detection (RDF, RDFS, OWL DL, OWL Full)
|
|
1863
|
+
- Basic metrics (classes, properties, individuals)
|
|
1864
|
+
- Import analysis with optional resolvability checking
|
|
1865
|
+
- Namespace categorisation
|
|
1866
|
+
- Class hierarchy analysis
|
|
1867
|
+
- Documentation coverage
|
|
1868
|
+
|
|
1869
|
+
FILE: RDF ontology file to describe (.ttl, .rdf, .owl, etc.)
|
|
1870
|
+
|
|
1871
|
+
\b
|
|
1872
|
+
Examples:
|
|
1873
|
+
# Basic description
|
|
1874
|
+
rdf-construct describe ontology.ttl
|
|
1875
|
+
|
|
1876
|
+
# Brief summary only
|
|
1877
|
+
rdf-construct describe ontology.ttl --brief
|
|
1878
|
+
|
|
1879
|
+
# JSON output for programmatic use
|
|
1880
|
+
rdf-construct describe ontology.ttl --format json -o description.json
|
|
1881
|
+
|
|
1882
|
+
# Markdown for documentation
|
|
1883
|
+
rdf-construct describe ontology.ttl --format markdown -o DESCRIPTION.md
|
|
1884
|
+
|
|
1885
|
+
# Skip slow import resolution
|
|
1886
|
+
rdf-construct describe ontology.ttl --no-resolve
|
|
1887
|
+
|
|
1888
|
+
\b
|
|
1889
|
+
Exit codes:
|
|
1890
|
+
0 - Success
|
|
1891
|
+
1 - Success with warnings (unresolvable imports, etc.)
|
|
1892
|
+
2 - Error (file not found, parse error)
|
|
1893
|
+
"""
|
|
1894
|
+
from rdf_construct.describe import describe_file, format_description
|
|
1895
|
+
|
|
1896
|
+
try:
|
|
1897
|
+
click.echo(f"Analysing {file}...", err=True)
|
|
1898
|
+
|
|
1899
|
+
# Perform analysis
|
|
1900
|
+
description = describe_file(
|
|
1901
|
+
file,
|
|
1902
|
+
brief=brief,
|
|
1903
|
+
resolve_imports=not no_resolve,
|
|
1904
|
+
include_reasoning=reasoning,
|
|
1905
|
+
)
|
|
1906
|
+
|
|
1907
|
+
# Format output
|
|
1908
|
+
use_colour = not no_colour and output_format == "text" and output is None
|
|
1909
|
+
formatted = format_description(
|
|
1910
|
+
description,
|
|
1911
|
+
format_name=output_format,
|
|
1912
|
+
use_colour=use_colour,
|
|
1913
|
+
)
|
|
1914
|
+
|
|
1915
|
+
# Write output
|
|
1916
|
+
if output:
|
|
1917
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
1918
|
+
output.write_text(formatted)
|
|
1919
|
+
click.secho(f"✓ Wrote description to {output}", fg="green", err=True)
|
|
1920
|
+
else:
|
|
1921
|
+
click.echo(formatted)
|
|
1922
|
+
|
|
1923
|
+
# Exit code based on warnings
|
|
1924
|
+
if description.imports and description.imports.unresolvable_count > 0:
|
|
1925
|
+
sys.exit(1)
|
|
1926
|
+
else:
|
|
1927
|
+
sys.exit(0)
|
|
1928
|
+
|
|
1929
|
+
except FileNotFoundError as e:
|
|
1930
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1931
|
+
sys.exit(2)
|
|
1932
|
+
except ValueError as e:
|
|
1933
|
+
click.secho(f"Error parsing RDF: {e}", fg="red", err=True)
|
|
1934
|
+
sys.exit(2)
|
|
1935
|
+
except Exception as e:
|
|
1936
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1937
|
+
sys.exit(2)
|
|
1938
|
+
|
|
1939
|
+
|
|
1813
1940
|
@cli.command()
|
|
1814
1941
|
@click.argument("sources", nargs=-1, type=click.Path(exists=True, path_type=Path))
|
|
1815
1942
|
@click.option(
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Describe command for RDF ontology analysis.
|
|
2
|
+
|
|
3
|
+
Provides quick orientation and understanding of ontology files,
|
|
4
|
+
answering: "What is this?", "How big is it?", "What does it depend on?",
|
|
5
|
+
and "Can I work with it?"
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from rdf_construct.describe import describe_file, format_description
|
|
9
|
+
|
|
10
|
+
description = describe_file(Path("ontology.ttl"))
|
|
11
|
+
print(format_description(description))
|
|
12
|
+
|
|
13
|
+
# Brief mode (metadata + metrics + profile only)
|
|
14
|
+
description = describe_file(Path("ontology.ttl"), brief=True)
|
|
15
|
+
|
|
16
|
+
# Skip import resolution (faster)
|
|
17
|
+
description = describe_file(Path("ontology.ttl"), resolve_imports=False)
|
|
18
|
+
|
|
19
|
+
# JSON output
|
|
20
|
+
print(format_description(description, format_name="json"))
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from rdf_construct.describe.models import (
|
|
24
|
+
OntologyDescription,
|
|
25
|
+
OntologyMetadata,
|
|
26
|
+
BasicMetrics,
|
|
27
|
+
ProfileDetection,
|
|
28
|
+
OntologyProfile,
|
|
29
|
+
NamespaceAnalysis,
|
|
30
|
+
NamespaceInfo,
|
|
31
|
+
NamespaceCategory,
|
|
32
|
+
ImportAnalysis,
|
|
33
|
+
ImportInfo,
|
|
34
|
+
ImportStatus,
|
|
35
|
+
HierarchyAnalysis,
|
|
36
|
+
DocumentationCoverage,
|
|
37
|
+
ReasoningAnalysis,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
from rdf_construct.describe.analyzer import (
|
|
41
|
+
describe_ontology,
|
|
42
|
+
describe_file,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from rdf_construct.describe.formatters import (
|
|
46
|
+
format_description,
|
|
47
|
+
format_text,
|
|
48
|
+
format_markdown,
|
|
49
|
+
format_json,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from rdf_construct.describe.profiles import detect_profile
|
|
53
|
+
from rdf_construct.describe.metrics import collect_metrics
|
|
54
|
+
from rdf_construct.describe.imports import analyse_imports
|
|
55
|
+
from rdf_construct.describe.namespaces import analyse_namespaces
|
|
56
|
+
from rdf_construct.describe.hierarchy import analyse_hierarchy
|
|
57
|
+
from rdf_construct.describe.documentation import analyse_documentation
|
|
58
|
+
from rdf_construct.describe.metadata import extract_metadata
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
# Main functions
|
|
63
|
+
"describe_file",
|
|
64
|
+
"describe_ontology",
|
|
65
|
+
"format_description",
|
|
66
|
+
# Formatters
|
|
67
|
+
"format_text",
|
|
68
|
+
"format_markdown",
|
|
69
|
+
"format_json",
|
|
70
|
+
# Analysis functions (for direct use)
|
|
71
|
+
"detect_profile",
|
|
72
|
+
"collect_metrics",
|
|
73
|
+
"analyse_imports",
|
|
74
|
+
"analyse_namespaces",
|
|
75
|
+
"analyse_hierarchy",
|
|
76
|
+
"analyse_documentation",
|
|
77
|
+
"extract_metadata",
|
|
78
|
+
# Data models
|
|
79
|
+
"OntologyDescription",
|
|
80
|
+
"OntologyMetadata",
|
|
81
|
+
"BasicMetrics",
|
|
82
|
+
"ProfileDetection",
|
|
83
|
+
"OntologyProfile",
|
|
84
|
+
"NamespaceAnalysis",
|
|
85
|
+
"NamespaceInfo",
|
|
86
|
+
"NamespaceCategory",
|
|
87
|
+
"ImportAnalysis",
|
|
88
|
+
"ImportInfo",
|
|
89
|
+
"ImportStatus",
|
|
90
|
+
"HierarchyAnalysis",
|
|
91
|
+
"DocumentationCoverage",
|
|
92
|
+
"ReasoningAnalysis",
|
|
93
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Main analyzer for ontology description.
|
|
2
|
+
|
|
3
|
+
Orchestrates all analysis components and aggregates results into
|
|
4
|
+
a complete OntologyDescription.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from rdflib import Graph
|
|
11
|
+
|
|
12
|
+
from rdf_construct.describe.models import OntologyDescription
|
|
13
|
+
from rdf_construct.describe.metadata import extract_metadata
|
|
14
|
+
from rdf_construct.describe.metrics import collect_metrics
|
|
15
|
+
from rdf_construct.describe.profiles import detect_profile
|
|
16
|
+
from rdf_construct.describe.namespaces import analyse_namespaces
|
|
17
|
+
from rdf_construct.describe.imports import analyse_imports
|
|
18
|
+
from rdf_construct.describe.hierarchy import analyse_hierarchy
|
|
19
|
+
from rdf_construct.describe.documentation import analyse_documentation
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def describe_ontology(
|
|
23
|
+
graph: Graph,
|
|
24
|
+
source: str | Path,
|
|
25
|
+
brief: bool = False,
|
|
26
|
+
resolve_imports: bool = True,
|
|
27
|
+
include_reasoning: bool = False,
|
|
28
|
+
) -> OntologyDescription:
|
|
29
|
+
"""Generate a complete description of an ontology.
|
|
30
|
+
|
|
31
|
+
Runs all analysis components and aggregates results.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
graph: Parsed RDF graph to analyse.
|
|
35
|
+
source: Source file path or identifier.
|
|
36
|
+
brief: If True, skip detailed analysis (imports, hierarchy, etc.).
|
|
37
|
+
resolve_imports: Whether to check resolvability of imports.
|
|
38
|
+
include_reasoning: Whether to include reasoning analysis.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
OntologyDescription with all analysis results.
|
|
42
|
+
"""
|
|
43
|
+
# Always perform core analysis
|
|
44
|
+
metadata = extract_metadata(graph)
|
|
45
|
+
metrics = collect_metrics(graph)
|
|
46
|
+
profile = detect_profile(graph)
|
|
47
|
+
|
|
48
|
+
# Create base description
|
|
49
|
+
description = OntologyDescription(
|
|
50
|
+
source=source,
|
|
51
|
+
timestamp=datetime.now(),
|
|
52
|
+
metadata=metadata,
|
|
53
|
+
metrics=metrics,
|
|
54
|
+
profile=profile,
|
|
55
|
+
brief=brief,
|
|
56
|
+
include_reasoning=include_reasoning,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Skip detailed analysis if brief mode
|
|
60
|
+
if brief:
|
|
61
|
+
return description
|
|
62
|
+
|
|
63
|
+
# Full analysis
|
|
64
|
+
description.namespaces = analyse_namespaces(graph)
|
|
65
|
+
description.imports = analyse_imports(graph, resolve=resolve_imports)
|
|
66
|
+
description.hierarchy = analyse_hierarchy(graph)
|
|
67
|
+
description.documentation = analyse_documentation(graph)
|
|
68
|
+
|
|
69
|
+
# Reasoning analysis is optional and off by default
|
|
70
|
+
if include_reasoning:
|
|
71
|
+
description.reasoning = _analyse_reasoning(graph, profile)
|
|
72
|
+
|
|
73
|
+
return description
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def describe_file(
|
|
77
|
+
file_path: Path,
|
|
78
|
+
brief: bool = False,
|
|
79
|
+
resolve_imports: bool = True,
|
|
80
|
+
include_reasoning: bool = False,
|
|
81
|
+
) -> OntologyDescription:
|
|
82
|
+
"""Generate a complete description of an ontology file.
|
|
83
|
+
|
|
84
|
+
Convenience function that handles file loading and format detection.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
file_path: Path to RDF file.
|
|
88
|
+
brief: If True, skip detailed analysis.
|
|
89
|
+
resolve_imports: Whether to check resolvability of imports.
|
|
90
|
+
include_reasoning: Whether to include reasoning analysis.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
OntologyDescription with all analysis results.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
FileNotFoundError: If file does not exist.
|
|
97
|
+
ValueError: If file cannot be parsed.
|
|
98
|
+
"""
|
|
99
|
+
if not file_path.exists():
|
|
100
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
101
|
+
|
|
102
|
+
# Detect format from extension
|
|
103
|
+
rdf_format = _infer_format(file_path)
|
|
104
|
+
|
|
105
|
+
# Parse the file
|
|
106
|
+
graph = Graph()
|
|
107
|
+
try:
|
|
108
|
+
graph.parse(str(file_path), format=rdf_format)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
raise ValueError(f"Failed to parse {file_path}: {e}") from e
|
|
111
|
+
|
|
112
|
+
return describe_ontology(
|
|
113
|
+
graph=graph,
|
|
114
|
+
source=file_path,
|
|
115
|
+
brief=brief,
|
|
116
|
+
resolve_imports=resolve_imports,
|
|
117
|
+
include_reasoning=include_reasoning,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _infer_format(path: Path) -> str:
|
|
122
|
+
"""Infer RDF format from file extension.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
path: Path to RDF file.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Format string for rdflib.
|
|
129
|
+
"""
|
|
130
|
+
suffix = path.suffix.lower()
|
|
131
|
+
format_map = {
|
|
132
|
+
".ttl": "turtle",
|
|
133
|
+
".turtle": "turtle",
|
|
134
|
+
".rdf": "xml",
|
|
135
|
+
".xml": "xml",
|
|
136
|
+
".owl": "xml",
|
|
137
|
+
".nt": "nt",
|
|
138
|
+
".ntriples": "nt",
|
|
139
|
+
".n3": "n3",
|
|
140
|
+
".jsonld": "json-ld",
|
|
141
|
+
".json": "json-ld",
|
|
142
|
+
}
|
|
143
|
+
return format_map.get(suffix, "turtle")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _analyse_reasoning(graph: Graph, profile) -> "ReasoningAnalysis":
|
|
147
|
+
"""Perform reasoning analysis (optional feature).
|
|
148
|
+
|
|
149
|
+
This is a placeholder for future reasoning analysis functionality.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
graph: RDF graph to analyse.
|
|
153
|
+
profile: Detected profile.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
ReasoningAnalysis with reasoning implications.
|
|
157
|
+
"""
|
|
158
|
+
from rdf_construct.describe.models import ReasoningAnalysis, OntologyProfile
|
|
159
|
+
|
|
160
|
+
# Determine entailment regime based on profile
|
|
161
|
+
regime_map = {
|
|
162
|
+
OntologyProfile.RDF: "none",
|
|
163
|
+
OntologyProfile.RDFS: "rdfs",
|
|
164
|
+
OntologyProfile.OWL_DL_SIMPLE: "owl-dl",
|
|
165
|
+
OntologyProfile.OWL_DL_EXPRESSIVE: "owl-dl",
|
|
166
|
+
OntologyProfile.OWL_FULL: "owl-full",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
regime = regime_map.get(profile.profile, "unknown")
|
|
170
|
+
|
|
171
|
+
return ReasoningAnalysis(
|
|
172
|
+
entailment_regime=regime,
|
|
173
|
+
inferred_superclasses=[],
|
|
174
|
+
inferred_types=[],
|
|
175
|
+
consistency_notes=[],
|
|
176
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Documentation coverage analysis for ontology description.
|
|
2
|
+
|
|
3
|
+
Analyses the presence of labels and definitions for classes and properties.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from rdflib import Graph, URIRef, RDF, RDFS
|
|
7
|
+
from rdflib.namespace import OWL
|
|
8
|
+
|
|
9
|
+
from rdf_construct.describe.models import DocumentationCoverage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Predicates considered as providing a label
|
|
13
|
+
LABEL_PREDICATES = {
|
|
14
|
+
RDFS.label,
|
|
15
|
+
URIRef("http://www.w3.org/2004/02/skos/core#prefLabel"),
|
|
16
|
+
URIRef("http://www.w3.org/2004/02/skos/core#altLabel"),
|
|
17
|
+
URIRef("http://purl.org/dc/elements/1.1/title"),
|
|
18
|
+
URIRef("http://purl.org/dc/terms/title"),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Predicates considered as providing a definition/description
|
|
22
|
+
DEFINITION_PREDICATES = {
|
|
23
|
+
RDFS.comment,
|
|
24
|
+
URIRef("http://www.w3.org/2004/02/skos/core#definition"),
|
|
25
|
+
URIRef("http://purl.org/dc/elements/1.1/description"),
|
|
26
|
+
URIRef("http://purl.org/dc/terms/description"),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def analyse_documentation(graph: Graph) -> DocumentationCoverage:
|
|
31
|
+
"""Analyse documentation coverage for classes and properties.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
graph: RDF graph to analyse.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
DocumentationCoverage with coverage metrics.
|
|
38
|
+
"""
|
|
39
|
+
# Get all classes
|
|
40
|
+
classes = _get_all_classes(graph)
|
|
41
|
+
classes_total = len(classes)
|
|
42
|
+
|
|
43
|
+
# Get all properties
|
|
44
|
+
properties = _get_all_properties(graph)
|
|
45
|
+
properties_total = len(properties)
|
|
46
|
+
|
|
47
|
+
# Count classes with labels
|
|
48
|
+
classes_with_label = sum(1 for cls in classes if _has_label(graph, cls))
|
|
49
|
+
|
|
50
|
+
# Count classes with definitions
|
|
51
|
+
classes_with_definition = sum(1 for cls in classes if _has_definition(graph, cls))
|
|
52
|
+
|
|
53
|
+
# Count properties with labels
|
|
54
|
+
properties_with_label = sum(1 for prop in properties if _has_label(graph, prop))
|
|
55
|
+
|
|
56
|
+
# Count properties with definitions
|
|
57
|
+
properties_with_definition = sum(
|
|
58
|
+
1 for prop in properties if _has_definition(graph, prop)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return DocumentationCoverage(
|
|
62
|
+
classes_with_label=classes_with_label,
|
|
63
|
+
classes_total=classes_total,
|
|
64
|
+
classes_with_definition=classes_with_definition,
|
|
65
|
+
properties_with_label=properties_with_label,
|
|
66
|
+
properties_total=properties_total,
|
|
67
|
+
properties_with_definition=properties_with_definition,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _get_all_classes(graph: Graph) -> set[URIRef]:
|
|
72
|
+
"""Get all classes from the graph.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
graph: RDF graph to query.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Set of class URIRefs.
|
|
79
|
+
"""
|
|
80
|
+
classes: set[URIRef] = set()
|
|
81
|
+
|
|
82
|
+
for cls in graph.subjects(RDF.type, OWL.Class):
|
|
83
|
+
if isinstance(cls, URIRef):
|
|
84
|
+
classes.add(cls)
|
|
85
|
+
|
|
86
|
+
for cls in graph.subjects(RDF.type, RDFS.Class):
|
|
87
|
+
if isinstance(cls, URIRef):
|
|
88
|
+
classes.add(cls)
|
|
89
|
+
|
|
90
|
+
return classes
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_all_properties(graph: Graph) -> set[URIRef]:
|
|
94
|
+
"""Get all properties from the graph.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
graph: RDF graph to query.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Set of property URIRefs.
|
|
101
|
+
"""
|
|
102
|
+
properties: set[URIRef] = set()
|
|
103
|
+
|
|
104
|
+
for prop_type in (
|
|
105
|
+
OWL.ObjectProperty,
|
|
106
|
+
OWL.DatatypeProperty,
|
|
107
|
+
OWL.AnnotationProperty,
|
|
108
|
+
RDF.Property,
|
|
109
|
+
):
|
|
110
|
+
for prop in graph.subjects(RDF.type, prop_type):
|
|
111
|
+
if isinstance(prop, URIRef):
|
|
112
|
+
properties.add(prop)
|
|
113
|
+
|
|
114
|
+
return properties
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _has_label(graph: Graph, subject: URIRef) -> bool:
|
|
118
|
+
"""Check if a subject has any label predicate.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
graph: RDF graph to query.
|
|
122
|
+
subject: Subject to check.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
True if subject has at least one label.
|
|
126
|
+
"""
|
|
127
|
+
for pred in LABEL_PREDICATES:
|
|
128
|
+
if any(graph.objects(subject, pred)):
|
|
129
|
+
return True
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _has_definition(graph: Graph, subject: URIRef) -> bool:
|
|
134
|
+
"""Check if a subject has any definition/description predicate.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
graph: RDF graph to query.
|
|
138
|
+
subject: Subject to check.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
True if subject has at least one definition.
|
|
142
|
+
"""
|
|
143
|
+
for pred in DEFINITION_PREDICATES:
|
|
144
|
+
if any(graph.objects(subject, pred)):
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Output formatters for ontology description."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from rdf_construct.describe.models import OntologyDescription
|
|
6
|
+
from rdf_construct.describe.formatters.text import format_text
|
|
7
|
+
from rdf_construct.describe.formatters.markdown import format_markdown
|
|
8
|
+
from rdf_construct.describe.formatters.json import format_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_description(
|
|
12
|
+
description: OntologyDescription,
|
|
13
|
+
format_name: str = "text",
|
|
14
|
+
use_colour: bool = True,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Format ontology description for output.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
description: The description to format.
|
|
20
|
+
format_name: Output format ("text", "json", "markdown", "md").
|
|
21
|
+
use_colour: Whether to use ANSI colour codes (text format only).
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Formatted string representation.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If format_name is not recognised.
|
|
28
|
+
"""
|
|
29
|
+
format_name = format_name.lower()
|
|
30
|
+
|
|
31
|
+
if format_name == "text":
|
|
32
|
+
return format_text(description, use_colour=use_colour)
|
|
33
|
+
elif format_name == "json":
|
|
34
|
+
return format_json(description)
|
|
35
|
+
elif format_name in ("markdown", "md"):
|
|
36
|
+
return format_markdown(description)
|
|
37
|
+
else:
|
|
38
|
+
valid = "text, json, markdown, md"
|
|
39
|
+
raise ValueError(f"Unknown format '{format_name}'. Valid formats: {valid}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"format_description",
|
|
44
|
+
"format_text",
|
|
45
|
+
"format_markdown",
|
|
46
|
+
"format_json",
|
|
47
|
+
]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""JSON formatter for ontology description output.
|
|
2
|
+
|
|
3
|
+
Produces structured JSON for programmatic consumption.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from rdf_construct.describe.models import OntologyDescription
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def format_json(
|
|
13
|
+
description: OntologyDescription,
|
|
14
|
+
indent: int = 2,
|
|
15
|
+
ensure_ascii: bool = False,
|
|
16
|
+
) -> str:
|
|
17
|
+
"""Format ontology description as JSON.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
description: OntologyDescription to format.
|
|
21
|
+
indent: Indentation level for pretty printing.
|
|
22
|
+
ensure_ascii: If True, escape non-ASCII characters.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
JSON string.
|
|
26
|
+
"""
|
|
27
|
+
data = description.to_dict()
|
|
28
|
+
|
|
29
|
+
return json.dumps(
|
|
30
|
+
data,
|
|
31
|
+
indent=indent,
|
|
32
|
+
ensure_ascii=ensure_ascii,
|
|
33
|
+
default=_json_serializer,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _json_serializer(obj: Any) -> Any:
|
|
38
|
+
"""Custom JSON serializer for non-standard types.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
obj: Object to serialize.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
JSON-serializable representation.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
TypeError: If object cannot be serialized.
|
|
48
|
+
"""
|
|
49
|
+
# Handle Path objects
|
|
50
|
+
if hasattr(obj, "__fspath__"):
|
|
51
|
+
return str(obj)
|
|
52
|
+
|
|
53
|
+
# Handle datetime
|
|
54
|
+
if hasattr(obj, "isoformat"):
|
|
55
|
+
return obj.isoformat()
|
|
56
|
+
|
|
57
|
+
# Handle enums
|
|
58
|
+
if hasattr(obj, "value"):
|
|
59
|
+
return obj.value
|
|
60
|
+
|
|
61
|
+
# Handle dataclasses with to_dict method
|
|
62
|
+
if hasattr(obj, "to_dict"):
|
|
63
|
+
return obj.to_dict()
|
|
64
|
+
|
|
65
|
+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
|