diff --git a/prez/reference_data/endpoints/endpoint_nodeshapes.ttl b/prez/reference_data/endpoints/endpoint_nodeshapes.ttl index 4c08855d..33afc14d 100644 --- a/prez/reference_data/endpoints/endpoint_nodeshapes.ttl +++ b/prez/reference_data/endpoints/endpoint_nodeshapes.ttl @@ -19,7 +19,6 @@ ex:Catalogs sh:property [ sh:path dcterms:hasPart ; sh:or ( - [ sh:class dcat:Resource ] [ sh:class skos:ConceptScheme ] [ sh:class skos:Collection ] [ sh:class dcat:Dataset ] @@ -29,7 +28,7 @@ ex:Catalogs ex:Collections a sh:NodeShape ; ont:hierarchyLevel 2 ; - sh:targetClass skos:ConceptScheme , skos:Collection , dcat:Resource , dcat:Dataset ; + sh:targetClass skos:ConceptScheme , skos:Collection , dcat:Dataset ; sh:property [ sh:path [ sh:inversePath dcterms:hasPart ] ; sh:class dcat:Catalog ; @@ -63,10 +62,10 @@ ex:CollectionConcept ex:Resource a sh:NodeShape ; ont:hierarchyLevel 3 ; - sh:targetClass rdf:Resource ; + sh:targetClass geo:FeatureCollection ; sh:property [ sh:path [ sh:inversePath dcterms:hasPart ] ; - sh:class dcat:Resource ; + sh:class dcat:Dataset ; ] , [ sh:path ( [ sh:inversePath dcterms:hasPart ] [ sh:inversePath dcterms:hasPart ] ); sh:class dcat:Catalog ; diff --git a/prez/reference_data/profiles/ogc_records_profile.ttl b/prez/reference_data/profiles/ogc_records_profile.ttl index 2af49074..072c29af 100644 --- a/prez/reference_data/profiles/ogc_records_profile.ttl +++ b/prez/reference_data/profiles/ogc_records_profile.ttl @@ -109,13 +109,7 @@ prez:OGCItemProfile ], [ sh:maxCount 0 ; - sh:path - ( - sh:union ( - dcterms:hasPart - rdfs:member - ) - ) + sh:path dcterms:hasPart , rdfs:member ; ] ; shext:bnode-depth 2 ; altr-ext:constrainsClass @@ -143,26 +137,17 @@ prez:OGCSchemesListProfile "text/turtle" ; altr-ext:hasDefaultResourceFormat "text/anot+turtle" ; altr-ext:constrainsClass skos:ConceptScheme ; - sh:property - [ - sh:minCount 0 ; - sh:path dcterms:publisher ; - ], - [ - sh:minCount 0 ; - sh:path reg:status ; - ], - [ - sh:minCount 0 ; - sh:path ( prov:qualifiedDerivation prov:hadRole ) ; - ], - [ - sh:minCount 0 ; - sh:path ( prov:qualifiedDerivation prov:entity ) ; - ], - [ - sh:path rdf:type - ] + sh:property [ + sh:path ( + sh:union ( + rdf:type + dcterms:publisher + reg:status + ( prov:qualifiedDerivation prov:hadRole ) + ( prov:qualifiedDerivation prov:entity ) + ) + ) + ] ; . diff --git a/prez/services/query_generation/shacl.py b/prez/services/query_generation/shacl.py index 0bebcbbb..d7381f25 100755 --- a/prez/services/query_generation/shacl.py +++ b/prez/services/query_generation/shacl.py @@ -1,7 +1,7 @@ from __future__ import annotations from string import Template -from typing import List, Optional, Any, Dict, Literal as TypingLiteral, Union, Tuple +from typing import List, Optional, Any, Dict, Literal as TypingLiteral, Union, Tuple, Type from pydantic import BaseModel from rdflib import URIRef, BNode, Graph, RDFS @@ -9,6 +9,14 @@ from rdflib.namespace import SH, RDF from rdflib.term import Node from sparql_grammar_pydantic import ( + InlineData, + DataBlock, + InlineDataOneVar, + DataBlockValue, + Filter, + Constraint, + OptionalGraphPattern, + IRIOrFunction, IRI, Var, GraphPatternNotTriples, @@ -19,14 +27,16 @@ GroupGraphPatternSub, TriplesBlock, TriplesSameSubjectPath, - InlineData, - DataBlock, - InlineDataOneVar, - DataBlockValue, - Filter, - Constraint, - OptionalGraphPattern, - IRIOrFunction, + PropertyListPathNotEmpty, + VerbPath, + SG_Path, + PathAlternative, + ObjectListPath, + ObjectPath, + GraphNodePath, + VarOrTerm, + GraphTerm, + GroupOrUnionGraphPattern, PathElt, PathEltOrInverse, PathPrimary, PathSequence, PathMod, ) from prez.reference_data.prez_ns import ONT, SHEXT @@ -242,6 +252,8 @@ def optional_gpnt(depth): self.gpnt_list.append(nested_ogp) + + class PropertyShape(Shape): uri: URIRef | BNode # URI of the shape graph: Graph @@ -249,7 +261,8 @@ class PropertyShape(Shape): focus_node: Union[IRI, Var] # inputs shape_number: int = 0 - property_paths: Optional[List[PropertyPath]] = None + and_property_paths: Optional[List[PropertyPath]] = None + union_property_paths: Optional[List[PropertyPath]] = None or_klasses: Optional[List[URIRef]] = None # outputs grammar: Optional[GroupGraphPatternSub] = None @@ -273,7 +286,8 @@ def maxCount(self): return int(maxc) def from_graph(self): - self.property_paths = [] + self.and_property_paths = [] + self.union_property_paths = [] _single_class = next(self.graph.objects(self.uri, SH["class"]), None) if _single_class: self.or_klasses = [URIRef(_single_class)] @@ -289,39 +303,57 @@ def from_graph(self): for pp in pps: self._process_property_path(pp) # get the longest property path first - for endpoints this will be the path any path_nodes apply to - self.property_paths = sorted( - self.property_paths, key=lambda x: len(x), reverse=True + self.and_property_paths = sorted( + self.and_property_paths, key=lambda x: len(x), reverse=True ) - def _process_property_path(self, pp): + def _process_property_path(self, pp, union: bool = False): if isinstance(pp, URIRef): - self.property_paths.append(Path(value=pp)) + self._add_path(Path(value=pp), union) elif isinstance(pp, BNode): - pred_objects_gen = self.graph.predicate_objects(subject=pp) - bn_pred, bn_obj = next(pred_objects_gen, (None, None)) + pred_objects = list(self.graph.predicate_objects(subject=pp)) + if not pred_objects: + return + + bn_pred, bn_obj = pred_objects[0] + if bn_obj == SH.union: - union_list = list(Collection(self.graph, pp)) - if union_list != [SH.union]: - union_list_bnode = union_list[1] - union_items = list(Collection(self.graph, union_list_bnode)) - for item in union_items: - self._process_property_path(item) - elif bn_pred == SH.inversePath: - self.property_paths.append(InversePath(value=bn_obj)) - # elif bn_pred == SH.alternativePath: - # predicates.extend(list(Collection(self.profile_graph, bn_obj))) + self._process_union(pp, union) + elif bn_pred in PRED_TO_PATH_CLASS: + path_class = PRED_TO_PATH_CLASS[bn_pred] + self._add_path(path_class(value=bn_obj), union) else: # sequence paths - paths = list(Collection(self.graph, pp)) - sp_list = [] - for path in paths: - if isinstance(path, BNode): - pred_objects_gen = self.graph.predicate_objects(subject=path) - bn_pred, bn_obj = next(pred_objects_gen, (None, None)) - if bn_pred == SH.inversePath: - sp_list.append(InversePath(value=bn_obj)) - elif isinstance(path, URIRef): - sp_list.append(Path(value=path)) - self.property_paths.append(SequencePath(value=sp_list)) + self._process_sequence(pp, union) + + def _process_union(self, pp, union: bool): + union_list = list(Collection(self.graph, pp)) + if union_list != [SH.union]: + union_list_bnode = union_list[1] + union_items = list(Collection(self.graph, union_list_bnode)) + for item in union_items: + self._process_property_path(item, True) + + def _process_sequence(self, pp, union: bool): + paths = list(Collection(self.graph, pp)) + sp_list = [] + for path in paths: + if isinstance(path, BNode): + pred_objects = list(self.graph.predicate_objects(subject=path)) + if pred_objects: + bn_pred, bn_obj = pred_objects[0] + if bn_pred in PRED_TO_PATH_CLASS: + path_class = PRED_TO_PATH_CLASS[bn_pred] + sp_list.append(path_class(value=bn_obj)) + elif isinstance(path, URIRef): + sp_list.append(Path(value=path)) + self._add_path(SequencePath(value=sp_list), union) + + def _add_path(self, path: PropertyPath, union: bool): + if union: + self.union_property_paths.append(path) + else: + self.and_property_paths.append(path) + def to_grammar(self): # label nodes in the inner select and profile part of the query differently for clarity. @@ -331,135 +363,89 @@ def to_grammar(self): path_or_prop = f"prof_{self.shape_number + 1}" # set up the path nodes - either from supplied values or set as variables - total_individual_nodes = sum([len(i) for i in self.property_paths]) + total_individual_nodes = sum([len(i) for i in self.and_property_paths]) for i in range(total_individual_nodes): path_node_str = f"{path_or_prop}_node_{i + 1}" if path_node_str not in self.path_nodes: self.path_nodes[path_node_str] = Var(value=path_node_str) self.tssp_list = [] - len_pp = max([len(i) for i in self.property_paths]) - # sh:class applies to the end of sequence paths - if f"{path_or_prop}_node_{len_pp}" in self.path_nodes: - path_node_term = self.path_nodes[f"{path_or_prop}_node_{len_pp}"] - else: - path_node_term = Var(value=f"{path_or_prop}_node_{len_pp}") - - # useful for determining which endpoint property shape should be used when a request comes in on endpoint - self.classes_at_len[f"{path_or_prop}_node_{len_pp}"] = self.or_klasses - - if self.or_klasses: - if len(self.or_klasses) == 1: - self.add_triple_to_tss_and_tssp( - ( - path_node_term, - IRI(value=RDF.type), - IRI(value=self.or_klasses[0]), - ) - ) + if path_or_prop == "path": + len_pp = max([len(i) for i in self.and_property_paths]) + # sh:class applies to the end of sequence paths + if f"{path_or_prop}_node_{len_pp}" in self.path_nodes: + path_node_term = self.path_nodes[f"{path_or_prop}_node_{len_pp}"] else: - self.add_triple_to_tss_and_tssp( - ( - path_node_term, - IRI(value=RDF.type), - Var(value=f"{path_or_prop}_node_classes_{len_pp}"), - ) - ) - dbvs = [ - DataBlockValue(value=IRI(value=klass)) for klass in self.or_klasses - ] - self.gpnt_list.append( - GraphPatternNotTriples( - content=InlineData( - data_block=DataBlock( - block=InlineDataOneVar( - variable=Var( - value=f"{path_or_prop}_node_classes_{len_pp}" - ), - datablockvalues=dbvs, - ) - ) - ) - ) - ) + path_node_term = Var(value=f"{path_or_prop}_node_{len_pp}") - if self.property_paths: - i = 0 - for property_path in self.property_paths: - if f"{path_or_prop}_node_{i + 1}" in self.path_nodes: - path_node_1 = self.path_nodes[f"{path_or_prop}_node_{i + 1}"] - else: - path_node_1 = Var(value=f"{path_or_prop}_node_{i + 1}") - # for sequence paths up to length two: - if f"{path_or_prop}_node_{i + 2}" in self.path_nodes: - path_node_2 = self.path_nodes[f"{path_or_prop}_node_{i + 2}"] - else: - path_node_2 = Var(value=f"{path_or_prop}_node_{i + 2}") - - if isinstance(property_path, Path): - if property_path.value == SHEXT.allPredicateValues: - pred = Var(value="preds") - obj = Var(value="vals") - else: - pred = IRI(value=property_path.value) - obj = path_node_1 - # vanilla property path + # useful for determining which endpoint property shape should be used when a request comes in on endpoint + self.classes_at_len[f"{path_or_prop}_node_{len_pp}"] = self.or_klasses + + if self.or_klasses: + if len(self.or_klasses) == 1: self.add_triple_to_tss_and_tssp( ( - self.focus_node, - pred, - obj, + path_node_term, + IRI(value=RDF.type), + IRI(value=self.or_klasses[0]), ) ) - i += 1 - - elif isinstance(property_path, InversePath): + else: self.add_triple_to_tss_and_tssp( ( - path_node_1, - IRI(value=property_path.value), - self.focus_node, + path_node_term, + IRI(value=RDF.type), + Var(value=f"{path_or_prop}_node_classes_{len_pp}"), ) ) - i += 1 - - elif isinstance(property_path, SequencePath): - for j, path in enumerate(property_path.value): - if isinstance(path, Path): - if j == 0: - self.add_triple_to_tss_and_tssp( - ( - self.focus_node, - IRI(value=path.value), - path_node_1, - ) - ) - else: - self.add_triple_to_tss_and_tssp( - ( - path_node_1, - IRI(value=path.value), - path_node_2, - ) - ) - elif isinstance(path, InversePath): - if j == 0: - self.add_triple_to_tss_and_tssp( - ( - path_node_1, - IRI(value=path.value), - self.focus_node, - ) - ) - else: - self.add_triple_to_tss_and_tssp( - ( - path_node_2, - IRI(value=path.value), - path_node_1, + dbvs = [ + DataBlockValue(value=IRI(value=klass)) + for klass in self.or_klasses + ] + self.gpnt_list.append( + GraphPatternNotTriples( + content=InlineData( + data_block=DataBlock( + block=InlineDataOneVar( + variable=Var( + value=f"{path_or_prop}_node_classes_{len_pp}" + ), + datablockvalues=dbvs, ) ) - i += len(property_path) + ) + ) + ) + + pp_i = 0 + tssp_list_for_and = [] + tssp_list_for_union = [] + if self.and_property_paths: + self.process_property_paths( + self.and_property_paths, path_or_prop, tssp_list_for_and, pp_i + ) + for inner_list in tssp_list_for_and: + self.tssp_list.extend(inner_list) + if self.union_property_paths: + self.process_property_paths( + self.union_property_paths, path_or_prop, tssp_list_for_union, pp_i + ) + ggp_list = [] + for inner_list in tssp_list_for_union: + ggp_list.append( + GroupGraphPattern( + content=GroupGraphPatternSub( + triples_block=TriplesBlock.from_tssp_list(inner_list) + ) + ) + ) + self.gpnt_list.append( + GraphPatternNotTriples( + content=GroupOrUnionGraphPattern( + group_graph_patterns=ggp_list + ) + ) + ) if self.minCount == 0: # triples = self.tssp_list.copy() @@ -479,7 +465,7 @@ def to_grammar(self): self.tssp_list = [] if self.maxCount == 0: - for p in self.property_paths: + for p in self.and_property_paths: assert isinstance(p, Path) # only support filtering direct predicates # reset the triples list @@ -493,7 +479,7 @@ def to_grammar(self): values = [ PrimaryExpression(content=IRIOrFunction(iri=IRI(value=p.value))) - for p in self.property_paths + for p in self.and_property_paths ] gpnt = GraphPatternNotTriples( content=Filter.filter_relational( @@ -504,6 +490,116 @@ def to_grammar(self): ) self.gpnt_list.append(gpnt) + def process_property_paths(self, property_paths, path_or_prop, tssp_list, pp_i): + for property_path in property_paths: + if f"{path_or_prop}_node_{pp_i + 1}" in self.path_nodes: + path_node_1 = self.path_nodes[f"{path_or_prop}_node_{pp_i + 1}"] + else: + path_node_1 = Var(value=f"{path_or_prop}_node_{pp_i + 1}") + + if f"{path_or_prop}_node_{pp_i + 2}" in self.path_nodes: + path_node_2 = self.path_nodes[f"{path_or_prop}_node_{pp_i + 2}"] + else: + path_node_2 = Var(value=f"{path_or_prop}_node_{pp_i + 2}") + + current_tssp = [] + + if isinstance(property_path, Path): + if property_path.value == SHEXT.allPredicateValues: + pred = Var(value="preds") + obj = Var(value="vals") + else: + pred = IRI(value=property_path.value) + obj = path_node_1 + triple = (self.focus_node, pred, obj) + self.tss_list.append(TriplesSameSubject.from_spo(*triple)) + current_tssp.append(TriplesSameSubjectPath.from_spo(*triple)) + pp_i += 1 + + elif isinstance(property_path, InversePath): + triple = (path_node_1, IRI(value=property_path.value), self.focus_node) + self.tss_list.append(TriplesSameSubject.from_spo(*triple)) + current_tssp.append(TriplesSameSubjectPath.from_spo(*triple)) + pp_i += 1 + + elif isinstance(property_path, Union[ZeroOrMorePath, OneOrMorePath, ZeroOrOnePath]): + triple = (self.focus_node, IRI(value=property_path.value), path_node_1) + self.tss_list.append(TriplesSameSubject.from_spo(*triple)) + self.tssp_list.append( + _tssp_for_pathmods(self.focus_node, IRI(value=property_path.value), path_node_1, property_path.operand) + ) + pp_i += 1 + + elif isinstance(property_path, SequencePath): + for j, path in enumerate(property_path.value): + if isinstance(path, Path): + if j == 0: + triple = (self.focus_node, IRI(value=path.value), path_node_1) + else: + triple = (path_node_1, IRI(value=path.value), path_node_2) + elif isinstance(path, InversePath): + if j == 0: + triple = (path_node_1, IRI(value=path.value), self.focus_node) + else: + triple = (path_node_2, IRI(value=path.value), path_node_1) + self.tss_list.append(TriplesSameSubject.from_spo(*triple)) + current_tssp.append(TriplesSameSubjectPath.from_spo(*triple)) + pp_i += len(property_path.value) + + if current_tssp: + tssp_list.append(current_tssp) + + return pp_i + + +def _tssp_for_pathmods(focus_node, pred, obj, pathmod): + """ + Creates path modifier TriplesSameSubjectPath objects. + """ + if isinstance(focus_node, IRI): + focus_node = GraphTerm(value=focus_node) + return TriplesSameSubjectPath( + content=( + VarOrTerm(varorterm=focus_node), + PropertyListPathNotEmpty( + first_pair=( + VerbPath( + path=SG_Path( + path_alternative=PathAlternative( + sequence_paths=[PathSequence( + list_path_elt_or_inverse=[ + PathEltOrInverse( + path_elt=PathElt( + path_primary=PathPrimary( + value=pred, + ), + path_mod=PathMod( + pathmod=pathmod + ) + ) + ) + ] + ) + ] + ) + ) + ), + ObjectListPath( + object_paths=[ + ObjectPath( + graph_node_path=GraphNodePath( + varorterm_or_triplesnodepath=VarOrTerm( + varorterm=obj + ) + ) + ) + ] + ), + ) + ), + ) + ) + class PropertyPath(BaseModel): class Config: @@ -511,23 +607,51 @@ class Config: uri: Optional[URIRef] = None + def __len__(self): + return 1 # Default length for all PropertyPath subclasses + class Path(PropertyPath): value: URIRef - def __len__(self): - return 1 - class SequencePath(PropertyPath): value: List[PropertyPath] def __len__(self): - return len(self.value) + return len(self.value) # Override to return the length of the sequence class InversePath(PropertyPath): value: URIRef + +class ZeroOrMorePath(PropertyPath): + value: URIRef + operand: str = "*" + + +class OneOrMorePath(PropertyPath): + value: URIRef + operand: str = "+" + + +class ZeroOrOnePath(PropertyPath): + value: URIRef + operand: str = "?" + + +class AlternativePath(PropertyPath): + value: List[PropertyPath] + def __len__(self): - return 1 + return len(self.value) + + +PRED_TO_PATH_CLASS: Dict[URIRef, Type[PropertyPath]] = { + SH.inversePath: InversePath, + SH.zeroOrMorePath: ZeroOrMorePath, + SH.oneOrMorePath: OneOrMorePath, + SH.zeroOrOnePath: ZeroOrOnePath, + SH.alternativePath: AlternativePath, +} diff --git a/test_data/bnode_depth-2-2.ttl b/test_data/bnode_depth-2-2.ttl index daf7b4e3..4d31dbf4 100644 --- a/test_data/bnode_depth-2-2.ttl +++ b/test_data/bnode_depth-2-2.ttl @@ -30,6 +30,10 @@ PREFIX xsd: ; skos:historyNote "The scheme for Australian physiographic units is derived from: Pain, C, Gregory, L, Wilson, P and McKenzie, N (2011), The physiographic regions of Australia – Explanatory notes 2011; Australian Collaborative Land Evaluation Program and National Committee on Soil and Terrain (dataset available at https://www.asris.csiro.au/themes/PhysioRegions.html). The Geological Survey of Western Australia's regolith classification system is available at https://dmpbookshop.eruditetechnologies.com.au/product/revised-classification-system-for-regolith-in-western-australia-and-the-recommended-approach-to-regolith-mapping.do (2024 update including physiographic units is in progress)."@en ; skos:prefLabel "Australian physiographic units"@en ; + prov:qualifiedDerivation + [ + prov:hadRole ; + ] ; prov:qualifiedAttribution [ prov:agent [ diff --git a/test_data/catprez.ttl b/test_data/catprez.ttl index 47f17372..ab8584b5 100644 --- a/test_data/catprez.ttl +++ b/test_data/catprez.ttl @@ -6,11 +6,11 @@ PREFIX rdfs: ex:CatalogOne a dcat:Catalog ; rdfs:label "Catalog One" ; - dcterms:hasPart ex:DCATResource ; + dcterms:hasPart ex:DCATDataset ; ex:property "Catalog property" ; . -ex:DCATResource a dcat:Resource ; +ex:DCATDataset a dcat:Dataset ; rdfs:label "DCAT Resource" ; dcterms:hasPart ex:RDFResource ; ex:property "DCAT Resource property" @@ -23,11 +23,11 @@ ex:RDFResource a rdf:Resource ; ex:CatalogTwo a dcat:Catalog ; rdfs:label "amazing catalog" ; - dcterms:hasPart ex:DCATResourceTwo ; + dcterms:hasPart ex:DCATDatasetTwo ; ex:property "complete" ; . -ex:DCATResourceTwo a dcat:Resource ; +ex:DCATDatasetTwo a dcat:Dataset ; rdfs:label "rightful" ; dcterms:hasPart ex:RDFResourceTwo ; ex:property "exposure" diff --git a/test_data/ogc_features.ttl b/test_data/ogc_features.ttl index 3416b0b3..eec80c5e 100644 --- a/test_data/ogc_features.ttl +++ b/test_data/ogc_features.ttl @@ -4,6 +4,10 @@ @prefix rdfs: . @prefix xsd: . @prefix ex: . +@prefix skos: . +@prefix sdo: . +@prefix sosa: . +@prefix void: . ex:DemoCatalog a dcat:Catalog ; dcterms:title "Demo Catalog" ; @@ -15,26 +19,59 @@ ex:DemoCatalog a dcat:Catalog ; ex:GeoDataset a dcat:Dataset ; dcterms:title "Geographic Dataset" ; dcterms:description "A dataset containing a feature collection of geographic features." ; - dcterms:hasPart ex:FeatureCollection ; ex:datasetTheme "Geography" ; dcterms:creator "Jane Doe" . ex:FeatureCollection a geo:FeatureCollection ; dcterms:description "A collection of geographic features representing points of interest." ; - geo:hasGeometry "POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))"^^geo:wktLiteral ; + geo:hasGeometry [ + geo:asWKT "POLYGON((0 0, 0 10, 10 10, 10 0, 0 0))"^^geo:wktLiteral + ] ; rdfs:member ex:Feature1, ex:Feature2 ; - ex:featureCount 2 . + ex:featureCount 2 ; + void:inDataset ex:GeoDataset . ex:Feature1 a geo:Feature ; rdfs:label "Point of Interest 1" ; + skos:prefLabel "POI 1" ; dcterms:description "A notable location within the feature collection." ; - geo:hasGeometry "POINT(5 5)"^^geo:wktLiteral ; + geo:hasGeometry [ + geo:asWKT "POINT(5 5)"^^geo:wktLiteral + ] ; ex:category "Landmark" ; - ex:visitorCount 1000 . + ex:visitorCount 1000 ; + sdo:additionalProperty [ + a sdo:PropertyValue ; + sdo:propertyID "height" ; + sdo:value "100"^^xsd:integer + ] ; + sosa:isFeatureOfInterestOf ex:Observation1 . ex:Feature2 a geo:Feature ; rdfs:label "Point of Interest 2" ; dcterms:description "Another notable location within the feature collection." ; - geo:hasGeometry "POINT(7 3)"^^geo:wktLiteral ; + geo:hasGeometry [ + geo:asWKT "POINT(7 3)"^^geo:wktLiteral + ] ; + sdo:spatial [ + geo:hasGeometry [ + geo:asWKT "POLYGON((6 2, 6 4, 8 4, 8 2, 6 2))"^^geo:wktLiteral + ] + ] ; ex:category "Historical Site" ; - ex:yearEstablished 1850 . \ No newline at end of file + ex:yearEstablished 1850 ; + sdo:additionalProperty [ + a sdo:PropertyValue ; + sdo:propertyID "age" ; + sdo:value "174"^^xsd:integer + ] . + +ex:Observation1 a sosa:Observation ; + sosa:hasFeatureOfInterest ex:Feature1 ; + sosa:observedProperty ex:Temperature ; + sosa:hasResult [ + a sdo:PropertyValue ; + sdo:value "25.5"^^xsd:decimal ; + sdo:unitCode "CEL" + ] ; + sosa:resultTime "2024-09-02T12:00:00Z"^^xsd:dateTime . \ No newline at end of file diff --git a/test_data/vocprez.ttl b/test_data/vocprez.ttl index b8de2b10..d0a4cdef 100644 --- a/test_data/vocprez.ttl +++ b/test_data/vocprez.ttl @@ -6,7 +6,7 @@ PREFIX skos: ex:VocPrezCatalog a dcat:Catalog ; rdfs:label "A Demo Catalog" ; - dcterms:hasPart ex:SchemingConceptScheme ; + dcterms:hasPart ex:SchemingConceptScheme , ; ex:property "cataract" ; . diff --git a/tests/data/prefixes/data_using_prefixes.ttl b/tests/data/prefixes/data_using_prefixes.ttl index 0d43b5d0..270d1040 100644 --- a/tests/data/prefixes/data_using_prefixes.ttl +++ b/tests/data/prefixes/data_using_prefixes.ttl @@ -6,6 +6,6 @@ PREFIX rdfs: a dcat:Catalog ; rdfs:label "A Catalog with prefixed david" ; - dcterms:hasPart ex:DCATResource ; + dcterms:hasPart ex:DCATDataset ; ex:property "some property" ; . \ No newline at end of file diff --git a/tests/test_endpoints_catprez.py b/tests/test_endpoints_catprez.py index 6ec76a23..79c06d91 100755 --- a/tests/test_endpoints_catprez.py +++ b/tests/test_endpoints_catprez.py @@ -34,8 +34,8 @@ def test_lower_level_listing_anot(client, a_catprez_catalog_link): r = client.get(f"{a_catprez_catalog_link}/collections?_mediatype=text/turtle") response_graph = Graph().parse(data=r.text) expected_response = ( - URIRef("https://example.com/DCATResource"), + URIRef("https://example.com/DCATDataset"), RDF.type, - DCAT.Resource, + DCAT.Dataset, ) assert next(response_graph.triples(expected_response)) diff --git a/tests/test_node_selection_shacl.py b/tests/test_node_selection_shacl.py index 4a520127..8b2c70c0 100755 --- a/tests/test_node_selection_shacl.py +++ b/tests/test_node_selection_shacl.py @@ -22,7 +22,6 @@ def test_nodeshape_parsing(nodeshape_uri): assert ns.targetClasses == [ URIRef("http://www.w3.org/2004/02/skos/core#ConceptScheme"), URIRef("http://www.w3.org/2004/02/skos/core#Collection"), - URIRef("http://www.w3.org/ns/dcat#Resource"), URIRef("http://www.w3.org/ns/dcat#Dataset"), ] assert len(ns.propertyShapesURIs) == 1 diff --git a/tests/test_property_selection_shacl.py b/tests/test_property_selection_shacl.py index 6a0e0576..85d009b7 100755 --- a/tests/test_property_selection_shacl.py +++ b/tests/test_property_selection_shacl.py @@ -1,3 +1,4 @@ +import pytest from rdflib import Graph, URIRef, SH, RDF, PROV, DCTERMS from prez.reference_data.prez_ns import REG @@ -7,25 +8,10 @@ IRI, OptionalGraphPattern, Filter, - TriplesSameSubjectPath, + TriplesSameSubjectPath, TriplesSameSubject, ) -# uri: URIRef | BNode # URI of the shape -# graph: Graph -# focus_node: IRI | Var = Var(value="focus_node") -# # inputs -# property_paths: Optional[List[PropertyPath]] = None -# or_klasses: Optional[List[URIRef]] = None -# # outputs -# grammar: Optional[GroupGraphPatternSub] = None -# tssp_list: Optional[List[SimplifiedTriple]] = None -# gpnt_list: Optional[List[GraphPatternNotTriples]] = None -# prof_nodes: Optional[Dict[str, Var | IRI]] = {} -# classes_at_len: Optional[Dict[str, List[URIRef]]] = {} -# _select_vars: Optional[List[Var]] = None - - def test_simple_path(): g = Graph().parse( data=""" @@ -107,52 +93,52 @@ def test_union(): uri=path_bn, graph=g, kind="profile", focus_node=Var(value="focus_node") ) assert ( - TriplesSameSubjectPath.from_spo( + TriplesSameSubject.from_spo( subject=Var(value="focus_node"), predicate=IRI(value=PROV.qualifiedDerivation), - object=Var(value="prof_1_node_1"), + object=Var(value="prof_1_node_3"), ) - in ps.tssp_list + in ps.tss_list ) assert ( - TriplesSameSubjectPath.from_spo( - subject=Var(value="prof_1_node_1"), + TriplesSameSubject.from_spo( + subject=Var(value="prof_1_node_3"), predicate=IRI(value=PROV.hadRole), - object=Var(value="prof_1_node_2"), + object=Var(value="prof_1_node_4"), ) - in ps.tssp_list + in ps.tss_list ) assert ( - TriplesSameSubjectPath.from_spo( + TriplesSameSubject.from_spo( subject=Var(value="focus_node"), predicate=IRI(value=PROV.qualifiedDerivation), - object=Var(value="prof_1_node_3"), + object=Var(value="prof_1_node_5"), ) - in ps.tssp_list + in ps.tss_list ) assert ( - TriplesSameSubjectPath.from_spo( - subject=Var(value="prof_1_node_3"), + TriplesSameSubject.from_spo( + subject=Var(value="prof_1_node_5"), predicate=IRI(value=PROV.entity), - object=Var(value="prof_1_node_4"), + object=Var(value="prof_1_node_6"), ) - in ps.tssp_list + in ps.tss_list ) assert ( - TriplesSameSubjectPath.from_spo( + TriplesSameSubject.from_spo( subject=Var(value="focus_node"), predicate=IRI(value=DCTERMS.publisher), - object=Var(value="prof_1_node_5"), + object=Var(value="prof_1_node_1"), ) - in ps.tssp_list + in ps.tss_list ) assert ( - TriplesSameSubjectPath.from_spo( + TriplesSameSubject.from_spo( subject=Var(value="focus_node"), predicate=IRI(value=REG.status), - object=Var(value="prof_1_node_6"), + object=Var(value="prof_1_node_2"), ) - in ps.tssp_list + in ps.tss_list ) @@ -189,12 +175,7 @@ def test_complex_optional_props(): sh:property [ sh:minCount 0 ; - sh:path ( - sh:union ( - dcterms:publisher - ( prov:qualifiedDerivation prov:hadRole ) - ) - ) + sh:path dcterms:publisher , ( prov:qualifiedDerivation prov:hadRole ) ] . @@ -218,12 +199,7 @@ def test_excluded_props(): sh:property [ sh:maxCount 0 ; - sh:path ( - sh:union ( - dcterms:publisher - reg:status - ) - ) + sh:path dcterms:publisher , reg:status ] . @@ -242,3 +218,31 @@ def test_excluded_props(): in ps.tssp_list ) assert isinstance(ps.gpnt_list[0].content, Filter) + + +@pytest.mark.parametrize( + ["cardinality_type", "expected_result"], + [ + ("sh:zeroOrMorePath", '?focus_node * ?prof_1_node_1'), + ("sh:oneOrMorePath", '?focus_node + ?prof_1_node_1'), + ("sh:zeroOrOnePath", '?focus_node ? ?prof_1_node_1'), + ], +) +def test_cardinality_props(cardinality_type, expected_result): + g = Graph().parse( + data=f""" + PREFIX dcterms: + PREFIX sh: + + sh:property [ + sh:path [ {cardinality_type} dcterms:publisher ] ; + ] + . + + """ + ) + path_bn = g.value(subject=URIRef("http://example-profile"), predicate=SH.property) + ps = PropertyShape( + uri=path_bn, graph=g, kind="profile", focus_node=Var(value="focus_node") + ) + assert ps.tssp_list[0].to_string() == expected_result