Skip to content

Commit

Permalink
Merge pull request #140 from RDFLib/edmond/feat/vocabs-all
Browse files Browse the repository at this point in the history
/count and vocabs-all endpoint
  • Loading branch information
edmondchuc authored Aug 1, 2023
2 parents 4efcde9 + 23b63fa commit e75091a
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 4 deletions.
3 changes: 1 addition & 2 deletions prez/models/vocprez_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ 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")
Expand All @@ -38,7 +37,7 @@ def populate(cls, values):
return values
if url_path in ["/object", "/v/object"]:
values["link_constructor"] = f"/v/object?uri="
elif len(url_parts) == 5: # concepts
elif len(url_parts) == 5 and "/all" not in url_path: # concepts
values["general_class"] = SKOS.Concept
if scheme_curie:
values["curie_id"] = concept_curie
Expand Down
33 changes: 33 additions & 0 deletions prez/queries/object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from textwrap import dedent

from jinja2 import Template


def object_inbound_query(iri: str, predicate: str) -> str:
query = Template(
"""
SELECT (COUNT(?iri) as ?count)
WHERE {
BIND(<{{ iri }}> as ?iri)
?other <{{ predicate }}> ?iri .
}
"""
).render(iri=iri, predicate=predicate)

return dedent(query)


def object_outbound_query(iri: str, predicate: str) -> str:
query = Template(
"""
SELECT (COUNT(?iri) as ?count)
WHERE {
BIND(<{{ iri }}> as ?iri)
?iri <{{ predicate }}> ?other .
}
"""
).render(iri=iri, predicate=predicate)

return dedent(query)
64 changes: 62 additions & 2 deletions prez/routers/object.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,69 @@
from fastapi import APIRouter, Request
from fastapi import APIRouter, Request, HTTPException, status, Query
from starlette.responses import PlainTextResponse

from prez.models import SpatialItem, VocabItem, CatalogItem
from prez.routers.curie import get_iri_route
from prez.sparql.methods import sparql_query_non_async
from prez.queries.object import object_inbound_query, object_outbound_query

router = APIRouter(tags=["Object"])


@router.get("/count", summary="Get object's statement count")
def count_route(
curie: str,
inbound: str = Query(
None,
examples={
"skos:inScheme": {
"summary": "skos:inScheme",
"value": "http://www.w3.org/2004/02/skos/core#inScheme",
},
"skos:topConceptOf": {
"summary": "skos:topConceptOf",
"value": "http://www.w3.org/2004/02/skos/core#topConceptOf",
},
"empty": {"summary": "Empty", "value": None},
},
),
outbound: str = Query(
None,
examples={
"empty": {"summary": "Empty", "value": None},
"skos:hasTopConcept": {
"summary": "skos:hasTopConcept",
"value": "http://www.w3.org/2004/02/skos/core#hasTopConcept",
},
},
),
):
"""Get an Object's statements count based on the inbound or outbound predicate"""
iri = get_iri_route(curie)

if inbound is None and outbound is None:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"At least 'inbound' or 'outbound' is supplied a valid IRI.",
)

if inbound and outbound:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
"Only provide one value for either 'inbound' or 'outbound', not both.",
)

if inbound is not None:
query = object_inbound_query(iri, inbound)
_, rows = sparql_query_non_async(query)
for row in rows:
return row["count"]["value"]

query = object_outbound_query(iri, outbound)
_, rows = sparql_query_non_async(query)
for row in rows:
return row["count"]["value"]


@router.get("/object", summary="Object")
async def object(
request: Request,
Expand All @@ -29,7 +87,9 @@ async def object(
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
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(
Expand Down
11 changes: 11 additions & 0 deletions prez/routers/vocprez.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ async def schemes_endpoint(
)


@router.get(
"/v/vocab/{scheme_curie}/all", summary="Get Concept Scheme and all its concepts"
)
async def vocprez_scheme(request: Request, scheme_curie: str):
"""Get a SKOS Concept Scheme and all of its concepts.
Note: This may be a very expensive operation depending on the size of the concept scheme.
"""
return await item_endpoint(request)


@router.get(
"/v/vocab/{concept_scheme_curie}",
summary="Get a SKOS Concept Scheme",
Expand Down
73 changes: 73 additions & 0 deletions tests/object/test_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import os
import subprocess
from time import sleep

import pytest
from fastapi.testclient import TestClient

PREZ_DIR = os.getenv("PREZ_DIR")
LOCAL_SPARQL_STORE = os.getenv("LOCAL_SPARQL_STORE")


@pytest.fixture(scope="module")
def test_client(request):
print("Run Local SPARQL Store")
p1 = subprocess.Popen(["python", str(LOCAL_SPARQL_STORE), "-p", "3031"])
sleep(1)

def teardown():
print("\nDoing teardown")
p1.kill()

request.addfinalizer(teardown)

# must only import app after config.py has been altered above so config is retained
from prez.app import app

return TestClient(app)


def get_curie(test_client: TestClient, iri: str) -> str:
with test_client as client:
response = client.get(f"/identifier/curie/{iri}")
if response.status_code != 200:
raise ValueError(f"Failed to retrieve curie for {iri}. {response.text}")
return response.text


@pytest.mark.parametrize(
"iri, inbound, outbound, count",
[
[
"http://linked.data.gov.au/def/borehole-purpose",
"http://www.w3.org/2004/02/skos/core#inScheme",
None,
0,
],
[
"http://linked.data.gov.au/def/borehole-purpose-no-children",
"http://www.w3.org/2004/02/skos/core#inScheme",
None,
0,
],
[
"http://linked.data.gov.au/def/borehole-purpose",
None,
"http://www.w3.org/2004/02/skos/core#hasTopConcept",
0,
],
],
)
def test_count(
test_client: TestClient,
iri: str,
inbound: str | None,
outbound: str | None,
count: int,
):
curie = get_curie(test_client, iri)

with test_client as client:
params = {"curie": curie, "inbound": inbound, "outbound": outbound}
response = client.get(f"/count", params=params)
assert int(response.text) == count

0 comments on commit e75091a

Please sign in to comment.