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
@@ -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
163149WHERE {{
164150 GRAPH ?relatedGraph {{
0 commit comments