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 CHANGED
@@ -4,7 +4,7 @@ Named after the ROM construct from William Gibson's Neuromancer -
4
4
  preserved, structured knowledge that can be queried and transformed.
5
5
  """
6
6
 
7
- __version__ = "0.3.0"
7
+ __version__ = "0.4.0"
8
8
 
9
9
  from . import core, uml
10
10
  from .cli import cli
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")