1+ import hashlib
12from rdflib import URIRef , Literal , Namespace , Graph
23from rdflib .namespace import RDF , RDFS , XSD , DCTERMS
34from web_algebra .operation import Operation
45
56
67class 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
159145WHERE {{
160146 GRAPH ?relatedGraph {{
0 commit comments