Skip to content

Commit c714b79

Browse files
authored
Merge pull request #19 from AtomGraph/feat-ldh-view-vocabulary
Adapt GenerateOntologyViews to LDH ldh:view vocabulary
2 parents 64cb542 + d1fb302 commit c714b79

1 file changed

Lines changed: 33 additions & 47 deletions

File tree

src/web_algebra/operations/linkeddatahub/content/generate_ontology_views.py

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1+
import hashlib
12
from rdflib import URIRef, Literal, Namespace, Graph
23
from rdflib.namespace import RDF, RDFS, XSD, DCTERMS
34
from web_algebra.operation import Operation
45

56

67
class GenerateOntologyViews(Operation):
7-
"""Generates LinkedDataHub view templates for non-functional properties.
8+
"""Generates LinkedDataHub views for ontology properties.
89
910
Takes an extracted ontology graph and generates an RDF graph containing:
1011
- ldh:View resources for each non-functional property
1112
- SPIN sp:Select queries for retrieving related resources
12-
- ldh:template links from classes to views
13+
- ldh:view links from properties to views
1314
14-
A property is considered non-functional if it does not have a
15-
owl:maxQualifiedCardinality restriction of 1.
15+
Functional properties (declared `owl:FunctionalProperty`) are skipped:
16+
they yield at most one value, so a table view would be redundant.
1617
"""
1718

1819
@classmethod
@@ -21,7 +22,7 @@ def name(cls):
2122

2223
@classmethod
2324
def description(cls) -> str:
24-
return "Generates LinkedDataHub view templates and SPIN queries for non-functional properties"
25+
return "Generates LinkedDataHub views and SPIN queries for ontology properties (excluding owl:FunctionalProperty)"
2526

2627
@classmethod
2728
def inputSchema(cls) -> dict:
@@ -45,45 +46,36 @@ def inputSchema(cls) -> dict:
4546
}
4647

4748
def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Graph:
48-
"""Generate LDH view templates for non-functional properties
49+
"""Generate LDH views for ontology properties
4950
5051
Args:
51-
ontology: RDF graph containing classes and properties with optional restrictions
52+
ontology: RDF graph containing property declarations
5253
base_uri: Base URI for generating view and query resource URIs
5354
service_uri: URI of the sd:Service resource to be referenced by queries
5455
5556
Returns:
56-
RDF graph containing ldh:View, sp:Select, and ldh:template triples
57+
RDF graph containing ldh:View, sp:Select, and ldh:view triples
5758
"""
5859
# Define namespaces
5960
LDH = Namespace("https://w3id.org/atomgraph/linkeddatahub#")
6061
SP = Namespace("http://spinrdf.org/sp#")
6162
SPIN = Namespace("http://spinrdf.org/spin#")
6263
AC = Namespace("https://w3id.org/atomgraph/client#")
6364

64-
# Query to find all non-functional properties with their classes
65+
# Find all distinct datatype/object properties that are not owl:FunctionalProperty.
66+
# Views attach to properties (LDH `ldh:view` has rdfs:domain rdf:Property), so we
67+
# iterate by property rather than by (class, property) pair.
6568
query = """
6669
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
67-
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
6870
PREFIX owl: <http://www.w3.org/2002/07/owl#>
6971
70-
SELECT DISTINCT ?class ?property ?propertyType ?range
72+
SELECT DISTINCT ?property ?propertyType
7173
WHERE {
72-
# Get all properties with their domain
73-
?property a ?propertyType ;
74-
rdfs:domain ?class ;
75-
rdfs:range ?range .
74+
?property a ?propertyType .
7675
FILTER(?propertyType IN (owl:DatatypeProperty, owl:ObjectProperty))
77-
78-
# Exclude functional properties (those with maxQualifiedCardinality = 1)
79-
FILTER NOT EXISTS {
80-
?class rdfs:subClassOf ?restriction .
81-
?restriction a owl:Restriction ;
82-
owl:onProperty ?property ;
83-
owl:maxQualifiedCardinality 1 .
84-
}
76+
FILTER NOT EXISTS { ?property a owl:FunctionalProperty }
8577
}
86-
ORDER BY ?class ?property
78+
ORDER BY ?property
8779
"""
8880

