Skip to content

Commit a57e0a1

Browse files
namedgraphclaude
andcommitted
Adapt GenerateOntologyViews to LDH ldh:view vocabulary
LinkedDataHub deprecated `ldh:template` (attached to classes) in favor of `ldh:view` / `ldh:inverseView` attached to properties — see LDH commits b54d8c8b6 (#267) and 1ec5da016. - Iterate distinct properties instead of (class, property) tuples - Attach views via `ldh:view` on the property (forward direction) - Skip `owl:FunctionalProperty` (clean property-level replacement for the old class-scoped `owl:maxQualifiedCardinality 1` check) - Drop class/range from view URI naming; sha1-suffix on local-name collisions Inverse views (`ldh:inverseView`) intentionally deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ab4a4eb commit a57e0a1

2 files changed

Lines changed: 34 additions & 48 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,23 +1,24 @@
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
1920
def description(cls) -> str:
20-
return "Generates LinkedDataHub view templates and SPIN queries for non-functional properties"
21+
return "Generates LinkedDataHub views and SPIN queries for ontology properties (excluding owl:FunctionalProperty)"
2122

2223
@classmethod
2324
def inputSchema(cls) -> dict:
@@ -41,45 +42,36 @@ def inputSchema(cls) -> dict:
4142
}
4243

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

60-
# Query to find all non-functional properties with their classes
61+
# Find all distinct datatype/object properties that are not owl:FunctionalProperty.
62+
# Views attach to properties (LDH `ldh:view` has rdfs:domain rdf:Property), so we
63+
# iterate by property rather than by (class, property) pair.
6164
query = """
6265
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
63-
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
6466
PREFIX owl: <http://www.w3.org/2002/07/owl#>
6567
66-
SELECT DISTINCT ?class ?property ?propertyType ?range
68+
SELECT DISTINCT ?property ?propertyType
6769
WHERE {
68-
# Get all properties with their domain
69-
?property a ?propertyType ;
70-
rdfs:domain ?class ;
71-
rdfs:range ?range .
70+
?property a ?propertyType .
7271
FILTER(?propertyType IN (owl:DatatypeProperty, owl:ObjectProperty))
73-
74-
# Exclude functional properties (those with maxQualifiedCardinality = 1)
75-
FILTER NOT EXISTS {
76-
?class rdfs:subClassOf ?restriction .
77-
?restriction a owl:Restriction ;
78-
owl:onProperty ?property ;
79-
owl:maxQualifiedCardinality 1 .
80-
}
72+
FILTER NOT EXISTS { ?property a owl:FunctionalProperty }
8173
}
82-
ORDER BY ?class ?property
74+
ORDER BY ?property
8375
"""
8476

8577
results = ontology.query(query)
@@ -94,48 +86,42 @@ def execute(self, ontology: Graph, base_uri: URIRef, service_uri: URIRef) -> Gra
9486
g.bind("rdfs", RDFS)
9587
g.bind("rdf", RDF)
9688

97-
# Generate views and queries for each non-functional property
89+
seen_locals: set[str] = set()
90+
9891
for row in results:
9992
row_dict = row.asdict()
100-
class_uri = row_dict["class"]
10193
property_uri = row_dict["property"]
10294
property_type = row_dict["propertyType"]
103-
range_uri = row_dict["range"]
10495

105-
# Validate that all values are URIRefs
106-
if not isinstance(class_uri, URIRef):
107-
raise TypeError(f"Expected class to be URIRef, got {type(class_uri)}")
10896
if not isinstance(property_uri, URIRef):
10997
raise TypeError(f"Expected property to be URIRef, got {type(property_uri)}")
11098
if not isinstance(property_type, URIRef):
11199
raise TypeError(f"Expected propertyType to be URIRef, got {type(property_type)}")
112-
if not isinstance(range_uri, URIRef):
113-
raise TypeError(f"Expected range to be URIRef, got {type(range_uri)}")
114100

115-
# Extract local names for URIs
116-
class_local = self._get_local_name(class_uri)
101+
# Disambiguate when two properties share a local name (different namespaces).
117102
property_local = self._get_local_name(property_uri)
103+
if property_local in seen_locals:
104+
suffix = hashlib.sha1(str(property_uri).encode()).hexdigest()[:6]
105+
property_local = f"{property_local}_{suffix}"
106+
seen_locals.add(property_local)
118107

119-
# Generate URIs for view and query
120-
view_uri = URIRef(f"{base_uri}#{class_local}_{property_local}_View")
121-
query_uri = URIRef(f"{base_uri}#{class_local}_{property_local}_Query")
108+
view_uri = URIRef(f"{base_uri}#{property_local}_View")
109+
query_uri = URIRef(f"{base_uri}#{property_local}_Query")
122110

123-
# Generate human-readable title
124111
title = f"{property_local}"
112+
sparql_text = self._generate_sparql_query(property_uri)
125113

126-
# Generate SPARQL query text
127-
sparql_text = self._generate_sparql_query(property_uri, property_type, range_uri)
128-
129-
# Create ldh:template link from class to view
130-
g.add((class_uri, LDH.template, view_uri))
114+
# Attach view to property via ldh:view (forward direction).
115+
# TODO: emit ldh:inverseView for selected object properties in a follow-up.
116+
g.add((property_uri, LDH.view, view_uri))
131117

132-
# Create ldh:View resource
118+
# ldh:View resource
133119
g.add((view_uri, RDF.type, LDH.View))
134120
g.add((view_uri, DCTERMS.title, Literal(title)))
135121
g.add((view_uri, SPIN.query, query_uri))
136122
g.add((view_uri, AC.mode, AC.TableMode))
137123

138-
# Create sp:Select query resource
124+
# sp:Select query resource
139125
g.add((query_uri, RDF.type, SP.Select))
140126
g.add((query_uri, DCTERMS.title, Literal(f"Select {property_local}")))
141127
g.add((query_uri, RDFS.label, Literal(f"Select {property_local}")))
@@ -153,8 +139,8 @@ def _get_local_name(self, uri: URIRef) -> str:
153139
return uri_str.split('/')[-1]
154140
return uri_str
155141

156-
def _generate_sparql_query(self, property_uri: URIRef, property_type: URIRef, range_uri: URIRef) -> str:
157-
"""Generate SPARQL SELECT query for a property"""
142+
def _generate_sparql_query(self, property_uri: URIRef) -> str:
143+
"""Generate SPARQL SELECT query for a property (forward direction)"""
158144
sparql = f"""SELECT DISTINCT ?related ?label
159145
WHERE {{
160146
GRAPH ?relatedGraph {{

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)