From 7a592ea60f9734adaa4841cb03b68357864e6f4f Mon Sep 17 00:00:00 2001 From: david Date: Sun, 25 Jun 2023 22:46:39 +1000 Subject: [PATCH 1/3] Object endpoint returns system links. Add endpoint reference data to support system link generation. Rename inbound/outbound children/parents to focus to/from child/parent terminology to align with SHACL. Rename general_class to base_class. Resolves #101 --- README-Dev.md | 4 +- dev/dev-config.ttl | 2 +- prez/app.py | 10 +- prez/cache.py | 3 + prez/config.py | 10 +- prez/models/catprez_item.py | 13 +- prez/models/catprez_listings.py | 4 +- prez/models/object_item.py | 53 +++++ prez/models/profiles_item.py | 2 +- prez/models/profiles_listings.py | 10 +- prez/models/spaceprez_item.py | 22 +- prez/models/spaceprez_listings.py | 8 +- prez/models/vocprez_item.py | 16 +- prez/models/vocprez_listings.py | 8 +- .../endpoints/catprez_endpoints.ttl | 25 ++ .../endpoints/spaceprez_endpoints.ttl | 43 ++++ .../endpoints/vocprez_endpoints.ttl | 59 +++++ .../profiles/catprez_default_profiles.ttl | 2 +- .../profiles/spaceprez_default_profiles.ttl | 4 +- .../profiles/vocprez_default_profiles.ttl | 8 +- prez/renderers/renderer.py | 30 +-- prez/routers/catprez.py | 21 +- prez/routers/object.py | 125 +++++++--- prez/routers/search.py | 4 +- prez/routers/spaceprez.py | 20 +- prez/routers/vocprez.py | 59 ++++- prez/services/app_service.py | 11 +- prez/services/generate_profiles.py | 6 +- prez/services/model_methods.py | 20 +- prez/services/search_methods.py | 4 +- prez/sparql/methods.py | 55 +++-- prez/sparql/objects_listings.py | 213 +++++++++++------- .../dataset_listing_anot.ttl | 5 +- .../data/spaceprez/input/multiple_object.ttl | 11 + tests/sparql/test_sparql_new.py | 4 +- 35 files changed, 644 insertions(+), 250 deletions(-) create mode 100644 prez/models/object_item.py create mode 100644 prez/reference_data/endpoints/catprez_endpoints.ttl create mode 100644 prez/reference_data/endpoints/spaceprez_endpoints.ttl create mode 100644 prez/reference_data/endpoints/vocprez_endpoints.ttl diff --git a/README-Dev.md b/README-Dev.md index 046d4b9f..f9a42bb0 100644 --- a/README-Dev.md +++ b/README-Dev.md @@ -287,8 +287,8 @@ SELECT ?profile ?title ?class (count(?mid) as ?distance) ?req_profile ?def_profi WHERE { VALUES ?class {} ?class rdfs:subClassOf* ?mid . - ?mid rdfs:subClassOf* ?general_class . - VALUES ?general_class { dcat:Dataset geo:FeatureCollection prez:FeatureCollectionList prez:FeatureList geo:Feature + ?mid rdfs:subClassOf* ?base_class . + VALUES ?base_class { dcat:Dataset geo:FeatureCollection prez:FeatureCollectionList prez:FeatureList geo:Feature skos:ConceptScheme skos:Concept skos:Collection prez:DatasetList prez:VocPrezCollectionList prez:SchemesList prez:CatalogList dcat:Catalog dcat:Resource } ?profile altr-ext:constrainsClass ?class ; diff --git a/dev/dev-config.ttl b/dev/dev-config.ttl index f1b9a28e..a1f26912 100644 --- a/dev/dev-config.ttl +++ b/dev/dev-config.ttl @@ -61,7 +61,7 @@ ja:DatasetRDFS rdfs:subClassOf ja:RDFDataset . ] ; fuseki:endpoint [ fuseki:operation fuseki:gsp-rw ] ; fuseki:endpoint [ fuseki:operation fuseki:query ] ; - fuseki:name "sp" . + fuseki:name "myds" . :dataset_readwrite rdf:type tdb2:DatasetTDB2 ; diff --git a/prez/app.py b/prez/app.py index 6736f489..1a453368 100644 --- a/prez/app.py +++ b/prez/app.py @@ -25,8 +25,13 @@ from prez.routers.spaceprez import router as spaceprez_router from prez.routers.sparql import router as sparql_router from prez.routers.vocprez import router as vocprez_router -from prez.services.app_service import healthcheck_sparql_endpoints, count_objects -from prez.services.app_service import populate_api_info, add_prefixes_to_prefix_graph +from prez.services.app_service import ( + healthcheck_sparql_endpoints, + count_objects, + create_endpoints_graph, + populate_api_info, + add_prefixes_to_prefix_graph, +) from prez.services.exception_catchers import ( catch_400, catch_404, @@ -107,6 +112,7 @@ async def app_startup(): await healthcheck_sparql_endpoints() await get_all_search_methods() await create_profiles_graph() + await create_endpoints_graph() await count_objects() await populate_api_info() await add_prefixes_to_prefix_graph() diff --git a/prez/cache.py b/prez/cache.py index e26200a1..d79fd538 100644 --- a/prez/cache.py +++ b/prez/cache.py @@ -5,6 +5,9 @@ profiles_graph_cache = ConjunctiveGraph() profiles_graph_cache.bind("prez", "https://prez.dev/") +endpoints_graph_cache = ConjunctiveGraph() +profiles_graph_cache.bind("prez", "https://prez.dev/") + prez_system_graph = Graph() prez_system_graph.bind("prez", "https://prez.dev/") diff --git a/prez/config.py b/prez/config.py index 665663c0..85323950 100644 --- a/prez/config.py +++ b/prez/config.py @@ -20,7 +20,7 @@ class Settings(BaseSettings): system_uri: Documentation property. An IRI for the Prez system as a whole. This value appears in the landing page RDF delivered by Prez ('/') top_level_classes: collection_classes: - general_classes: + base_classes: log_level: log_output: cql_props: dict = { @@ -51,7 +51,7 @@ class Settings(BaseSettings): system_uri: Optional[str] top_level_classes: Optional[dict] collection_classes: Optional[dict] - general_classes: Optional[dict] + base_classes: Optional[dict] prez_flavours: Optional[list] = ["SpacePrez", "VocPrez", "CatPrez", "ProfilesPrez"] log_level = "INFO" log_output = "stdout" @@ -128,16 +128,16 @@ def populate_collection_classes(cls, values): return values @root_validator() - def populate_general_classes(cls, values): + def populate_base_classes(cls, values): additional_classes = { "SpacePrez": [GEO.Feature], "VocPrez": [SKOS.Concept], "CatPrez": [DCAT.Dataset], "Profiles": [PROF.Profile], } - values["general_classes"] = {} + values["base_classes"] = {} for prez in list(additional_classes.keys()) + ["Profiles"]: - values["general_classes"][prez] = ( + values["base_classes"][prez] = ( values["collection_classes"].get(prez) + additional_classes[prez] ) return values diff --git a/prez/models/catprez_item.py b/prez/models/catprez_item.py index 3b3860ab..b40814e0 100644 --- a/prez/models/catprez_item.py +++ b/prez/models/catprez_item.py @@ -12,9 +12,10 @@ class CatalogItem(BaseModel): uri: Optional[URIRef] = None + endpoint_uri: Optional[str] classes: Optional[Set[URIRef]] curie_id: Optional[str] = None - general_class: Optional[URIRef] = None + base_class: Optional[URIRef] = None catalog_curie: Optional[str] = None resource_curie: Optional[str] = None url_path: Optional[str] = None @@ -34,19 +35,19 @@ def populate(cls, values): uri = values.get("uri") curie_id = values.get("curie_id") url_parts = url_path.split("/") - if url_path in ["/object", "/c/object"]: - values["link_constructor"] = f"/c/object?uri=" + endpoint_uri = values.get("endpoint_uri") + values["endpoint_uri"] = URIRef(endpoint_uri) if len(url_parts) == 4: - values["general_class"] = DCAT.Catalog + values["base_class"] = DCAT.Catalog curie_id = values.get("catalog_curie") values["link_constructor"] = f"/c/catalogs/{curie_id}" elif len(url_parts) == 5: - values["general_class"] = DCAT.Resource + values["base_class"] = DCAT.Resource curie_id = values.get("resource_curie") assert curie_id or uri, "Either an curie_id or uri must be provided" if curie_id: # get the URI values["uri"] = get_uri_for_curie_id(curie_id) else: # uri provided, get the curie_id values["curie_id"] = get_curie_id_for_uri(uri) - values["classes"] = get_classes(values["uri"]) + values["classes"] = get_classes(values["uri"], values["endpoint_uri"]) return values diff --git a/prez/models/catprez_listings.py b/prez/models/catprez_listings.py index ff2a2e77..11511b13 100644 --- a/prez/models/catprez_listings.py +++ b/prez/models/catprez_listings.py @@ -10,7 +10,7 @@ class CatalogMembers(BaseModel): url_path: str uri: Optional[URIRef] = None - general_class: Optional[URIRef] + base_class: Optional[URIRef] classes: Optional[FrozenSet[URIRef]] selected_class: Optional[URIRef] = None link_constructor: Optional[str] @@ -22,7 +22,7 @@ def populate(cls, values): if url_path in ["/object", "/c/object"]: values["link_constructor"] = f"/c/object?uri=" if url_path == "/c/catalogs": - values["general_class"] = DCAT.Catalog + values["base_class"] = DCAT.Catalog values["link_constructor"] = "/c/catalogs" values["classes"] = frozenset([PREZ.CatalogList]) return values diff --git a/prez/models/object_item.py b/prez/models/object_item.py new file mode 100644 index 00000000..6190884d --- /dev/null +++ b/prez/models/object_item.py @@ -0,0 +1,53 @@ +from typing import Optional +from typing import Set + +from pydantic import BaseModel, root_validator +from rdflib import URIRef, PROF + +from prez.models.model_exceptions import ClassNotFoundException +from prez.services.model_methods import get_classes + + +class ObjectItem(BaseModel): + uri: Optional[URIRef] = None + classes: Optional[Set[URIRef]] = frozenset([PROF.Profile]) + selected_class: Optional[URIRef] = None + + def __hash__(self): + return hash(self.uri) + + @root_validator + def populate(cls, values): + try: + values["classes"] = get_classes(values["uri"]) + except ClassNotFoundException: + # TODO return a generic DESCRIBE on the object - we can't use any of prez's profiles/endpoints to render + # information about the object, but we can provide any RDF we have for it. + pass + uri_str = values["uri"] + values["uri"] = URIRef(uri_str) + return values + + # get_parents_query = + # object_query = + + # values + # + # assert uri or id + # if id: + # values["uri"] = get_uri_for_curie_id(id) + # elif uri: + # values["id"] = get_curie_id_for_uri(uri) + # q = f"""SELECT ?class {{ <{values["uri"]}> a ?class }}""" + # r = profiles_graph_cache.query(q) + # if len(r.bindings) > 0: + # values["classes"] = frozenset([prof.get("class") for prof in r.bindings]) + # return values + + # get classes from remote endpoint + # get endpoints which deliver classes & endpoint templates & parent relations for endpoints (from local prez graph) + # in parallel: + # get parent uris from remote endpoint + # get object information using open profile from remote endpoint + # construct the system links using the parent uris from the remote endpoint. + # merge the response with the system links diff --git a/prez/models/profiles_item.py b/prez/models/profiles_item.py index b2462ec0..bb2ee90e 100644 --- a/prez/models/profiles_item.py +++ b/prez/models/profiles_item.py @@ -17,7 +17,7 @@ class ProfileItem(BaseModel): id: Optional[str] = None link_constructor: str = "/profiles" - # general_class: Optional[URIRef] = None + # base_class: Optional[URIRef] = None # url_path: Optional[str] = None selected_class: Optional[URIRef] = None diff --git a/prez/models/profiles_listings.py b/prez/models/profiles_listings.py index 6e0dfad4..0472f4f9 100644 --- a/prez/models/profiles_listings.py +++ b/prez/models/profiles_listings.py @@ -10,7 +10,7 @@ class ProfilesMembers(BaseModel): url_path: str uri: Optional[URIRef] = None - general_class: Optional[URIRef] + base_class: Optional[URIRef] classes: Optional[FrozenSet[URIRef]] = frozenset([PREZ.ProfilesList]) selected_class: Optional[URIRef] = None link_constructor: Optional[str] @@ -20,15 +20,15 @@ class ProfilesMembers(BaseModel): def populate(cls, values): url_path = values.get("url_path") if url_path.startswith("/v/"): - values["general_class"] = PREZ.VocPrezProfile + values["base_class"] = PREZ.VocPrezProfile values["link_constructor"] = "/v/profiles" elif url_path.startswith("/c/"): - values["general_class"] = PREZ.CatPrezProfile + values["base_class"] = PREZ.CatPrezProfile values["link_constructor"] = "/c/profiles" elif url_path.startswith("/s/"): - values["general_class"] = PREZ.SpacePrezProfile + values["base_class"] = PREZ.SpacePrezProfile values["link_constructor"] = "/s/profiles" else: - values["general_class"] = PROF.Profile + values["base_class"] = PROF.Profile values["link_constructor"] = "/profiles" return values diff --git a/prez/models/spaceprez_item.py b/prez/models/spaceprez_item.py index 7731a2cc..b3c2dd4e 100644 --- a/prez/models/spaceprez_item.py +++ b/prez/models/spaceprez_item.py @@ -16,12 +16,11 @@ class SpatialItem(BaseModel): id: Optional[str] uri: Optional[URIRef] url_path: Optional[str] - general_class: Optional[URIRef] + endpoint_uri: Optional[str] + base_class: Optional[URIRef] feature_curie: Optional[str] collection_curie: Optional[str] dataset_curie: Optional[str] - parent_curie: Optional[str] - parent_uri: Optional[URIRef] classes: Optional[Set[URIRef]] link_constructor: Optional[str] selected_class: Optional[URIRef] = None @@ -38,28 +37,23 @@ def populate(cls, values): dataset_curie = values.get("dataset_curie") collection_curie = values.get("collection_curie") feature_curie = values.get("feature_curie") - url_path = values.get("url_path") - if url_path in ["/object", "/s/object"]: - values["link_constructor"] = f"/s/object?uri=" + endpoint_uri = values.get("endpoint_uri") + values["endpoint_uri"] = URIRef(endpoint_uri) if feature_curie: values["id"] = feature_curie values["uri"] = get_uri_for_curie_id(feature_curie) - values["general_class"] = GEO.Feature - values["parent_uri"] = get_uri_for_curie_id(collection_curie) - values["parent_curie"] = collection_curie + values["base_class"] = GEO.Feature elif collection_curie: values["id"] = collection_curie values["uri"] = get_uri_for_curie_id(collection_curie) - values["general_class"] = GEO.FeatureCollection - values["parent_uri"] = get_uri_for_curie_id(dataset_curie) - values["parent_curie"] = dataset_curie + values["base_class"] = GEO.FeatureCollection values[ "link_constructor" ] = f"/s/datasets/{dataset_curie}/collections/{collection_curie}/items" elif dataset_curie: values["id"] = dataset_curie values["uri"] = get_uri_for_curie_id(dataset_curie) - values["general_class"] = DCAT.Dataset + values["base_class"] = DCAT.Dataset values["link_constructor"] = f"/s/datasets/{dataset_curie}/collections" - values["classes"] = get_classes(values["uri"]) + values["classes"] = get_classes(values["uri"], values["endpoint_uri"]) return values diff --git a/prez/models/spaceprez_listings.py b/prez/models/spaceprez_listings.py index fc53cc6d..dccfa3c2 100644 --- a/prez/models/spaceprez_listings.py +++ b/prez/models/spaceprez_listings.py @@ -14,7 +14,7 @@ class SpatialMembers(BaseModel): parent_uri: Optional[URIRef] = None dataset_curie: Optional[URIRef] collection_curie: Optional[URIRef] - general_class: Optional[URIRef] + base_class: Optional[URIRef] classes: Optional[FrozenSet[URIRef]] selected_class: Optional[FrozenSet[URIRef]] = None top_level_listing: Optional[bool] = False @@ -24,7 +24,7 @@ class SpatialMembers(BaseModel): def populate(cls, values): url_path = values["url_path"] if url_path.endswith("/datasets"): # /s/datasets - values["general_class"] = DCAT.Dataset + values["base_class"] = DCAT.Dataset values["link_constructor"] = "/s/datasets" values["classes"] = frozenset([PREZ.DatasetList]) # graph @@ -34,7 +34,7 @@ def populate(cls, values): "/collections" ): # /s/datasets/{dataset_curie}/collections dataset_curie = values.get("dataset_curie") - values["general_class"] = GEO.FeatureCollection + values["base_class"] = GEO.FeatureCollection values["link_constructor"] = f"/s/datasets/{dataset_curie}/collections" values["classes"] = frozenset([PREZ.FeatureCollectionList]) values["uri"] = get_uri_for_curie_id(dataset_curie) @@ -43,7 +43,7 @@ def populate(cls, values): ): # /s/datasets/{dataset_curie}/collections/{collection_curie}/items dataset_curie = values.get("dataset_curie") collection_curie = values.get("collection_curie") - values["general_class"] = GEO.Feature + values["base_class"] = GEO.Feature values[ "link_constructor" ] = f"/s/datasets/{dataset_curie}/collections/{collection_curie}/items" diff --git a/prez/models/vocprez_item.py b/prez/models/vocprez_item.py index e0f182bc..d89ea044 100644 --- a/prez/models/vocprez_item.py +++ b/prez/models/vocprez_item.py @@ -12,11 +12,12 @@ class VocabItem(BaseModel): uri: Optional[URIRef] = None classes: Optional[Set[URIRef]] curie_id: Optional[str] = None - general_class: Optional[URIRef] = None + base_class: Optional[URIRef] = None scheme_curie: Optional[str] = None collection_curie: Optional[str] = None concept_curie: Optional[str] = None url_path: Optional[str] = None + endpoint_uri: Optional[str] selected_class: Optional[URIRef] = None top_level_listing: Optional[bool] = False @@ -29,17 +30,16 @@ def __hash__(self): @root_validator def populate(cls, values): url_path = values.get("url_path") - uri = values.get("uri") concept_curie = values.get("concept_curie") scheme_curie = values.get("scheme_curie") collection_curie = values.get("collection_curie") url_parts = url_path.split("/") + endpoint_uri = values.get("endpoint_uri") + values["endpoint_uri"] = URIRef(endpoint_uri) if url_path == "/v": return values - if url_path in ["/object", "/v/object"]: - values["link_constructor"] = f"/v/object?uri=" elif len(url_parts) == 5: # concepts - values["general_class"] = SKOS.Concept + values["base_class"] = SKOS.Concept if scheme_curie: values["curie_id"] = concept_curie values["link_constructor"] = f"/v/vocab/{scheme_curie}" @@ -49,14 +49,14 @@ def populate(cls, values): values["link_constructor"] = f"/v/collection/{collection_curie}" elif url_parts[2] == "collection": # collections values["curie_id"] = values.get("collection_curie") - values["general_class"] = SKOS.Collection + values["base_class"] = SKOS.Collection values["link_constructor"] = f"/v/collection/{collection_curie}" elif url_parts[2] in ["scheme", "vocab"]: # vocabularies - values["general_class"] = SKOS.ConceptScheme + values["base_class"] = SKOS.ConceptScheme values["curie_id"] = values.get("scheme_curie") values["link_constructor"] = f"/v/vocab/{scheme_curie}" if not values["uri"]: values["uri"] = get_uri_for_curie_id(values["curie_id"]) - values["classes"] = get_classes(values["uri"]) + values["classes"] = get_classes(values["uri"], values["endpoint_uri"]) return values diff --git a/prez/models/vocprez_listings.py b/prez/models/vocprez_listings.py index f6e87e0a..2bf2ff3c 100644 --- a/prez/models/vocprez_listings.py +++ b/prez/models/vocprez_listings.py @@ -10,7 +10,7 @@ class VocabMembers(BaseModel): url_path: str uri: Optional[URIRef] = None - general_class: Optional[URIRef] + base_class: Optional[URIRef] classes: Optional[FrozenSet[URIRef]] selected_class: Optional[URIRef] = None link_constructor: Optional[str] @@ -20,15 +20,15 @@ class VocabMembers(BaseModel): def populate(cls, values): url_path = values.get("url_path") if url_path == "/v/collection": - values["general_class"] = SKOS.Collection + values["base_class"] = SKOS.Collection values["link_constructor"] = "/v/collection" values["classes"] = frozenset([PREZ.VocPrezCollectionList]) elif url_path == "/v/scheme": - values["general_class"] = SKOS.ConceptScheme + values["base_class"] = SKOS.ConceptScheme values["link_constructor"] = "/v/scheme" values["classes"] = frozenset([PREZ.SchemesList]) elif url_path == "/v/vocab": - values["general_class"] = SKOS.ConceptScheme + values["base_class"] = SKOS.ConceptScheme values["link_constructor"] = "/v/vocab" values["classes"] = frozenset([PREZ.SchemesList]) return values diff --git a/prez/reference_data/endpoints/catprez_endpoints.ttl b/prez/reference_data/endpoints/catprez_endpoints.ttl new file mode 100644 index 00000000..f38be76e --- /dev/null +++ b/prez/reference_data/endpoints/catprez_endpoints.ttl @@ -0,0 +1,25 @@ +PREFIX dcat: +PREFIX dcterms: +PREFIX endpoint: +PREFIX ont: +PREFIX prez: +PREFIX rdfs: +PREFIX skos: + +endpoint:catalog-listing a ont:Endpoint ; + ont:deliversClasses prez:CatalogList ; + ont:endpointTemplate "/c/catalogs" ; +. + +endpoint:catalog a ont:Endpoint ; + ont:parentEndpoint endpoint:catalog-listing ; + ont:deliversClasses dcat:Catalog ; + ont:endpointTemplate "/c/catalogs/$object" ; +. + +endpoint:resource a ont:Endpoint ; + ont:parentEndpoint endpoint:catalog ; + ont:deliversClasses dcat:Resource ; + ont:endpointTemplate "/c/catalogs/$parent_1/$object" ; + ont:ParentToFocusRelation dcterms:hasPart ; +. diff --git a/prez/reference_data/endpoints/spaceprez_endpoints.ttl b/prez/reference_data/endpoints/spaceprez_endpoints.ttl new file mode 100644 index 00000000..c3fbdee5 --- /dev/null +++ b/prez/reference_data/endpoints/spaceprez_endpoints.ttl @@ -0,0 +1,43 @@ +PREFIX dcat: +PREFIX endpoint: +PREFIX geo: +PREFIX ont: +PREFIX prez: +PREFIX rdfs: + +endpoint:dataset-listing a ont:Endpoint ; + ont:deliversClasses prez:DatasetList ; + ont:endpointTemplate "/s/datasets" ; +. + +endpoint:dataset a ont:Endpoint ; + ont:parentEndpoint endpoint:dataset-listing ; + ont:deliversClasses dcat:Dataset ; + ont:endpointTemplate "/s/datasets/$object" ; +. + +endpoint:feature-collection-listing a ont:Endpoint ; + ont:parentEndpoint endpoint:dataset ; + ont:deliversClasses prez:FeatureCollectionList ; + ont:endpointTemplate "/s/datasets/$parent_1/collections" ; +. + +endpoint:feature-collection a ont:Endpoint ; + ont:parentEndpoint endpoint:feature-collection-listing ; + ont:deliversClasses geo:FeatureCollection ; + ont:endpointTemplate "/s/datasets/$parent_1/collections/$object" ; + ont:ParentToFocusRelation rdfs:member ; +. + +endpoint:feature-listing a ont:Endpoint ; + ont:parentEndpoint endpoint:feature-collection ; + ont:deliversClasses prez:FeatureList ; + ont:endpointTemplate "/s/datasets/$parent_2/collections/$parent_1/items" ; +. + +endpoint:feature a ont:Endpoint ; + ont:parentEndpoint endpoint:feature-listing ; + ont:deliversClasses geo:Feature ; + ont:endpointTemplate "/s/datasets/$parent_2/collections/$parent_1/items/$object" ; + ont:ParentToFocusRelation rdfs:member ; +. diff --git a/prez/reference_data/endpoints/vocprez_endpoints.ttl b/prez/reference_data/endpoints/vocprez_endpoints.ttl new file mode 100644 index 00000000..5b912085 --- /dev/null +++ b/prez/reference_data/endpoints/vocprez_endpoints.ttl @@ -0,0 +1,59 @@ +PREFIX endpoint: +PREFIX ont: +PREFIX prez: +PREFIX rdfs: +PREFIX skos: + +endpoint:collection-listing a ont:Endpoint ; + ont:deliversClasses prez:VocPrezCollectionList ; + ont:endpointTemplate "/v/collection" ; +. + +endpoint:collection a ont:Endpoint ; + ont:parentEndpoint endpoint:collection ; + ont:deliversClasses skos:Collection ; + ont:endpointTemplate "/v/collection/$object" ; +. + +endpoint:collection-concept a ont:Endpoint ; + ont:parentEndpoint endpoint:collection ; + ont:deliversClasses skos:Concept ; + ont:endpointTemplate "/v/collection/$parent_1/$object" ; + ont:ParentToFocusRelation skos:member ; +. + +endpoint:schemes-listing a ont:Endpoint ; + ont:deliversClasses prez:SchemesList ; + ont:endpointTemplate "/v/scheme" ; +. + + endpoint:vocabs-listing a ont:Endpoint ; + ont:deliversClasses prez:SchemesList ; + ont:endpointTemplate "/v/vocab" ; +. + +endpoint:vocab a ont:Endpoint ; + ont:parentEndpoint endpoint:vocabs-listing ; + ont:deliversClasses skos:ConceptScheme ; + ont:endpointTemplate "/v/vocab/$object" ; +. + +endpoint:scheme a ont:Endpoint ; + ont:parentEndpoint endpoint:schemes-listing ; + ont:deliversClasses skos:ConceptScheme ; + ont:endpointTemplate "/v/scheme/$object" ; +. + +endpoint:vocab-concept a ont:Endpoint ; + ont:parentEndpoint endpoint:vocab ; + ont:deliversClasses skos:Concept ; + ont:endpointTemplate "/v/vocab/$parent_1/$object" ; + ont:FocusToParentRelation skos:inScheme ; +. + +endpoint:scheme-concept a ont:Endpoint ; + ont:parentEndpoint endpoint:scheme ; + ont:deliversClasses skos:Concept ; + ont:endpointTemplate "/v/scheme/$parent_1/$object" ; + ont:FocusToParentRelation skos:inScheme ; +. diff --git a/prez/reference_data/profiles/catprez_default_profiles.ttl b/prez/reference_data/profiles/catprez_default_profiles.ttl index 21bcd1b6..732fe7b6 100644 --- a/prez/reference_data/profiles/catprez_default_profiles.ttl +++ b/prez/reference_data/profiles/catprez_default_profiles.ttl @@ -55,7 +55,7 @@ prez:CatPrezProfile altr-ext:hasNodeShape [ a sh:NodeShape ; sh:targetClass dcat:Catalog ; - altr-ext:outboundChildren dcterms:hasPart ; + altr-ext:focusToChild dcterms:hasPart ; sh:sequencePath ( dcterms:hasPart dcterms:issued diff --git a/prez/reference_data/profiles/spaceprez_default_profiles.ttl b/prez/reference_data/profiles/spaceprez_default_profiles.ttl index cf660349..19550ed8 100644 --- a/prez/reference_data/profiles/spaceprez_default_profiles.ttl +++ b/prez/reference_data/profiles/spaceprez_default_profiles.ttl @@ -89,7 +89,7 @@ prez:SpacePrezProfile ] , [ a sh:NodeShape ; sh:targetClass geo:FeatureCollection , prez:FeatureCollectionList , prez:FeatureList ; - altr-ext:outboundChildren rdfs:member ; + altr-ext:focusToChild rdfs:member ; ] . @@ -126,6 +126,6 @@ prez:SpacePrezProfile altr-ext:hasNodeShape [ a sh:NodeShape ; sh:targetClass dcat:Dataset ; - altr-ext:outboundChildren rdfs:member ; + altr-ext:focusToChild rdfs:member ; ] . diff --git a/prez/reference_data/profiles/vocprez_default_profiles.ttl b/prez/reference_data/profiles/vocprez_default_profiles.ttl index e7adf7fa..4e756a4a 100644 --- a/prez/reference_data/profiles/vocprez_default_profiles.ttl +++ b/prez/reference_data/profiles/vocprez_default_profiles.ttl @@ -85,17 +85,17 @@ prez:VocPrezProfile altr-ext:hasNodeShape [ a sh:NodeShape ; sh:targetClass skos:ConceptScheme ; - altr-ext:outboundChildren skos:hasTopConcept ; + altr-ext:focusToChild skos:hasTopConcept ; ] ; altr-ext:hasNodeShape [ a sh:NodeShape ; sh:targetClass skos:Collection ; - altr-ext:outboundChildren skos:member ; + altr-ext:focusToChild skos:member ; ] ; altr-ext:hasNodeShape [ a sh:NodeShape ; sh:targetClass skos:ConceptScheme ; - altr-ext:inboundChildren skos:inScheme ; + altr-ext:childToFocus skos:inScheme ; ] ; altr-ext:hasNodeShape [ a sh:NodeShape ; @@ -105,7 +105,7 @@ prez:VocPrezProfile altr-ext:hasNodeShape [ a sh:NodeShape ; sh:targetClass skos:Concept ; - altr-ext:outboundParents skos:inScheme ; + altr-ext:focusToParent skos:inScheme ; ] ; altr-ext:hasNodeShape [ a sh:NodeShape ; diff --git a/prez/renderers/renderer.py b/prez/renderers/renderer.py index 56eb58a2..0ae525e8 100644 --- a/prez/renderers/renderer.py +++ b/prez/renderers/renderer.py @@ -12,7 +12,7 @@ from prez.models.profiles_and_mediatypes import ProfilesMediatypesInfo from prez.models.profiles_item import ProfileItem from prez.reference_data.prez_ns import PREZ -from prez.sparql.methods import queries_to_graph +from prez.sparql.methods import send_queries from prez.services.curie_functions import get_curie_id_for_uri from prez.sparql.objects_listings import ( generate_item_construct, @@ -34,7 +34,7 @@ async def return_from_queries( Executes SPARQL queries, loads these to RDFLib Graphs, and calls the "return_from_graph" function to return the content """ - graph = await queries_to_graph(queries) + graph, _ = await send_queries(queries) return await return_from_graph( graph, mediatype, profile, profile_headers, predicates_for_link_addition ) @@ -88,7 +88,7 @@ async def return_annotated_rdf( queries_for_uncached, annotations_graph = await get_annotation_properties( graph, **profile_annotation_props ) - anots_from_triplestore = await queries_to_graph([queries_for_uncached]) + anots_from_triplestore, _ = await send_queries([queries_for_uncached]) if len(anots_from_triplestore) > 1: annotations_graph += anots_from_triplestore cache += anots_from_triplestore @@ -111,9 +111,9 @@ def generate_prez_links(graph, predicates_for_link_addition): if predicates_for_link_addition["link_constructor"].endswith("/object?uri="): generate_object_endpoint_link(graph, predicates_for_link_addition) else: - if predicates_for_link_addition["ob_chi"]: + if predicates_for_link_addition["focus_to_child"]: triples_for_links = graph.triples_choices( - (None, predicates_for_link_addition["ob_chi"], None) + (None, predicates_for_link_addition["focus_to_child"], None) ) for triple in triples_for_links: graph.add( @@ -126,9 +126,9 @@ def generate_prez_links(graph, predicates_for_link_addition): ), ) ) - if predicates_for_link_addition["ib_chi"]: + if predicates_for_link_addition["child_to_focus"]: for triple in graph.triples_choices( - (None, predicates_for_link_addition["ib_chi"], None) + (None, predicates_for_link_addition["child_to_focus"], None) ): graph.add( ( @@ -140,9 +140,9 @@ def generate_prez_links(graph, predicates_for_link_addition): ), ) ) - if predicates_for_link_addition["ob_par"]: + if predicates_for_link_addition["focus_to_parent"]: triples_for_links = graph.triples_choices( - (None, predicates_for_link_addition["ob_par"], None) + (None, predicates_for_link_addition["focus_to_parent"], None) ) new_link_constructor = "/".join( predicates_for_link_addition["link_constructor"].split("/")[:-1] @@ -157,9 +157,9 @@ def generate_prez_links(graph, predicates_for_link_addition): ), ) ) - if predicates_for_link_addition["ib_par"]: + if predicates_for_link_addition["parent_to_focus"]: triples_for_links = graph.triples_choices( - (None, predicates_for_link_addition["ib_par"], None) + (None, predicates_for_link_addition["parent_to_focus"], None) ) new_link_constructor = "/".join( predicates_for_link_addition["link_constructor"].split("/")[:-1] @@ -194,10 +194,10 @@ def generate_prez_links(graph, predicates_for_link_addition): def generate_object_endpoint_link(graph, predicates_for_link_addition): all_preds = ( - predicates_for_link_addition["ib_par"] - + predicates_for_link_addition["ob_par"] - + predicates_for_link_addition["ib_chi"] - + predicates_for_link_addition["ob_chi"] + predicates_for_link_addition["parent_to_focus"] + + predicates_for_link_addition["focus_to_parent"] + + predicates_for_link_addition["child_to_focus"] + + predicates_for_link_addition["focus_to_child"] ) objects_for_links = graph.triples_choices((None, all_preds, None)) for o in objects_for_links: diff --git a/prez/routers/catprez.py b/prez/routers/catprez.py index e81e31bf..79210d40 100644 --- a/prez/routers/catprez.py +++ b/prez/routers/catprez.py @@ -22,7 +22,11 @@ async def catprez_profiles(): return PlainTextResponse("CatPrez Home") -@router.get("/c/catalogs", summary="List Catalogs") +@router.get( + "/c/catalogs", + summary="List Catalogs", + name="https://prez.dev/endpoint/catprez/catalog-listing", +) async def catalogs_endpoint( request: Request, page: int = 1, @@ -54,14 +58,22 @@ async def catalogs_endpoint( ) -@router.get("/c/catalogs/{catalog_curie}/{resource_curie}", summary="Get Resource") +@router.get( + "/c/catalogs/{catalog_curie}/{resource_curie}", + summary="Get Resource", + name="https://prez.dev/endpoint/catprez/resource", +) async def resource_endpoint( request: Request, catalog_curie: str = None, resource_curie: str = None ): return await item_endpoint(request) -@router.get("/c/catalogs/{catalog_curie}", summary="Get Catalog") +@router.get( + "/c/catalogs/{catalog_curie}", + summary="Get Catalog", + name="https://prez.dev/endpoint/catprez/catalog", +) async def catalog_endpoint(request: Request, catalog_curie: str = None): return await item_endpoint(request) @@ -72,7 +84,8 @@ async def item_endpoint(request: Request, cp_item: Optional[CatalogItem] = None) cp_item = CatalogItem( **request.path_params, **request.query_params, - url_path=str(request.url.path) + url_path=str(request.url.path), + endpoint_uri=request.scope["route"].name ) prof_and_mt_info = ProfilesMediatypesInfo(request=request, classes=cp_item.classes) cp_item.selected_class = prof_and_mt_info.selected_class diff --git a/prez/routers/object.py b/prez/routers/object.py index 7a76966e..d70302ee 100644 --- a/prez/routers/object.py +++ b/prez/routers/object.py @@ -1,51 +1,102 @@ +from string import Template + from fastapi import APIRouter, Request -from starlette.responses import PlainTextResponse +from rdflib import Graph, Literal, URIRef -from prez.models import SpatialItem, VocabItem, CatalogItem +from prez.cache import endpoints_graph_cache +from prez.models.object_item import ObjectItem +from prez.models.profiles_and_mediatypes import ProfilesMediatypesInfo +from prez.reference_data.prez_ns import PREZ +from prez.renderers.renderer import return_from_graph +from prez.services.curie_functions import get_curie_id_for_uri +from prez.sparql.methods import send_queries +from prez.sparql.objects_listings import ( + get_endpoint_template_queries, + generate_relationship_query, + generate_item_construct, +) router = APIRouter(tags=["Object"]) @router.get("/object", summary="Object") -async def object( +async def object_function( request: Request, ): - from prez.config import settings + object_item = ObjectItem(**request.path_params, **request.query_params) + prof_and_mt_info = ProfilesMediatypesInfo( + request=request, classes=object_item.classes + ) + # ignore profile returned by ProfilesMediatypesInfo for now - there is no 'hierarchy' among prez flavours' profiles + # at present, the behaviour for which should be chosen (or if one should be chosen at all) has not been defined. + object_item.selected_class = None + endpoint_to_relations = get_endpoint_info_for_classes(object_item.classes) - uri = request.query_params.get("uri") - if not uri: - return PlainTextResponse( - "An object uri must be provided as a query string argument (?uri=)" - ) + relationship_query = generate_relationship_query( + object_item.uri, endpoint_to_relations + ) + item_query = generate_item_construct(object_item, PREZ["profile/open"]) + item_graph, tabular_results = await send_queries( + rdf_queries=[item_query], tabular_queries=[relationship_query] + ) + # construct the system endpoint links + internal_links_graph = generate_system_links(tabular_results[0], object_item.uri) + return await return_from_graph( + item_graph + internal_links_graph, + prof_and_mt_info.mediatype, + PREZ["profile/open"], + prof_and_mt_info.profile_headers, + ) - prez_items = { - "SpacePrez": SpatialItem, - "VocPrez": VocabItem, - "CatPrez": CatalogItem, - } - - returned_items = {} - for prez in prez_items.keys(): - try: - item = prez_items[prez](uri=uri, url_path="/object") - returned_items[prez] = item - except Exception: # will get exception if URI does not exist with classes in prez flavour's SPARQL endpoint - pass - if len(returned_items) == 0: - return PlainTextResponse( - f"No object found for the provided URI in enabled prez flavours: {', '.join(settings.enabled_prezs)}" - ) - elif len(returned_items): - prez = list(returned_items.keys())[0] - if prez == "SpacePrez": - from prez.routers.spaceprez import item_endpoint - return await item_endpoint(request, returned_items[prez]) - elif prez == "VocPrez": - from prez.routers.vocprez import item_endpoint +# TODO add to readme: +# get classes from remote endpoint +# get endpoints which deliver classes & endpoint templates & parent relations for endpoints (from local prez graph) +# in parallel: +# get parent uris from remote endpoint +# get object information using open profile from remote endpoint +# construct the system links using the parent uris from the remote endpoint. +# merge the response with the system links - return await item_endpoint(request, returned_items[prez]) - elif prez == "CatPrez": - from prez.routers.catprez import item_endpoint - return await item_endpoint(request, returned_items[prez]) +def get_endpoint_info_for_classes(classes) -> dict: + """ + Queries Prez's in memory reference data for endpoints to determine which endpoints are relevant for the classes an + object has, along with information about "parent" objects included in the URL path for the object. This information + is whether the relationship in RDF is expected to be from the parent to the child, or from the child to the parent, + and the predicate used for the relationship. + """ + endpoint_query = get_endpoint_template_queries(classes) + results = endpoints_graph_cache.query(endpoint_query) + endpoint_to_relations = {} + for result in results.bindings: + endpoint_template = result["endpointTemplate"] + relation = result["relation"] + direction = result["direction"] + if endpoint_template not in endpoint_to_relations: + endpoint_to_relations[endpoint_template] = [(relation, direction)] + else: + endpoint_to_relations[endpoint_template].append((relation, direction)) + return endpoint_to_relations + + +def generate_system_links(relationship_results: list, object_uri: str) -> Graph: + internal_links_graph = Graph() + endpoints = [] + for endpoint_results in relationship_results: + endpoint_template = Template(endpoint_results["endpoint"]["value"]) + template_args = { + k: get_curie_id_for_uri(v["value"]) + for k, v in endpoint_results.items() + if k != "endpoint" + } | {"object": get_curie_id_for_uri(object_uri)} + endpoints.append(endpoint_template.substitute(template_args)) + for endpoint in endpoints: + internal_links_graph.add( + ( + URIRef(object_uri), + PREZ["link"], + Literal(endpoint), + ) + ) + return internal_links_graph diff --git a/prez/routers/search.py b/prez/routers/search.py index 34e8790f..c76cb1e1 100644 --- a/prez/routers/search.py +++ b/prez/routers/search.py @@ -4,7 +4,7 @@ from prez.cache import search_methods from prez.renderers.renderer import return_rdf -from prez.sparql.methods import query_to_graph +from prez.sparql.methods import rdf_queries_to_graph from prez.sparql.objects_listings import generate_item_construct router = APIRouter(tags=["Search"]) @@ -35,7 +35,7 @@ async def search( search_query, URIRef("https://w3id.org/profile/mem") ) - graph = await query_to_graph(full_query) + graph = await rdf_queries_to_graph(full_query) graph.bind("prez", "https://prez.dev/") return await return_rdf(graph, mediatype="text/anot+turtle", profile_headers={}) diff --git a/prez/routers/spaceprez.py b/prez/routers/spaceprez.py index 84621ed7..62cc36c1 100644 --- a/prez/routers/spaceprez.py +++ b/prez/routers/spaceprez.py @@ -22,7 +22,11 @@ async def spaceprez_profiles(): return PlainTextResponse("SpacePrez Home") -@router.get("/s/datasets", summary="List Datasets") +@router.get( + "/s/datasets", + summary="List Datasets", + name="https://prez.dev/endpoint/spaceprez/dataset", +) async def list_items( request: Request, page: Optional[int] = 1, per_page: Optional[int] = 20 ): @@ -55,6 +59,7 @@ async def list_items( @router.get( "/s/datasets/{dataset_curie}/collections", summary="List Feature Collections", + name="https://prez.dev/endpoint/spaceprez/feature-collection-listing", ) async def list_items_feature_collections( request: Request, dataset_curie: str, page: int = 1, per_page: int = 20 @@ -65,6 +70,7 @@ async def list_items_feature_collections( @router.get( "/s/datasets/{dataset_curie}/collections/{collection_curie}/items", summary="List Features", + name="https://prez.dev/endpoint/spaceprez/feature-listing", ) async def list_items_features( request: Request, @@ -76,7 +82,11 @@ async def list_items_features( return await list_items(request, page, per_page) -@router.get("/s/datasets/{dataset_curie}", summary="Get Dataset") +@router.get( + "/s/datasets/{dataset_curie}", + summary="Get Dataset", + name="https://prez.dev/endpoint/spaceprez/dataset", +) async def dataset_item(request: Request, dataset_curie: str): return await item_endpoint(request) @@ -84,6 +94,7 @@ async def dataset_item(request: Request, dataset_curie: str): @router.get( "/s/datasets/{dataset_curie}/collections/{collection_curie}", summary="Get Feature Collection", + name="https://prez.dev/endpoint/spaceprez/feature-collection", ) async def feature_collection_item( request: Request, dataset_curie: str, collection_curie: str @@ -94,6 +105,7 @@ async def feature_collection_item( @router.get( "/s/datasets/{dataset_curie}/collections/{collection_curie}/items/{feature_curie}", summary="Get Feature", + name="https://prez.dev/endpoint/spaceprez/feature", ) async def feature_item( request: Request, dataset_curie: str, collection_curie: str, feature_curie: str @@ -101,13 +113,13 @@ async def feature_item( return await item_endpoint(request) -@router.get("/s/object") async def item_endpoint(request: Request, spatial_item: Optional[SpatialItem] = None): if not spatial_item: spatial_item = SpatialItem( **request.path_params, **request.query_params, - url_path=str(request.url.path) + url_path=str(request.url.path), + endpoint_uri=request.scope["route"].name ) prof_and_mt_info = ProfilesMediatypesInfo( request=request, classes=spatial_item.classes diff --git a/prez/routers/vocprez.py b/prez/routers/vocprez.py index 8d630980..c77c1f89 100644 --- a/prez/routers/vocprez.py +++ b/prez/routers/vocprez.py @@ -25,9 +25,21 @@ async def vocprez_home(): return PlainTextResponse("VocPrez Home") -@router.get("/v/collection", summary="List Collections") -@router.get("/v/scheme", summary="List ConceptSchemes") -@router.get("/v/vocab", summary="List Vocabularies") +@router.get( + "/v/collection", + summary="List Collections", + name="https://prez.dev/endpoint/vocprez/collection-listing", +) +@router.get( + "/v/scheme", + summary="List ConceptSchemes", + name="https://prez.dev/endpoint/vocprez/schemes-listing", +) +@router.get( + "/v/vocab", + summary="List Vocabularies", + name="https://prez.dev/endpoint/vocprez/vocabs-listing", +) async def schemes_endpoint( request: Request, page: int = 1, @@ -59,41 +71,64 @@ async def schemes_endpoint( ) -@router.get("/v/vocab/{scheme_curie}", summary="Get ConceptScheme") -@router.get("/v/scheme/{scheme_curie}", summary="Get ConceptScheme") +@router.get( + "/v/vocab/{scheme_curie}", + summary="Get Vocabulary", + name="https://prez.dev/endpoint/vocprez/vocab", +) +@router.get( + "/v/scheme/{scheme_curie}", + summary="Get Concept Scheme", + name="https://prez.dev/endpoint/vocprez/scheme", +) async def vocprez_scheme(request: Request, scheme_curie: str): return await item_endpoint(request) -@router.get("/v/collection/{collection_curie}", summary="Get Collection") +@router.get( + "/v/collection/{collection_curie}", + summary="Get Collection", + name="https://prez.dev/endpoint/vocprez/collection", +) async def vocprez_collection(request: Request, collection_curie: str): return await item_endpoint(request) -@router.get("/v/collection/{collection_curie}/{concept_curie}", summary="Get Concept") +@router.get( + "/v/collection/{collection_curie}/{concept_curie}", + summary="Get Concept", + name="https://prez.dev/endpoint/vocprez/collection-concept", +) async def vocprez_collection_concept( request: Request, collection_curie: str, concept_curie: str ): return await item_endpoint(request) -@router.get("/v/scheme/{scheme_curie}/{concept_curie}", summary="Get Concept") -@router.get("/v/vocab/{scheme_curie}/{concept_curie}", summary="Get Concept") +@router.get( + "/v/scheme/{scheme_curie}/{concept_curie}", + summary="Get Concept", + name="https://prez.dev/endpoint/vocprez/scheme-concept", +) +@router.get( + "/v/vocab/{scheme_curie}/{concept_curie}", + summary="Get Concept", + name="https://prez.dev/endpoint/vocprez/vocab-concept", +) async def vocprez_scheme_concept( request: Request, scheme_curie: str, concept_curie: str ): return await item_endpoint(request) -@router.get("/v/object", summary="Get VocPrez Object") async def item_endpoint(request: Request, vp_item: Optional[VocabItem] = None): """Returns a VocPrez skos:Concept, Collection, Vocabulary, or ConceptScheme in the requested profile & mediatype""" - if not vp_item: vp_item = VocabItem( **request.path_params, **request.query_params, - url_path=str(request.url.path) + url_path=str(request.url.path), + endpoint_uri=request.scope["route"].name ) prof_and_mt_info = ProfilesMediatypesInfo(request=request, classes=vp_item.classes) vp_item.selected_class = prof_and_mt_info.selected_class diff --git a/prez/services/app_service.py b/prez/services/app_service.py index bb93956a..10789062 100644 --- a/prez/services/app_service.py +++ b/prez/services/app_service.py @@ -10,10 +10,11 @@ profiles_graph_cache, counts_graph, prefix_graph, + endpoints_graph_cache, ) from prez.config import settings from prez.reference_data.prez_ns import PREZ, ALTREXT -from prez.sparql.methods import query_to_graph +from prez.sparql.methods import rdf_queries_to_graph from prez.sparql.objects_listings import startup_count_objects log = logging.getLogger(__name__) @@ -50,7 +51,7 @@ async def healthcheck_sparql_endpoints(): async def count_objects(): query = startup_count_objects() - graph = await query_to_graph(query) + graph = await rdf_queries_to_graph(query) if len(graph) > 1: counts_graph.__iadd__(graph) @@ -98,3 +99,9 @@ async def add_prefixes_to_prefix_graph(): f'"{f.name}"' ) log.info("Prefixes from local files added to prefix graph") + + +async def create_endpoints_graph() -> Graph: + for f in (Path(__file__).parent.parent / "reference_data/endpoints").glob("*.ttl"): + endpoints_graph_cache.parse(f) + log.info("Prez endpoints graph loaded") diff --git a/prez/services/generate_profiles.py b/prez/services/generate_profiles.py index f6713431..7607d48d 100644 --- a/prez/services/generate_profiles.py +++ b/prez/services/generate_profiles.py @@ -7,8 +7,8 @@ from prez.cache import profiles_graph_cache from prez.models.model_exceptions import NoProfilesException -from prez.sparql.methods import query_to_graph from prez.services.curie_functions import get_curie_id_for_uri +from prez.sparql.methods import rdf_queries_to_graph from prez.sparql.objects_listings import select_profile_mediatype log = logging.getLogger(__name__) @@ -55,7 +55,7 @@ async def create_profiles_graph() -> Graph: } } """ - g = await query_to_graph(remote_profiles_query) + g = await rdf_queries_to_graph(remote_profiles_query) if len(g) > 0: profiles_graph_cache.__iadd__(g) log.info(f"Remote profile(s) found and added") @@ -63,7 +63,7 @@ async def create_profiles_graph() -> Graph: log.info("No remote profiles found") -@lru_cache(maxsize=128) +# @lru_cache(maxsize=128) def get_profiles_and_mediatypes( classes: FrozenSet[URIRef], requested_profile: URIRef = None, diff --git a/prez/services/model_methods.py b/prez/services/model_methods.py index cb377d72..8baf62e3 100644 --- a/prez/services/model_methods.py +++ b/prez/services/model_methods.py @@ -1,23 +1,33 @@ from typing import List - +from prez.cache import endpoints_graph_cache from rdflib import URIRef from prez.models.model_exceptions import URINotFoundException, ClassNotFoundException from prez.sparql.methods import sparql_query_non_async, sparql_ask_non_async -def get_classes(uri: URIRef, parent_predicates: List[URIRef] = None): +def get_classes(uri: URIRef, endpoint: URIRef = None) -> frozenset: q = f""" SELECT ?class {{<{uri}> a ?class . }} """ r = sparql_query_non_async(q) - classes = frozenset([c["class"]["value"] for c in r[1]]) + if endpoint: + endpoint_classes = endpoints_graph_cache.objects( + subject=endpoint, predicate=URIRef("https://prez.dev/ont/deliversClasses") + ) + object_classes_delivered_by_endpoint = [] + for c in r[1]: + if URIRef(c["class"]["value"]) in endpoint_classes: + object_classes_delivered_by_endpoint.append(c["class"]["value"]) + classes = frozenset(object_classes_delivered_by_endpoint) + else: + classes = frozenset([c["class"]["value"] for c in r[1]]) if not classes: # does the URI exist? r = sparql_ask_non_async(f"ASK {{<{uri}> ?p ?o}}") if not r[1]: # uri not found raise URINotFoundException(uri) - else: # we found the URI but it has no classes (line 14) + else: # we found the URI but it has no classes raise ClassNotFoundException(uri) - return frozenset([c["class"]["value"] for c in r[1]]) + return classes diff --git a/prez/services/search_methods.py b/prez/services/search_methods.py index 98021695..64370521 100644 --- a/prez/services/search_methods.py +++ b/prez/services/search_methods.py @@ -7,7 +7,7 @@ from prez.cache import search_methods from prez.models import SearchMethod from prez.reference_data.prez_ns import PREZ -from prez.sparql.methods import query_to_graph +from prez.sparql.methods import rdf_queries_to_graph log = logging.getLogger(__name__) @@ -24,7 +24,7 @@ async def get_remote_search_methods(): WHERE {{ ?s a prez:SearchMethod ; ?p ?o . }} """ - graph = await query_to_graph(remote_search_methods_query) + graph = await rdf_queries_to_graph(remote_search_methods_query) if len(graph) > 1: await generate_search_methods(graph) log.info(f"Remote search methods found and added.") diff --git a/prez/sparql/methods.py b/prez/sparql/methods.py index bbdca33c..8fd0d068 100644 --- a/prez/sparql/methods.py +++ b/prez/sparql/methods.py @@ -1,6 +1,6 @@ import asyncio import logging -from typing import Dict, Tuple, Union +from typing import Dict, Tuple, Union, Any from typing import List import httpx @@ -13,7 +13,6 @@ PREZ = Namespace("https://prez.dev/") - async_client = AsyncClient( auth=(settings.sparql_username, settings.sparql_password) if settings.sparql_username @@ -26,7 +25,6 @@ else None, ) - log = logging.getLogger(__name__) TIMEOUT = 30.0 @@ -104,15 +102,7 @@ async def send_query(query: str, mediatype="text/turtle"): return await async_client.send(query_rq, stream=True) -async def send_queries(queries: List[str]): - """Sends multiple SPARQL queries asynchronously. - Args: queries: List[str]: A list of SPARQL queries to be sent asynchronously. - Returns: List[httpx.Response]: A list of httpx.Response objects, one for each query - """ - return await asyncio.gather(*[send_query(query) for query in queries]) - - -async def query_to_graph(query: str): +async def rdf_queries_to_graph(query: str): """ Sends a SPARQL query asynchronously and parses the response into an RDFLib Graph. Args: query: str: A SPARQL query to be sent asynchronously. @@ -124,15 +114,38 @@ async def query_to_graph(query: str): return g.parse(data=response.text, format="turtle") -async def queries_to_graph(queries: List[str]): +async def send_queries( + rdf_queries: List[str], tabular_queries: List[str] = [] +) -> Tuple[Graph, List[Any]]: """ - Sends multiple SPARQL queries asynchronously and parses the responses into an RDFLib Graph. - Args: queries: List[str]: A list of SPARQL queries to be sent asynchronously. - Returns: rdflib.Graph: An RDFLib Graph object + Sends multiple SPARQL queries asynchronously and parses the responses into an RDFLib Graph for RDF queries + and a table format for table queries. + + Args: + rdf_queries: List[str]: A list of SPARQL queries for RDF graphs to be sent asynchronously. + tabular_queries: List[str]: A list of SPARQL queries for tables to be sent asynchronously. + + Returns: + Tuple[rdflib.Graph, List[Any]]: An RDFLib Graph object for RDF queries and a list of tables for table queries. """ - graphs = await asyncio.gather( - *[query_to_graph(query) for query in queries if query] + results = await asyncio.gather( + *[rdf_queries_to_graph(query) for query in rdf_queries if query], + *[tabular_queries_to_table(query) for query in tabular_queries if query] ) - for g in graphs[1:]: - graphs[0].__iadd__(g) - return graphs[0] + g = Graph() + tabular_results = [] + for result in results: + if isinstance(result, Graph): + g += result + else: + tabular_results.append(result) + return g, tabular_results + + +async def tabular_queries_to_table(query: str): + """ + Sends a SPARQL query asynchronously and parses the response into a table format. + """ + response = await send_query(query, "application/sparql-results+json") + await response.aread() + return response.json()["results"]["bindings"] diff --git a/prez/sparql/objects_listings.py b/prez/sparql/objects_listings.py index 3bb2b8ac..0dd04503 100644 --- a/prez/sparql/objects_listings.py +++ b/prez/sparql/objects_listings.py @@ -2,9 +2,9 @@ from functools import lru_cache from itertools import chain from textwrap import dedent -from typing import List, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union, Dict, FrozenSet -from rdflib import Graph, URIRef, RDFS, DCTERMS, Namespace +from rdflib import Graph, URIRef, RDFS, DCTERMS, Namespace, Literal from prez.cache import tbox_cache, profiles_graph_cache from prez.models import ( @@ -51,26 +51,26 @@ def generate_listing_construct( exclude_predicates ) = inverse_predicates = sequence_predicates = None ( - inbound_children, - inbound_parents, - outbound_children, - outbound_parents, + child_to_focus, + parent_to_focus, + focus_to_child, + focus_to_parent, relative_properties, ) = get_listing_predicates(profile, focus_item.selected_class) if ( focus_item.uri # and not focus_item.top_level_listing # if it's a top level class we don't need a listing relation - we're # # searching by class - and not inbound_children - and not inbound_parents - and not outbound_children - and not outbound_parents - # do not need to check relative properties - they will only be used if one of the inbound/outbound parent/child - # relations are defined + and not child_to_focus + and not parent_to_focus + and not focus_to_child + and not focus_to_parent + # do not need to check relative properties - they will only be used if one of the other listing relations + # are defined ): log.warning( f"Requested listing of objects related to {focus_item.uri}, however the profile {profile} does not" - f" define any listing relations for this for this class, for example outbound children." + f" define any listing relations for this for this class, for example focus to child." ) return None, {} uri_or_tl_item = ( @@ -92,32 +92,32 @@ def generate_listing_construct( PREFIX skos: CONSTRUCT {{ - {f'{uri_or_tl_item} a <{focus_item.general_class}> .{chr(10)}' if focus_item.top_level_listing else ""}\ + {f'{uri_or_tl_item} a <{focus_item.base_class}> .{chr(10)}' if focus_item.top_level_listing else ""}\ {sequence_construct} - {f'{uri_or_tl_item} ?outbound_children ?child_item .{chr(10)}' if outbound_children else ""}\ - {f'{uri_or_tl_item} ?outbound_parents ?parent_item .{chr(10)}' if outbound_parents else ""}\ - {f'?inbound_child_s ?inbound_child {uri_or_tl_item} .{chr(10)}' if inbound_children else ""}\ - {f'?inbound_parent_s ?inbound_parent {uri_or_tl_item} .{chr(10)}' if inbound_parents else ""}\ - {generate_relative_properties("construct", relative_properties, inbound_children, inbound_parents, - outbound_children, outbound_parents)}\ + {f'{uri_or_tl_item} ?focus_to_child ?child_item .{chr(10)}' if focus_to_child else ""}\ + {f'{uri_or_tl_item} ?focus_to_parent ?parent_item .{chr(10)}' if focus_to_parent else ""}\ + {f'?child_to_focus_s ?child_to_focus {uri_or_tl_item} .{chr(10)}' if child_to_focus else ""}\ + {f'?parent_to_focus_s ?parent_to_focus {uri_or_tl_item} .{chr(10)}' if parent_to_focus else ""}\ + {generate_relative_properties("construct", relative_properties, child_to_focus, parent_to_focus, + focus_to_child, focus_to_parent)}\ {f"{uri_or_tl_item} ?p ?o ." if include_predicates else ""}\ }} WHERE {{ - {f'{uri_or_tl_item} a <{focus_item.general_class}> .{chr(10)}' if focus_item.top_level_listing else ""}\ + {f'{uri_or_tl_item} a <{focus_item.base_class}> .{chr(10)}' if focus_item.top_level_listing else ""}\ {f'OPTIONAL {{ {uri_or_tl_item} ?p ?o .' if include_predicates else ""}\ {f'{generate_include_predicates(include_predicates)} }}' if include_predicates else ""} \ {sequence_construct_where}\ - {generate_outbound_predicates(uri_or_tl_item, outbound_children, outbound_parents)} \ - {generate_inbound_predicates(uri_or_tl_item, inbound_children, inbound_parents)} {chr(10)} \ - {generate_relative_properties("select", relative_properties, inbound_children, inbound_parents, - outbound_children, outbound_parents)}\ + {generate_focus_to_x_predicates(uri_or_tl_item, focus_to_child, focus_to_parent)} \ + {generate_x_to_focus_predicates(uri_or_tl_item, child_to_focus, parent_to_focus)} {chr(10)} \ + {generate_relative_properties("select", relative_properties, child_to_focus, parent_to_focus, + focus_to_child, focus_to_parent)}\ {{ SELECT ?top_level_item WHERE {{ - {f'{uri_or_tl_item} a <{focus_item.general_class}> .{chr(10)}' if focus_item.top_level_listing else ""}\ + {f'{uri_or_tl_item} a <{focus_item.base_class}> .{chr(10)}' if focus_item.top_level_listing else ""}\ }} {f"LIMIT {per_page}{chr(10)}" - f"OFFSET {(page - 1) * per_page}" if page is not None and per_page is not None else ""} + f"OFFSET {(page - 1) * per_page}" if page is not None and per_page is not None else ""} }} }} @@ -127,11 +127,11 @@ def generate_listing_construct( log.debug(f"Listing construct query for {focus_item} is:\n{query}") predicates_for_link_addition = { "link_constructor": focus_item.link_constructor, - "ib_par": inbound_parents, - "ob_par": outbound_parents, - "ib_chi": inbound_children, - "ob_chi": outbound_children, - "top_level_gen_class": focus_item.general_class + "parent_to_focus": parent_to_focus, + "focus_to_parent": focus_to_parent, + "child_to_focus": child_to_focus, + "focus_to_child": focus_to_child, + "top_level_gen_class": focus_item.base_class if focus_item.top_level_listing else None, # if this is a top level class, include it's general class here so we can create @@ -174,14 +174,14 @@ def generate_item_construct(focus_item, profile: URIRef): {f'{search_query_construct()} {chr(10)}' if search_query else ""}\ \t{uri_or_search_item} ?p ?o1 . {sequence_construct} - {f'{chr(9)}?s ?inbound_p {uri_or_search_item} .' if inverse_predicates else ""} + {f'{chr(9)}?s ?inverse_predicate {uri_or_search_item} .' if inverse_predicates else ""} {generate_bnode_construct(bnode_depth)} \ \n}} WHERE {{ {{ {f'{focus_item.populated_query}' if search_query else ""} }} {{ {uri_or_search_item} ?p ?o1 . {chr(10)} \ - {f'?s ?inbound_p {uri_or_search_item}{chr(10)}' if inverse_predicates else chr(10)} \ + {f'?s ?inverse_predicate {uri_or_search_item}{chr(10)}' if inverse_predicates else chr(10)} \ {generate_include_predicates(include_predicates)} \ {generate_inverse_predicates(inverse_predicates)} \ {generate_bnode_select(bnode_depth)}\ @@ -228,8 +228,8 @@ def generate_relative_properties( "op": out_parents, } other_kvs = { - "ic": "inbound_child_s", - "ip": "inbound_parent_s", + "ic": "child_to_focus_s", + "ip": "parent_to_focus_s", "oc": "child_item", "op": "parent_item", } @@ -243,31 +243,31 @@ def generate_relative_properties( return rel_string -def generate_outbound_predicates(uri_or_tl_item, outbound_children, outbound_parents): +def generate_focus_to_x_predicates(uri_or_tl_item, focus_to_child, focus_to_parent): where = "" - if outbound_children: - where += f"""{uri_or_tl_item} ?outbound_children ?child_item . - VALUES ?outbound_children {{ {" ".join('<' + str(pred) + '>' for pred in outbound_children)} }}\n""" - if outbound_parents: - where += f"""{uri_or_tl_item} ?outbound_parents ?parent_item . - VALUES ?outbound_parents {{ {" ".join('<' + str(pred) + '>' for pred in outbound_parents)} }}\n""" - # if not outbound_children and not outbound_parents: - # where += "VALUES ?outbound_children {}\nVALUES ?outbound_parents {}" + if focus_to_child: + where += f"""{uri_or_tl_item} ?focus_to_child ?child_item . + VALUES ?focus_to_child {{ {" ".join('<' + str(pred) + '>' for pred in focus_to_child)} }}\n""" + if focus_to_parent: + where += f"""{uri_or_tl_item} ?focus_to_parent ?parent_item . + VALUES ?focus_to_parent {{ {" ".join('<' + str(pred) + '>' for pred in focus_to_parent)} }}\n""" + # if not focus_to_child and not focus_to_parent: + # where += "VALUES ?focus_to_child {}\nVALUES ?focus_to_parent {}" return where -def generate_inbound_predicates(uri_or_tl_item, inbound_children, inbound_parents): - if not inbound_children and not inbound_parents: +def generate_x_to_focus_predicates(uri_or_tl_item, child_to_focus, parent_to_focus): + if not child_to_focus and not parent_to_focus: return "" where = "" - if inbound_children: - where += f"""?inbound_child_s ?inbound_child {uri_or_tl_item} ; - VALUES ?inbound_child {{ {" ".join('<' + str(pred) + '>' for pred in inbound_children)} }}\n""" - if inbound_parents: - where += f"""?inbound_parent_s ?inbound_parent {uri_or_tl_item} ; - VALUES ?inbound_parent {{ {" ".join('<' + str(pred) + '>' for pred in inbound_parents)} }}\n""" - # if not inbound_children and not inbound_parents: - # where += "VALUES ?inbound_child {}\nVALUES ?inbound_parent {}" + if child_to_focus: + where += f"""?child_to_focus_s ?child_to_focus {uri_or_tl_item} ; + VALUES ?child_to_focus {{ {" ".join('<' + str(pred) + '>' for pred in child_to_focus)} }}\n""" + if parent_to_focus: + where += f"""?parent_to_focus_s ?parent_to_focus {uri_or_tl_item} ; + VALUES ?parent_to_focus {{ {" ".join('<' + str(pred) + '>' for pred in parent_to_focus)} }}\n""" + # if not child_to_focus and not parent_to_focus: + # where += "VALUES ?child_to_focus {}\nVALUES ?parent_to_focus {}" return where @@ -284,10 +284,10 @@ def generate_include_predicates(include_predicates): def generate_inverse_predicates(inverse_predicates): """ Generates a SPARQL VALUES clause for a list of inverse predicates, of the form: - VALUES ?inbound_p { } + VALUES ?inverse_predicate { } """ if inverse_predicates: - return f"""VALUES ?inbound_p{{\n{chr(10).join([f"<{p}>" for p in inverse_predicates])}\n}}""" + return f"""VALUES ?inverse_predicate{{\n{chr(10).join([f"<{p}>" for p in inverse_predicates])}\n}}""" return "" @@ -438,7 +438,7 @@ def other_predicates_statement(other_predicates, uncached_terms_other): VALUES ?unexplained_term {{ {" ".join('<' + str(term) + '>' for term in uncached_terms["provenance"])} }} }} - { other_predicates_statement(other_predicates, uncached_terms["other"]) if other_predicates else ""} + {other_predicates_statement(other_predicates, uncached_terms["other"]) if other_predicates else ""} }}""" return queries_for_uncached, labels_g @@ -534,11 +534,11 @@ def generate_listing_count_construct( f""" PREFIX prez: - CONSTRUCT {{ <{item.general_class}> prez:count ?count }} + CONSTRUCT {{ <{item.base_class}> prez:count ?count }} WHERE {{ SELECT (COUNT(?item) as ?count) WHERE {{ - ?item a <{item.general_class}> . + ?item a <{item.base_class}> . }} }}""" ).strip() @@ -633,58 +633,58 @@ def get_listing_predicates(profile, selected_class): 1. "Collection" endpoints, for top level listing of objects of a particular type 2. For a specific object, where it has members The predicates retrieved from profiles are: - - inbound children, for example where the object of interest is a Concept Scheme, and is linked to Concept(s) via + - child to focus, for example where the object of interest is a Concept Scheme, and is linked to Concept(s) via the predicate skos:inScheme - - outbound children, for example where the object of interest is a Feature Collection, and is linked to Feature(s) + - focus to child, for example where the object of interest is a Feature Collection, and is linked to Feature(s) via the predicate rdfs:member - - inbound parents, for example where the object of interest is a Feature Collection, and is linked to Dataset(s) via + - parent to focus, for example where the object of interest is a Feature Collection, and is linked to Dataset(s) via the predicate dcterms:hasPart - - outbound parents, for example where the object of interest is a Concept, and is linked to Concept Scheme(s) via + - focus to parents, for example where the object of interest is a Concept, and is linked to Concept Scheme(s) via the predicate skos:inScheme - relative properties, properties of the parent/child objects that should also be returned. For example, if the focus object is a Concept Scheme, and the predicate skos:inScheme is used to link from Concept(s) (using - altr-ext:inboundChildren) then specifying skos:broader as a relative property will cause the broader concepts to + altr-ext:childToFocus) then specifying skos:broader as a relative property will cause the broader concepts to be returned for each concept """ shape_bns = get_relevant_shape_bns_for_profile(selected_class, profile) if not shape_bns: return [], [], [], [], [] - inbound_children = [ + child_to_focus = [ i[2] for i in profiles_graph_cache.triples_choices( ( shape_bns, - ALTREXT.inboundChildren, + ALTREXT.childToFocus, None, ) ) ] - inbound_parents = [ + parent_to_focus = [ i[2] for i in profiles_graph_cache.triples_choices( ( shape_bns, - ALTREXT.inboundParents, + ALTREXT.parentToFocus, None, ) ) ] - outbound_children = [ + focus_to_child = [ i[2] for i in profiles_graph_cache.triples_choices( ( shape_bns, - ALTREXT.outboundChildren, + ALTREXT.focusToChild, None, ) ) ] - outbound_parents = [ + focus_to_parent = [ i[2] for i in profiles_graph_cache.triples_choices( ( shape_bns, - ALTREXT.outboundParents, + ALTREXT.focusToParent, None, ) ) @@ -700,10 +700,10 @@ def get_listing_predicates(profile, selected_class): ) ] return ( - inbound_children, - inbound_parents, - outbound_children, - outbound_parents, + child_to_focus, + parent_to_focus, + focus_to_child, + focus_to_parent, relative_properties, ) @@ -813,8 +813,8 @@ def select_profile_mediatype( WHERE {{ VALUES ?class {{{" ".join('<' + str(klass) + '>' for klass in classes)}}} ?class rdfs:subClassOf* ?mid . - ?mid rdfs:subClassOf* ?general_class . - VALUES ?general_class {{ dcat:Dataset geo:FeatureCollection prez:FeatureCollectionList prez:FeatureList geo:Feature + ?mid rdfs:subClassOf* ?base_class . + VALUES ?base_class {{ dcat:Dataset geo:FeatureCollection prez:FeatureCollectionList prez:FeatureList geo:Feature skos:ConceptScheme skos:Concept skos:Collection prez:DatasetList prez:VocPrezCollectionList prez:SchemesList prez:CatalogList prez:ProfilesList dcat:Catalog dcat:Resource prof:Profile prez:SPARQLQuery }} ?profile altr-ext:constrainsClass ?class ; @@ -857,6 +857,61 @@ def generate_mediatype_if_statements(requested_mediatypes: list): return ifs +def get_endpoint_template_queries(classes: FrozenSet[URIRef]): + query = f"""PREFIX ont: + + SELECT ?classes ?parent_endpoint ?endpoint ?relation ?direction ?endpointTemplate + (count(?intermediate) as ?distance) WHERE {{ + VALUES ?classes {{ {" ".join('<' + klass + '>' for klass in classes)} }} + ?endpoint a ont:Endpoint ; + ont:endpointTemplate ?endpointTemplate ; + ont:deliversClasses ?classes . + ?endpoint ont:parentEndpoint* ?intermediate . + ?intermediate ont:parentEndpoint* ?parent_endpoint . + OPTIONAL {{ + ?parent_endpoint ont:ParentToFocusRelation ?relation . + BIND ("parent_to_focus" AS ?direction) + }} + OPTIONAL {{ + ?parent_endpoint ont:FocusToParentRelation ?relation . + BIND ("focus_to_parent" AS ?direction) + }} + FILTER (BOUND(?relation)) + }} GROUP BY ?endpoint ?parent_endpoint ?relation ?direction ?classes ?endpointTemplate + ORDER BY ?endpoint DESC(?distance)""" + return query + + +def generate_relationship_query( + uri: URIRef, endpoint_to_relations: Dict[URIRef, List[Tuple[URIRef, URIRef]]] +): + for endpoint, relations in endpoint_to_relations.items(): + construct_subquery = generate_relationship_query(uri, endpoint, relations) + + +def generate_relationship_query( + uri: URIRef, endpoint_to_relations: Dict[URIRef, List[Tuple[URIRef, Literal]]] +): + subqueries = [] + for endpoint, relations in endpoint_to_relations.items(): + subquery = f"""{{ SELECT ?endpoint {" ".join(["?parent_" + str(i+1) for i, _ in enumerate(relations)])} + WHERE {{\n BIND("{endpoint}" as ?endpoint)\n""" + previous_uri = f"<{uri}>" + for i, relation in enumerate(relations): + predicate, direction = relation + parent = "?parent_" + str(i + 1) + if direction == Literal("parent_to_focus"): + subquery += f"{parent} <{predicate}> {previous_uri} .\n" + else: # assuming the direction is "focus_to_parent" + subquery += f"{previous_uri} <{predicate}> {parent} .\n" + previous_uri = parent + subquery += "}}" + subqueries.append(subquery) + + union_query = "SELECT * {" + " UNION ".join(subqueries) + "}" + return union_query + + def startup_count_objects(): """ Retrieves hardcoded counts for collections in the dataset (feature collections, datasets etc.) diff --git a/tests/data/spaceprez/expected_responses/dataset_listing_anot.ttl b/tests/data/spaceprez/expected_responses/dataset_listing_anot.ttl index 49cb195b..7322ebfe 100644 --- a/tests/data/spaceprez/expected_responses/dataset_listing_anot.ttl +++ b/tests/data/spaceprez/expected_responses/dataset_listing_anot.ttl @@ -19,5 +19,8 @@ dcterms:title "Geocoded National Address File"@en ; ns1:link "/s/datasets/ldgovau:gnaf" . + a dcat:Dataset ; + ns1:link "/s/datasets/ns1:dataset" . + dcat:Dataset rdfs:label "Dataset"@en ; - ns1:count 3 . + ns1:count 4 . diff --git a/tests/data/spaceprez/input/multiple_object.ttl b/tests/data/spaceprez/input/multiple_object.ttl index 8652ac69..303a862e 100644 --- a/tests/data/spaceprez/input/multiple_object.ttl +++ b/tests/data/spaceprez/input/multiple_object.ttl @@ -1,3 +1,4 @@ +PREFIX dcat: PREFIX skos: PREFIX dcterms: PREFIX reg: @@ -17,3 +18,13 @@ PREFIX geo: skos:inScheme ; skos:prefLabel "alteration facies contact"@en ; . + + + a geo:FeatureCollection ; + rdfs:member ; +. + + + a dcat:Dataset ; + rdfs:member ; +. diff --git a/tests/sparql/test_sparql_new.py b/tests/sparql/test_sparql_new.py index 8cbcca2b..a9b360c7 100644 --- a/tests/sparql/test_sparql_new.py +++ b/tests/sparql/test_sparql_new.py @@ -211,8 +211,8 @@ def test_generate_listing_construct_pagination(): def test_get_profile_predicates_sequence(sp_test_client): profile = URIRef("https://w3id.org/profile/vocpub") - general_class = SKOS.ConceptScheme - preds = get_item_predicates(profile, general_class) + base_class = SKOS.ConceptScheme + preds = get_item_predicates(profile, base_class) assert preds[3] == [ [ URIRef("http://www.w3.org/2000/01/rdf-schema#member"), From aa61e611746ae79d605c3d09a8f5bba09ad1e9d5 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 25 Jun 2023 22:59:53 +1000 Subject: [PATCH 2/3] Update documentation. Remove commented code. --- README-Dev.md | 16 +++++++++++++++- prez/models/object_item.py | 24 ------------------------ prez/routers/object.py | 10 ---------- 3 files changed, 15 insertions(+), 35 deletions(-) diff --git a/README-Dev.md b/README-Dev.md index f9a42bb0..3a8b2e3a 100644 --- a/README-Dev.md +++ b/README-Dev.md @@ -77,7 +77,21 @@ using the properties listed below. | provenance | dcterms:provenance | dcterms:source | altr-ext:hasExplanationPredicate | | other | (None) | schema:color | altr-ext:otherAnnotationProps | -## High Level Sequence +## High Level Sequence `/object` endpoint + +Prez provides a `/object` endpoint as an endpoint that supplies any information known about a given URI. If an annotated +mediatype is requested, prez will additionally provide all system links for endpoints which can render the object. The +high level sequence for this endpoint is as follows: + +1. Get the URI for the object from the query string +2. Get the class(es) of the object from the triplestore +3. Use prez's reference data for endpoints to determine which endpoints can render this object, and, a template for +these endpoints, specifying any variables that need to be substituted (such as parent URIs). +4. Get the object information from the triplestore, using an open profile, and in parallel any system information needed +to construct the system links. +5. Return the response + +## High Level Sequence listing and individual object endpoints Prez follows the following logic to determine what information to return, based on a profile, and in what mediatype to return it. diff --git a/prez/models/object_item.py b/prez/models/object_item.py index 6190884d..da1edfd0 100644 --- a/prez/models/object_item.py +++ b/prez/models/object_item.py @@ -27,27 +27,3 @@ def populate(cls, values): uri_str = values["uri"] values["uri"] = URIRef(uri_str) return values - - # get_parents_query = - # object_query = - - # values - # - # assert uri or id - # if id: - # values["uri"] = get_uri_for_curie_id(id) - # elif uri: - # values["id"] = get_curie_id_for_uri(uri) - # q = f"""SELECT ?class {{ <{values["uri"]}> a ?class }}""" - # r = profiles_graph_cache.query(q) - # if len(r.bindings) > 0: - # values["classes"] = frozenset([prof.get("class") for prof in r.bindings]) - # return values - - # get classes from remote endpoint - # get endpoints which deliver classes & endpoint templates & parent relations for endpoints (from local prez graph) - # in parallel: - # get parent uris from remote endpoint - # get object information using open profile from remote endpoint - # construct the system links using the parent uris from the remote endpoint. - # merge the response with the system links diff --git a/prez/routers/object.py b/prez/routers/object.py index d70302ee..318ac139 100644 --- a/prez/routers/object.py +++ b/prez/routers/object.py @@ -49,16 +49,6 @@ async def object_function( ) -# TODO add to readme: -# get classes from remote endpoint -# get endpoints which deliver classes & endpoint templates & parent relations for endpoints (from local prez graph) -# in parallel: -# get parent uris from remote endpoint -# get object information using open profile from remote endpoint -# construct the system links using the parent uris from the remote endpoint. -# merge the response with the system links - - def get_endpoint_info_for_classes(classes) -> dict: """ Queries Prez's in memory reference data for endpoints to determine which endpoints are relevant for the classes an From e493bdd5de4c33bb90ae9fa8dcbe13ded9c98d88 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 26 Jun 2023 14:24:30 +1000 Subject: [PATCH 3/3] Update documentation. Remove commented code. Fix object tests. --- .../endpoints/spaceprez_endpoints.ttl | 1 + prez/routers/object.py | 18 +++--- prez/sparql/objects_listings.py | 61 ++++++++++--------- tests/data/object/expected_responses/fc.ttl | 7 +++ .../object/expected_responses/feature.ttl | 13 ++++ tests/object/test_endpoints_object.py | 24 +++++++- 6 files changed, 85 insertions(+), 39 deletions(-) create mode 100644 tests/data/object/expected_responses/fc.ttl create mode 100644 tests/data/object/expected_responses/feature.ttl diff --git a/prez/reference_data/endpoints/spaceprez_endpoints.ttl b/prez/reference_data/endpoints/spaceprez_endpoints.ttl index c3fbdee5..6598ebe3 100644 --- a/prez/reference_data/endpoints/spaceprez_endpoints.ttl +++ b/prez/reference_data/endpoints/spaceprez_endpoints.ttl @@ -14,6 +14,7 @@ endpoint:dataset a ont:Endpoint ; ont:parentEndpoint endpoint:dataset-listing ; ont:deliversClasses dcat:Dataset ; ont:endpointTemplate "/s/datasets/$object" ; + ont:FocustoChildRelation rdfs:member ; . endpoint:feature-collection-listing a ont:Endpoint ; diff --git a/prez/routers/object.py b/prez/routers/object.py index 318ac139..411e1108 100644 --- a/prez/routers/object.py +++ b/prez/routers/object.py @@ -31,7 +31,6 @@ async def object_function( # at present, the behaviour for which should be chosen (or if one should be chosen at all) has not been defined. object_item.selected_class = None endpoint_to_relations = get_endpoint_info_for_classes(object_item.classes) - relationship_query = generate_relationship_query( object_item.uri, endpoint_to_relations ) @@ -59,14 +58,15 @@ def get_endpoint_info_for_classes(classes) -> dict: endpoint_query = get_endpoint_template_queries(classes) results = endpoints_graph_cache.query(endpoint_query) endpoint_to_relations = {} - for result in results.bindings: - endpoint_template = result["endpointTemplate"] - relation = result["relation"] - direction = result["direction"] - if endpoint_template not in endpoint_to_relations: - endpoint_to_relations[endpoint_template] = [(relation, direction)] - else: - endpoint_to_relations[endpoint_template].append((relation, direction)) + if results.bindings != [{}]: + for result in results.bindings: + endpoint_template = result["endpointTemplate"] + relation = result["relation"] + direction = result["direction"] + if endpoint_template not in endpoint_to_relations: + endpoint_to_relations[endpoint_template] = [(relation, direction)] + else: + endpoint_to_relations[endpoint_template].append((relation, direction)) return endpoint_to_relations diff --git a/prez/sparql/objects_listings.py b/prez/sparql/objects_listings.py index 0dd04503..892ff704 100644 --- a/prez/sparql/objects_listings.py +++ b/prez/sparql/objects_listings.py @@ -860,35 +860,37 @@ def generate_mediatype_if_statements(requested_mediatypes: list): def get_endpoint_template_queries(classes: FrozenSet[URIRef]): query = f"""PREFIX ont: - SELECT ?classes ?parent_endpoint ?endpoint ?relation ?direction ?endpointTemplate - (count(?intermediate) as ?distance) WHERE {{ +SELECT ?classes ?parent_endpoint ?endpoint ?relation ?direction ?endpointTemplate +(count(?intermediate) as ?distance) WHERE {{ VALUES ?classes {{ {" ".join('<' + klass + '>' for klass in classes)} }} - ?endpoint a ont:Endpoint ; - ont:endpointTemplate ?endpointTemplate ; - ont:deliversClasses ?classes . - ?endpoint ont:parentEndpoint* ?intermediate . - ?intermediate ont:parentEndpoint* ?parent_endpoint . - OPTIONAL {{ - ?parent_endpoint ont:ParentToFocusRelation ?relation . - BIND ("parent_to_focus" AS ?direction) - }} - OPTIONAL {{ - ?parent_endpoint ont:FocusToParentRelation ?relation . - BIND ("focus_to_parent" AS ?direction) - }} - FILTER (BOUND(?relation)) - }} GROUP BY ?endpoint ?parent_endpoint ?relation ?direction ?classes ?endpointTemplate - ORDER BY ?endpoint DESC(?distance)""" + {{ + ?endpoint a ont:Endpoint ; + ont:endpointTemplate ?endpointTemplate ; + ont:deliversClasses ?classes . + }} + UNION + {{ + ?endpoint a ont:Endpoint ; + ont:endpointTemplate ?endpointTemplate ; + ont:deliversClasses ?classes . + ?endpoint ont:parentEndpoint* ?intermediate . + ?intermediate ont:parentEndpoint* ?parent_endpoint . + OPTIONAL {{ + ?parent_endpoint ont:ParentToFocusRelation ?relation . + BIND ("parent_to_focus" AS ?direction) + }} + OPTIONAL {{ + ?parent_endpoint ont:FocusToParentRelation ?relation . + BIND ("focus_to_parent" AS ?direction) + }} + FILTER (BOUND(?relation)) + }} +}} GROUP BY ?endpoint ?parent_endpoint ?relation ?direction ?classes ?endpointTemplate +ORDER BY ?endpoint DESC(?distance) + """ return query -def generate_relationship_query( - uri: URIRef, endpoint_to_relations: Dict[URIRef, List[Tuple[URIRef, URIRef]]] -): - for endpoint, relations in endpoint_to_relations.items(): - construct_subquery = generate_relationship_query(uri, endpoint, relations) - - def generate_relationship_query( uri: URIRef, endpoint_to_relations: Dict[URIRef, List[Tuple[URIRef, Literal]]] ): @@ -900,10 +902,11 @@ def generate_relationship_query( for i, relation in enumerate(relations): predicate, direction = relation parent = "?parent_" + str(i + 1) - if direction == Literal("parent_to_focus"): - subquery += f"{parent} <{predicate}> {previous_uri} .\n" - else: # assuming the direction is "focus_to_parent" - subquery += f"{previous_uri} <{predicate}> {parent} .\n" + if predicate: + if direction == Literal("parent_to_focus"): + subquery += f"{parent} <{predicate}> {previous_uri} .\n" + else: # assuming the direction is "focus_to_parent" + subquery += f"{previous_uri} <{predicate}> {parent} .\n" previous_uri = parent subquery += "}}" subqueries.append(subquery) diff --git a/tests/data/object/expected_responses/fc.ttl b/tests/data/object/expected_responses/fc.ttl new file mode 100644 index 00000000..3c271989 --- /dev/null +++ b/tests/data/object/expected_responses/fc.ttl @@ -0,0 +1,7 @@ +@prefix geo: . +@prefix ns1: . +@prefix rdfs: . + + a geo:FeatureCollection ; + rdfs:member ; + ns1:link "/s/datasets/ns1:dataset/collections/ns1:feature-collection" . diff --git a/tests/data/object/expected_responses/feature.ttl b/tests/data/object/expected_responses/feature.ttl new file mode 100644 index 00000000..9df551fe --- /dev/null +++ b/tests/data/object/expected_responses/feature.ttl @@ -0,0 +1,13 @@ +@prefix dcterms: . +@prefix geo: . +@prefix ns1: . +@prefix xsd: . + + a geo:Feature, + ; + dcterms:identifier "102208962"^^xsd:token ; + dcterms:title "Contracted Catchment 102208962" ; + dcterms:type ; + geo:hasGeometry [ geo:asWKT "MULTIPOLYGON (((122.23180562900006 -17.564583177999964, 122.23208340700012 -17.564583177999964, 122.23208340700012 -17.56486095599996, 122.23180562900006 -17.56486095599996, 122.23180562900006 -17.564583177999964)), ((122.23180562900006 -17.564583177999964, 122.23152785200011 -17.564583177999964, 122.23152785200011 -17.564305399999967, 122.23180562900006 -17.564305399999967, 122.23180562900006 -17.564583177999964)), ((122.23152785200011 -17.564305399999967, 122.23125007400006 -17.564305399999967, 122.23125007400006 -17.56402762199997, 122.23152785200011 -17.56402762199997, 122.23152785200011 -17.564305399999967)), ((122.23125007400006 -17.56402762199997, 122.22902785200006 -17.56402762199997, 122.22902785200006 -17.564305399999967, 122.22875007400012 -17.564305399999967, 122.22875007400012 -17.564583177999964, 122.22847229600006 -17.564583177999964, 122.22847229600006 -17.56486095599996, 122.22819451800001 -17.56486095599996, 122.22819451800001 -17.56513873299997, 122.22791674000007 -17.56513873299997, 122.22791674000007 -17.565416510999967, 122.22763896300012 -17.565416510999967, 122.22763896300012 -17.565694288999964, 122.22736118500006 -17.565694288999964, 122.22736118500006 -17.56597206699996, 122.22708340700001 -17.56597206699996, 122.22708340700001 -17.56624984499996, 122.22680562900007 -17.56624984499996, 122.22680562900007 -17.566527621999967, 122.22291674000007 -17.566527621999967, 122.22291674000007 -17.566805399999964, 122.22263896300001 -17.566805399999964, 122.22263896300001 -17.56708317799996, 122.22236118500007 -17.56708317799996, 122.22236118500007 -17.56736095599996, 122.22208340700001 -17.56736095599996, 122.22208340700001 -17.567638732999967, 122.22180562900007 -17.567638732999967, 122.22180562900007 -17.567916510999964, 122.22152785200001 -17.567916510999964, 122.22152785200001 -17.568194288999962, 122.22125007400007 -17.568194288999962, 122.22125007400007 -17.56847206699996, 122.22097229600001 -17.56847206699996, 122.22097229600001 -17.568749844999957, 122.22069451800007 -17.568749844999957, 122.22069451800007 -17.569027621999965, 122.22041674000002 -17.569027621999965, 122.22041674000002 -17.569305399999962, 122.22013896300007 -17.569305399999962, 122.22013896300007 -17.56958317799996, 122.21986118500001 -17.56958317799996, 122.21986118500001 -17.569860955999957, 122.21958340700007 -17.569860955999957, 122.21958340700007 -17.570138732999965, 122.21930562900002 -17.570138732999965, 122.21930562900002 -17.570416510999962, 122.21902785200007 -17.570416510999962, 122.21902785200007 -17.57069428899996, 122.21875007400001 -17.57069428899996, 122.21875007400001 -17.570972066999957, 122.21208340700002 -17.570972066999957, 122.21208340700002 -17.571249844999954, 122.21180562900008 -17.571249844999954, 122.21180562900008 -17.571527621999962, 122.21152785100003 -17.571527621999962, 122.21152785100003 -17.57180539999996, 122.21125007400008 -17.57180539999996, 122.21125007400008 -17.572083177999957, 122.21097229600002 -17.572083177999957, 122.21097229600002 -17.572360955999955, 122.21069451800008 -17.572360955999955, 122.21069451800008 -17.572638732999962, 122.21041674000003 -17.572638732999962, 122.21041674000003 -17.57291651099996, 122.21013896300008 -17.57291651099996, 122.21013896300008 -17.573194288999957, 122.20986118500002 -17.573194288999957, 122.20986118500002 -17.573472066999955, 122.20958340700008 -17.573472066999955, 122.20958340700008 -17.573749844999952, 122.20930562900003 -17.573749844999952, 122.20930562900003 -17.57402762199996, 122.20902785100009 -17.57402762199996, 122.20902785100009 -17.574305399999957, 122.20875007400002 -17.574305399999957, 122.20875007400002 -17.574583177999955, 122.20847229600008 -17.574583177999955, 122.20847229600008 -17.574860955999952, 122.20819451800003 -17.574860955999952, 122.20819451800003 -17.57513873299996, 122.20791674000009 -17.57513873299996, 122.20791674000009 -17.575416510999958, 122.20763896300002 -17.575416510999958, 122.20763896300002 -17.575694288999955, 122.20736118500008 -17.575694288999955, 122.20736118500008 -17.575972066999952, 122.20708340700003 -17.575972066999952, 122.20708340700003 -17.57624984499995, 122.20680562900009 -17.57624984499995, 122.20680562900009 -17.576527621999958, 122.20652785100003 -17.576527621999958, 122.20652785100003 -17.576805399999955, 122.20625007400008 -17.576805399999955, 122.20625007400008 -17.577083177999953, 122.20597229600003 -17.577083177999953, 122.20597229600003 -17.57736095599995, 122.20569451800009 -17.57736095599995, 122.20569451800009 -17.577638733999947, 122.20541674000003 -17.577638733999947, 122.20541674000003 -17.577916510999955, 122.20513896300008 -17.577916510999955, 122.20513896300008 -17.578194288999953, 122.20486118500003 -17.578194288999953, 122.20486118500003 -17.57847206699995, 122.20430562900003 -17.57847206699995, 122.20430562900003 -17.578749844999948, 122.20402785100009 -17.578749844999948, 122.20402785100009 -17.579027621999955, 122.20375007400003 -17.579027621999955, 122.20375007400003 -17.579305399999953, 122.20347229600009 -17.579305399999953, 122.20347229600009 -17.57958317799995, 122.2001389620001 -17.57958317799995, 122.2001389620001 -17.579860955999948, 122.19986118500003 -17.579860955999948, 122.19986118500003 -17.580138733999945, 122.19958340700009 -17.580138733999945, 122.19958340700009 -17.580416510999953, 122.19930562900004 -17.580416510999953, 122.19930562900004 -17.58069428899995, 122.1990278510001 -17.58069428899995, 122.1990278510001 -17.580972066999948, 122.19875007400003 -17.580972066999948, 122.19875007400003 -17.581249844999945, 122.19847229600009 -17.581249844999945, 122.19847229600009 -17.581527621999953, 122.19819451800004 -17.581527621999953, 122.19819451800004 -17.58180539999995, 122.1979167400001 -17.58180539999995, 122.1979167400001 -17.582083177999948, 122.19763896200004 -17.582083177999948, 122.19763896200004 -17.582360955999945, 122.19736118500009 -17.582360955999945, 122.19736118500009 -17.582638733999943, 122.19708340700004 -17.582638733999943, 122.19708340700004 -17.58291651099995, 122.19652785100004 -17.58291651099995, 122.19652785100004 -17.583194288999948, 122.19597229600004 -17.583194288999948, 122.19597229600004 -17.583472066999946, 122.19152785100005 -17.583472066999946, 122.19152785100005 -17.583749844999943, 122.1912500740001 -17.583749844999943, 122.1912500740001 -17.58402762199995, 122.19097229600004 -17.58402762199995, 122.19097229600004 -17.58430539999995, 122.1906945180001 -17.58430539999995, 122.1906945180001 -17.584583177999946, 122.19041674000005 -17.584583177999946, 122.19041674000005 -17.584860955999943, 122.18986118500004 -17.584860955999943, 122.18986118500004 -17.58513873399994, 122.1868056290001 -17.58513873399994, 122.1868056290001 -17.584860955999943, 122.18597229600005 -17.584860955999943, 122.18597229600005 -17.584583177999946, 122.18541674000005 -17.584583177999946, 122.18541674000005 -17.58430539999995, 122.18513896200011 -17.58430539999995, 122.18513896200011 -17.584583177999946, 122.18541674000005 -17.584583177999946, 122.18541674000005 -17.584860955999943, 122.1856945180001 -17.584860955999943, 122.1856945180001 -17.58986095599994, 122.18541674000005 -17.58986095599994, 122.18541674000005 -17.59097206699994, 122.18513896200011 -17.59097206699994, 122.18513896200011 -17.591249844999936, 122.18513896200011 -17.591527622999934, 122.18541674000005 -17.591527622999934, 122.1856945180001 -17.591527622999934, 122.1856945180001 -17.59180539999994, 122.18597229600005 -17.59180539999994, 122.18597229600005 -17.59208317799994, 122.18625007300011 -17.59208317799994, 122.18625007300011 -17.592360955999936, 122.18652785100005 -17.592360955999936, 122.18652785100005 -17.592638733999934, 122.1868056290001 -17.592638733999934, 122.1868056290001 -17.59291651099994, 122.18708340700005 -17.59291651099994, 122.18708340700005 -17.59430539999994, 122.18708340700005 -17.594583177999937, 122.18708340700005 -17.594860955999934, 122.1868056290001 -17.594860955999934, 122.1868056290001 -17.59513873399993, 122.1868056290001 -17.59541651099994, 122.1868056290001 -17.595694288999937, 122.1868056290001 -17.595972066999934, 122.1868056290001 -17.59624984499993, 122.18652785100005 -17.59624984499993, 122.18652785100005 -17.59652762299993, 122.18652785100005 -17.596805399999937, 122.18652785100005 -17.597083177999934, 122.18625007300011 -17.597083177999934, 122.18625007300011 -17.59736095599993, 122.18597229600005 -17.59736095599993, 122.18597229600005 -17.59763873399993, 122.18597229600005 -17.597916510999937, 122.1856945180001 -17.597916510999937, 122.1856945180001 -17.598194288999935, 122.18541674000005 -17.598194288999935, 122.18541674000005 -17.598472066999932, 122.18541674000005 -17.59874984499993, 122.18541674000005 -17.599027622999927, 122.18513896200011 -17.599027622999927, 122.18513896200011 -17.599305399999935, 122.18513896200011 -17.599583177999932, 122.18513896200011 -17.59986095599993, 122.18486118500005 -17.59986095599993, 122.18486118500005 -17.600138733999927, 122.18486118500005 -17.600416510999935, 122.18486118500005 -17.600694288999932, 122.18486118500005 -17.60097206699993, 122.1845834070001 -17.60097206699993, 122.1845834070001 -17.601249844999927, 122.18430562900005 -17.601249844999927, 122.18430562900005 -17.60152762299998, 122.18402785100011 -17.60152762299998, 122.18402785100011 -17.601805399999932, 122.18375007300006 -17.601805399999932, 122.18375007300006 -17.60208317799993, 122.1834722960001 -17.60208317799993, 122.1834722960001 -17.602360955999927, 122.18319451800005 -17.602360955999927, 122.18319451800005 -17.60263873399998, 122.18291674000011 -17.60263873399998, 122.18291674000011 -17.602916510999933, 122.18263896200006 -17.602916510999933, 122.18263896200006 -17.60319428899993, 122.1823611850001 -17.60319428899993, 122.1823611850001 -17.603472066999927, 122.18208340700005 -17.603472066999927, 122.18208340700005 -17.60374984499998, 122.18180562900011 -17.60374984499998, 122.18180562900011 -17.60402762299998, 122.18152785100006 -17.60402762299998, 122.18152785100006 -17.60430539999993, 122.18125007300011 -17.60430539999993, 122.18125007300011 -17.604583177999928, 122.18097229600005 -17.604583177999928, 122.18097229600005 -17.604860955999982, 122.18069451800011 -17.604860955999982, 122.18069451800011 -17.60513873399998, 122.18041674000006 -17.60513873399998, 122.18041674000006 -17.605416511999977, 122.18013896200011 -17.605416511999977, 122.18013896200011 -17.605694288999928, 122.17986118500005 -17.605694288999928, 122.17986118500005 -17.605972066999982, 122.17958340700011 -17.605972066999982, 122.17958340700011 -17.60624984499998, 122.17930562900005 -17.60624984499998, 122.17930562900005 -17.606527622999977, 122.17902785100011 -17.606527622999977, 122.17902785100011 -17.606805399999928, 122.17875007300006 -17.606805399999928, 122.17875007300006 -17.607083177999982, 122.17847229600011 -17.607083177999982, 122.17847229600011 -17.60736095599998, 122.17819451800005 -17.60736095599998, 122.17819451800005 -17.607638733999977, 122.17791674000011 -17.607638733999977, 122.17791674000011 -17.607916511999974, 122.17763896200006 -17.607916511999974, 122.17763896200006 -17.608194288999982, 122.17736118500011 -17.608194288999982, 122.17736118500011 -17.60847206699998, 122.17708340700005 -17.60847206699998, 122.17708340700005 -17.608749844999977, 122.17680562900011 -17.608749844999977, 122.17680562900011 -17.609027622999974, 122.17652785100006 -17.609027622999974, 122.17652785100006 -17.608749844999977, 122.17652785100006 -17.60847206699998, 122.176250073 -17.60847206699998, 122.176250073 -17.608194288999982, 122.176250073 -17.607916511999974, 122.176250073 -17.607638733999977, 122.17597229600005 -17.607638733999977, 122.17597229600005 -17.60736095599998, 122.17597229600005 -17.607083177999982, 122.17597229600005 -17.606805399999928, 122.17569451800011 -17.606805399999928, 122.17569451800011 -17.606527622999977, 122.17569451800011 -17.60624984499998, 122.17569451800011 -17.605972066999982, 122.17569451800011 -17.605694288999928, 122.175138962 -17.605694288999928, 122.175138962 -17.605416511999977, 122.17486118500005 -17.605416511999977, 122.17486118500005 -17.604860955999982, 122.17458340700011 -17.604860955999982, 122.17458340700011 -17.604583177999928, 122.17458340700011 -17.60430539999993, 122.17430562900006 -17.60430539999993, 122.17430562900006 -17.60402762299998, 122.17430562900006 -17.60374984499998, 122.17402785100012 -17.60374984499998, 122.17402785100012 -17.603472066999927, 122.17402785100012 -17.60319428899993, 122.17402785100012 -17.60208317799993, 122.17375007300006 -17.60208317799993, 122.17375007300006 -17.601805399999932, 122.17375007300006 -17.60152762299998, 122.17375007300006 -17.601249844999927, 122.17347229600011 -17.601249844999927, 122.17347229600011 -17.60097206699993, 122.17347229600011 -17.600694288999932, 122.17319451800006 -17.600694288999932, 122.17319451800006 -17.600416510999935, 122.17291674000012 -17.600416510999935, 122.17291674000012 -17.600138733999927, 122.17263896200006 -17.600138733999927, 122.17263896200006 -17.59986095599993, 122.17263896200006 -17.599583177999932, 122.17236118400001 -17.599583177999932, 122.17236118400001 -17.599305399999935, 122.17208340700006 -17.599305399999935, 122.17208340700006 -17.599027622999927, 122.17180562900012 -17.599027622999927, 122.17180562900012 -17.59874984499993, 122.17152785100006 -17.59874984499993, 122.17152785100006 -17.598472066999932, 122.17152785100006 -17.598194288999935, 122.17152785100006 -17.597916510999937, 122.17152785100006 -17.59763873399993, 122.17125007300001 -17.59763873399993, 122.17125007300001 -17.59736095599993, 122.17125007300001 -17.597083177999934, 122.17097229600006 -17.597083177999934, 122.17097229600006 -17.59652762299993, 122.17069451800012 -17.59652762299993, 122.17069451800012 -17.59624984499993, 122.17041674000006 -17.59624984499993, 122.17041674000006 -17.595972066999934, 122.17013896200001 -17.595972066999934, 122.17013896200001 -17.595694288999937, 122.16958340700012 -17.595694288999937, 122.16958340700012 -17.59541651099994, 122.16930562900006 -17.59541651099994, 122.16930562900006 -17.59513873399993, 122.16902785100001 -17.59513873399993, 122.16902785100001 -17.594860955999934, 122.16902785100001 -17.594583177999937, 122.16902785100001 -17.59430539999994, 122.16875007300007 -17.59430539999994, 122.16875007300007 -17.59402762299993, 122.16847229600012 -17.59402762299993, 122.16847229600012 -17.593749844999934, 122.16819451800006 -17.593749844999934, 122.16819451800006 -17.593472066999936, 122.16819451800006 -17.59319428899994, 122.16791674000001 -17.59319428899994, 122.16791674000001 -17.59291651099994, 122.16791674000001 -17.592638733999934, 122.16791674000001 -17.592360955999936, 122.16791674000001 -17.59208317799994, 122.16819451800006 -17.59208317799994, 122.16819451800006 -17.59180539999994, 122.16847229600012 -17.59180539999994, 122.16847229600012 -17.591527622999934, 122.16819451800006 -17.591527622999934, 122.16819451800006 -17.591249844999936, 122.16791674000001 -17.591249844999936, 122.16791674000001 -17.59097206699994, 122.16763896200007 -17.59097206699994, 122.16763896200007 -17.59069428899994, 122.16763896200007 -17.590416510999944, 122.16736118400001 -17.590416510999944, 122.16736118400001 -17.590138733999936, 122.16708340700006 -17.590138733999936, 122.16680562900001 -17.590138733999936, 122.16680562900001 -17.58986095599994, 122.16652785100007 -17.58986095599994, 122.16652785100007 -17.58958317799994, 122.16652785100007 -17.589305399999944, 122.16652785100007 -17.589027621999946, 122.16652785100007 -17.58874984499994, 122.16625007300001 -17.58874984499994, 122.16625007300001 -17.589027621999946, 122.16597229600006 -17.589027621999946, 122.16569451800001 -17.589027621999946, 122.16541674000007 -17.589027621999946, 122.16513896200001 -17.589027621999946, 122.16513896200001 -17.58874984499994, 122.16486118400007 -17.58874984499994, 122.16486118400007 -17.58847206699994, 122.16458340700001 -17.58847206699994, 122.16430562900007 -17.58847206699994, 122.16430562900007 -17.588194288999944, 122.16430562900007 -17.587916510999946, 122.16402785100001 -17.587916510999946, 122.16402785100001 -17.58763873399994, 122.16430562900007 -17.58763873399994, 122.16430562900007 -17.58736095599994, 122.16430562900007 -17.587083177999943, 122.16430562900007 -17.586805399999946, 122.16402785100001 -17.586805399999946, 122.16402785100001 -17.58652762199995, 122.16375007300007 -17.58652762199995, 122.16375007300007 -17.58624984499994, 122.16347229600001 -17.58624984499994, 122.16347229600001 -17.585972066999943, 122.16347229600001 -17.585694288999946, 122.16319451800007 -17.585694288999946, 122.16319451800007 -17.58541651099995, 122.16291674000001 -17.58541651099995, 122.16291674000001 -17.58513873399994, 122.16263896200007 -17.58513873399994, 122.16263896200007 -17.584860955999943, 122.16263896200007 -17.584583177999946, 122.16263896200007 -17.58430539999995, 122.16236118400002 -17.58430539999995, 122.16208340700007 -17.58430539999995, 122.16208340700007 -17.58402762199995, 122.16180562900001 -17.58402762199995, 122.16152785100007 -17.58402762199995, 122.16152785100007 -17.583749844999943, 122.16125007300002 -17.583749844999943, 122.16097229600007 -17.583749844999943, 122.16069451800001 -17.583749844999943, 122.16069451800001 -17.583472066999946, 122.16041674000007 -17.583472066999946, 122.16013896200002 -17.583472066999946, 122.16013896200002 -17.583194288999948, 122.15986118400008 -17.583194288999948, 122.15986118400008 -17.58291651099995, 122.15902785100002 -17.58291651099995, 122.15902785100002 -17.582638733999943, 122.15875007300008 -17.582638733999943, 122.15875007300008 -17.582360955999945, 122.15847229500002 -17.582360955999945, 122.15847229500002 -17.582638733999943, 122.15791674000002 -17.582638733999943, 122.15763896200008 -17.582638733999943, 122.15763896200008 -17.58291651099995, 122.15736118400002 -17.58291651099995, 122.15708340700007 -17.58291651099995, 122.15708340700007 -17.582638733999943, 122.15680562900002 -17.582638733999943, 122.15680562900002 -17.582360955999945, 122.15652785100008 -17.582360955999945, 122.15652785100008 -17.582083177999948, 122.15652785100008 -17.58180539999995, 122.15625007300002 -17.58180539999995, 122.15625007300002 -17.581249844999945, 122.15597229500008 -17.581249844999945, 122.15597229500008 -17.580972066999948, 122.15541674000008 -17.580972066999948, 122.15541674000008 -17.58069428899995, 122.15513896200002 -17.58069428899995, 122.15458340700002 -17.58069428899995, 122.15458340700002 -17.580416510999953, 122.15430562900008 -17.580416510999953, 122.15347229500003 -17.580416510999953, 122.15347229500003 -17.580138733999945, 122.15291674000002 -17.580138733999945, 122.15291674000002 -17.579860955999948, 122.15263896200008 -17.579860955999948, 122.15263896200008 -17.57958317799995, 122.15236118400003 -17.57958317799995, 122.15236118400003 -17.579305399999953, 122.15208340700008 -17.579305399999953, 122.15208340700008 -17.578749844999948, 122.15180562900002 -17.578749844999948, 122.15180562900002 -17.57847206699995, 122.15152785100008 -17.57847206699995, 122.15152785100008 -17.577916510999955, 122.15125007300003 -17.577916510999955, 122.15125007300003 -17.57513873299996, 122.15125007300003 -17.574860955999952, 122.15125007300003 -17.574583177999955, 122.15125007300003 -17.574305399999957, 122.15125007300003 -17.57180539999996, 122.15097229500009 -17.57180539999996, 122.15097229500009 -17.57069428899996, 122.15069451800002 -17.57069428899996, 122.15069451800002 -17.56958317799996, 122.15041674000008 -17.56958317799996, 122.15041674000008 -17.569305399999962, 122.15041674000008 -17.568749844999957, 122.15013896200003 -17.568749844999957, 122.15013896200003 -17.567916510999964, 122.14986118400009 -17.567916510999964, 122.14986118400009 -17.56736095599996, 122.14958340700002 -17.56736095599996, 122.14958340700002 -17.566805399999964, 122.14930562900008 -17.566805399999964, 122.14930562900008 -17.566527621999967, 122.14902785100003 -17.566527621999967, 122.14902785100003 -17.56597206699996, 122.14875007300009 -17.56597206699996, 122.14875007300009 -17.565694288999964, 122.14847229500003 -17.565694288999964, 122.14847229500003 -17.56513873299997, 122.14819451800008 -17.56513873299997, 122.14819451800008 -17.56486095599996, 122.14763896200009 -17.56486095599996, 122.14763896200009 -17.564583177999964, 122.14736118400003 -17.564583177999964, 122.14736118400003 -17.564305399999967, 122.14708340700008 -17.564305399999967, 122.14708340700008 -17.56402762199997, 122.14680562900003 -17.56402762199997, 122.14680562900003 -17.56374984499996, 122.14652785100009 -17.56374984499996, 122.14652785100009 -17.563472066999964, 122.14625007300003 -17.563472066999964, 122.14625007300003 -17.563194288999966, 122.14625007300003 -17.56291651099997, 122.14625007300003 -17.56263873299997, 122.14625007300003 -17.562360955999964, 122.14597229500009 -17.562360955999964, 122.14597229500009 -17.56180539999997, 122.14569451800003 -17.56180539999997, 122.14569451800003 -17.559860955999966, 122.14597229500009 -17.559860955999966, 122.14597229500009 -17.55958317799997, 122.14597229500009 -17.559027621999974, 122.14597229500009 -17.558749843999976, 122.14625007300003 -17.558749843999976, 122.14625007300003 -17.55847206699997, 122.14625007300003 -17.55819428899997, 122.14652785100009 -17.55819428899997, 122.14652785100009 -17.557916510999974, 122.14680562900003 -17.557916510999974, 122.14680562900003 -17.556805399999973, 122.14708340700008 -17.556805399999973, 122.14708340700008 -17.550972066999975, 122.15013896200003 -17.550972066999975, 122.15013896200003 -17.551249843999926, 122.15041674000008 -17.551249843999926, 122.15041674000008 -17.550694288999978, 122.15069451800002 -17.550694288999978, 122.15069451800002 -17.549583177999978, 122.15097229500009 -17.549583177999978, 122.15097229500009 -17.54874984399993, 122.15125007300003 -17.54874984399993, 122.15125007300003 -17.547916510999983, 122.15152785100008 -17.547916510999983, 122.15152785100008 -17.545416510999928, 122.15125007300003 -17.545416510999928, 122.15125007300003 -17.54402762199993, 122.15097229500009 -17.54402762199993, 122.15097229500009 -17.539027621999935, 122.15541674000008 -17.539027621999935, 122.15541674000008 -17.538749843999938, 122.15736118400002 -17.538749843999938, 122.15736118400002 -17.53847206699993, 122.16486118400007 -17.53847206699993, 122.16486118400007 -17.538749843999938, 122.16513896200001 -17.538749843999938, 122.16513896200001 -17.539027621999935, 122.16569451800001 -17.539027621999935, 122.16569451800001 -17.539305399999932, 122.16597229600006 -17.539305399999932, 122.16597229600006 -17.53958317799993, 122.16680562900001 -17.53958317799993, 122.16680562900001 -17.539860954999938, 122.175138962 -17.539860954999938, 122.175138962 -17.53958317799993, 122.17597229600005 -17.53958317799993, 122.17597229600005 -17.539305399999932, 122.17652785100006 -17.539305399999932, 122.17652785100006 -17.539027621999935, 122.17708340700005 -17.539027621999935, 122.17708340700005 -17.538749843999938, 122.17986118500005 -17.538749843999938, 122.17986118500005 -17.539027621999935, 122.18013896200011 -17.539027621999935, 122.18013896200011 -17.539305399999932, 122.18041674000006 -17.539305399999932, 122.18041674000006 -17.53958317799993, 122.18097229600005 -17.53958317799993, 122.18097229600005 -17.539860954999938, 122.18125007300011 -17.539860954999938, 122.18125007300011 -17.540138732999935, 122.18180562900011 -17.540138732999935, 122.18180562900011 -17.540416510999933, 122.1823611850001 -17.540416510999933, 122.1823611850001 -17.54069428899993, 122.18291674000011 -17.54069428899993, 122.18291674000011 -17.540972066999927, 122.1834722960001 -17.540972066999927, 122.1834722960001 -17.541249843999935, 122.18402785100011 -17.541249843999935, 122.18402785100011 -17.541527621999933, 122.18430562900005 -17.541527621999933, 122.18430562900005 -17.54180539999993, 122.1845834070001 -17.54180539999993, 122.1845834070001 -17.542360954999936, 122.18486118500005 -17.542360954999936, 122.18486118500005 -17.542638732999933, 122.18513896200011 -17.542638732999933, 122.18513896200011 -17.54291651099993, 122.18541674000005 -17.54291651099993, 122.18541674000005 -17.543472066999982, 122.1856945180001 -17.543472066999982, 122.1856945180001 -17.543749843999933, 122.18597229600005 -17.543749843999933, 122.18597229600005 -17.54402762199993, 122.18625007300011 -17.54402762199993, 122.18625007300011 -17.544305399999928, 122.18652785100005 -17.544305399999928, 122.18652785100005 -17.544583177999982, 122.18708340700005 -17.544583177999982, 122.18708340700005 -17.544860954999933, 122.1879167400001 -17.544860954999933, 122.1879167400001 -17.54513873299993, 122.1884722960001 -17.54513873299993, 122.1884722960001 -17.545416510999928, 122.1890278510001 -17.545416510999928, 122.1890278510001 -17.545694288999982, 122.18986118500004 -17.545694288999982, 122.18986118500004 -17.54597206699998, 122.19986118500003 -17.54597206699998, 122.19986118500003 -17.54624984399993, 122.20041674000004 -17.54624984399993, 122.20041674000004 -17.546527621999928, 122.20069451800009 -17.546527621999928, 122.20069451800009 -17.546805399999982, 122.20097229600003 -17.546805399999982, 122.20097229600003 -17.54708317799998, 122.20125007400009 -17.54708317799998, 122.20125007400009 -17.54736095499993, 122.20152785100004 -17.54736095499993, 122.20152785100004 -17.54763873299993, 122.20180562900009 -17.54763873299993, 122.20180562900009 -17.547916510999983, 122.20208340700003 -17.547916510999983, 122.20208340700003 -17.54819428899998, 122.20236118500009 -17.54819428899998, 122.20236118500009 -17.548472066999977, 122.20375007400003 -17.548472066999977, 122.20375007400003 -17.54874984399993, 122.20458340700009 -17.54874984399993, 122.20458340700009 -17.549027621999983, 122.20513896300008 -17.549027621999983, 122.20513896300008 -17.54930539999998, 122.20541674000003 -17.54930539999998, 122.20541674000003 -17.549583177999978, 122.20597229600003 -17.549583177999978, 122.20597229600003 -17.549860955999975, 122.20625007400008 -17.549860955999975, 122.20625007400008 -17.550138732999926, 122.20652785100003 -17.550138732999926, 122.20652785100003 -17.55041651099998, 122.20680562900009 -17.55041651099998, 122.20680562900009 -17.550694288999978, 122.20708340700003 -17.550694288999978, 122.20708340700003 -17.550972066999975, 122.20736118500008 -17.550972066999975, 122.20736118500008 -17.551249843999926, 122.20763896300002 -17.551249843999926, 122.20763896300002 -17.55152762199998, 122.20791674000009 -17.55152762199998, 122.20791674000009 -17.551805399999978, 122.20819451800003 -17.551805399999978, 122.20819451800003 -17.552083177999975, 122.20847229600008 -17.552083177999975, 122.20847229600008 -17.552360955999973, 122.20875007400002 -17.552360955999973, 122.20875007400002 -17.55263873299998, 122.20902785100009 -17.55263873299998, 122.20902785100009 -17.552916510999978, 122.20930562900003 -17.552916510999978, 122.20930562900003 -17.553194288999975, 122.20958340700008 -17.553194288999975, 122.20958340700008 -17.553472066999973, 122.21013896300008 -17.553472066999973, 122.21013896300008 -17.55374984399998, 122.21041674000003 -17.55374984399998, 122.21041674000003 -17.55402762199998, 122.21069451800008 -17.55402762199998, 122.21069451800008 -17.554305399999976, 122.21097229600002 -17.554305399999976, 122.21097229600002 -17.554583177999973, 122.21125007400008 -17.554583177999973, 122.21125007400008 -17.55486095599997, 122.21152785100003 -17.55486095599997, 122.21152785100003 -17.55513873299998, 122.21180562900008 -17.55513873299998, 122.21180562900008 -17.555416510999976, 122.21208340700002 -17.555416510999976, 122.21208340700002 -17.555694288999973, 122.21236118500008 -17.555694288999973, 122.21236118500008 -17.55597206699997, 122.21319451800002 -17.55597206699997, 122.21319451800002 -17.55624984399998, 122.21402785100008 -17.55624984399998, 122.21402785100008 -17.556527621999976, 122.21486118500002 -17.556527621999976, 122.21486118500002 -17.556805399999973, 122.21541674000002 -17.556805399999973, 122.21541674000002 -17.55708317799997, 122.21569451800008 -17.55708317799997, 122.21569451800008 -17.557360955999968, 122.21625007400007 -17.557360955999968, 122.21625007400007 -17.557638732999976, 122.21680562900008 -17.557638732999976, 122.21680562900008 -17.557916510999974, 122.21736118500007 -17.557916510999974, 122.21736118500007 -17.55819428899997, 122.21791674000008 -17.55819428899997, 122.21791674000008 -17.55847206699997, 122.21819451800002 -17.55847206699997, 122.21819451800002 -17.558749843999976, 122.21847229600007 -17.558749843999976, 122.21847229600007 -17.559027621999974, 122.21875007400001 -17.559027621999974, 122.21875007400001 -17.55930539999997, 122.21930562900002 -17.55930539999997, 122.21930562900002 -17.55958317799997, 122.21958340700007 -17.55958317799997, 122.21958340700007 -17.559860955999966, 122.22041674000002 -17.559860955999966, 122.22041674000002 -17.560138732999974, 122.22097229600001 -17.560138732999974, 122.22097229600001 -17.56041651099997, 122.22152785200001 -17.56041651099997, 122.22152785200001 -17.56069428899997, 122.22180562900007 -17.56069428899997, 122.22180562900007 -17.560972066999966, 122.22208340700001 -17.560972066999966, 122.22208340700001 -17.561249843999974, 122.22236118500007 -17.561249843999974, 122.22236118500007 -17.56152762199997, 122.22263896300001 -17.56152762199997, 122.22263896300001 -17.56180539999997, 122.22291674000007 -17.56180539999997, 122.22291674000007 -17.562083177999966, 122.22319451800001 -17.562083177999966, 122.22319451800001 -17.562360955999964, 122.22347229600007 -17.562360955999964, 122.22347229600007 -17.56263873299997, 122.22375007400001 -17.56263873299997, 122.22375007400001 -17.56291651099997, 122.22402785200006 -17.56291651099997, 122.22402785200006 -17.563194288999966, 122.22430562900001 -17.563194288999966, 122.22430562900001 -17.563472066999964, 122.23097229600012 -17.563472066999964, 122.23097229600012 -17.56374984499996, 122.23125007400006 -17.56374984499996, 122.23125007400006 -17.56402762199997)))"^^geo:wktLiteral ] ; + geo:hasMetricArea 3.455107e+07 ; + ns1:link "/s/datasets/ldgovau:geofabric/collections/fc:catchments/items/hydrd:102208962" . diff --git a/tests/object/test_endpoints_object.py b/tests/object/test_endpoints_object.py index 697b681b..eef95d0c 100644 --- a/tests/object/test_endpoints_object.py +++ b/tests/object/test_endpoints_object.py @@ -40,7 +40,29 @@ def dataset_uri(test_client): return g.value(None, RDF.type, DCAT.Dataset) -def test_object_endpoint(test_client, dataset_uri): +def test_object_endpoint_sp_dataset(test_client, dataset_uri): with test_client as client: r = client.get(f"/object?uri={dataset_uri}") assert r.status_code == 200 + + +def test_feature_collection(test_client): + with test_client as client: + r = client.get(f"/object?uri=https://test/feature-collection") + response_graph = Graph().parse(data=r.text) + expected_graph = Graph().parse( + Path(__file__).parent / "../data/object/expected_responses/fc.ttl" + ) + assert response_graph.isomorphic(expected_graph) + + +def test_feature(test_client): + with test_client as client: + r = client.get( + f"/object?uri=https://linked.data.gov.au/datasets/geofabric/hydroid/102208962" + ) + response_graph = Graph().parse(data=r.text) + expected_graph = Graph().parse( + Path(__file__).parent / "../data/object/expected_responses/feature.ttl" + ) + assert response_graph.isomorphic(expected_graph)