8981
results = ontology.query(query)
@@ -98,48 +90,42 @@ def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Gra
9890
g.bind("rdfs", RDFS)
9991
g.bind("rdf", RDF)
10092

101-
# Generate views and queries for each non-functional property
93+
seen_locals: set[str] = set()
94+
10295
for row in results:
10396
row_dict = row.asdict()
104-
class_uri = row_dict["class"]
10597
property_uri = row_dict["property"]
10698
property_type = row_dict["propertyType"]
107-
range_uri = row_dict["range"]
10899

109-
# Validate that all values are URIRefs
110-
if not isinstance(class_uri, URIRef):
111-
raise TypeError(f"Expected class to be URIRef, got {type(class_uri)}")
112100
if not isinstance(property_uri, URIRef):
113101
raise TypeError(f"Expected property to be URIRef, got {type(property_uri)}")
114102
if not isinstance(property_type, URIRef):
115103
raise TypeError(f"Expected propertyType to be URIRef, got {type(property_type)}")
116-
if not isinstance(range_uri, URIRef):
117-
raise TypeError(f"Expected range to be URIRef, got {type(range_uri)}")
118104

119-
# Extract local names for URIs
120-
class_local = self._get_local_name(class_uri)
105+
# Disambiguate when two properties share a local name (different namespaces).
121106
property_local = self._get_local_name(property_uri)
107+
if property_local in seen_locals:
108+
suffix = hashlib.sha1(str(property_uri).encode()).hexdigest()[:6]
109+
property_local = f"{property_local}_{suffix}"
110+
seen_locals.add(property_local)
122111

123-
# Generate URIs for view and query
124-
view_uri = URIRef(f"{base_uri}#{class_local}_{property_local}_View")
125-
query_uri = URIRef(f"{base_uri}#{class_local}_{property_local}_Query")
112+
view_uri = URIRef(f"{base_uri}#{property_local}_View")
113+
query_uri = URIRef(f"{base_uri}#{property_local}_Query")
126114

127-
# Generate human-readable title
128115
title = f"{property_local}"
116+
sparql_text = self._generate_sparql_query(property_uri)
129117

130-
# Generate SPARQL query text
131-
sparql_text = self._generate_sparql_query(property_uri, property_type, range_uri)
132-
133-
# Create ldh:template link from class to view
134-
g.add((class_uri, LDH.template, view_uri))
118+
# Attach view to property via ldh:view (forward direction).
119+
# TODO: emit ldh:inverseView for selected object properties in a follow-up.
120+
g.add((property_uri, LDH.view, view_uri))
135121

136-
# Create ldh:View resource
122+
# ldh:View resource
137123
g.add((view_uri, RDF.type, LDH.View))
138124
g.add((view_uri, DCTERMS.title, Literal(title)))
139125
g.add((view_uri, SPIN.query, query_uri))
140126
g.add((view_uri, AC.mode, AC.TableMode))
141127

142-
# Create sp:Select query resource
128+
# sp:Select query resource
143129
g.add((query_uri, RDF.type, SP.Select))
144130
g.add((query_uri, DCTERMS.title, Literal(f"Select {property_local}")))
145131
g.add((query_uri, RDFS.label, Literal(f"Select {property_local}")))
@@ -157,8 +143,8 @@ def _get_local_name(self, uri: URIRef) -> str:
157143
return uri_str.split('/')[-1]
158144
return uri_str
159145

160-
def _generate_sparql_query(self, property_uri: URIRef, property_type: URIRef, range_uri: URIRef) -> str:
161-
"""Generate SPARQL SELECT query for a property"""
146+
def _generate_sparql_query(self, property_uri: URIRef) -> str:
147+
"""Generate SPARQL SELECT query for a property (forward direction)"""
162148
sparql = f"""SELECT DISTINCT ?related ?label
163149
WHERE {{
164150
GRAPH ?relatedGraph {{

0 commit comments

Comments
 (0)