Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

/count and vocabs-all endpoint #140

Merged
merged 7 commits into from
Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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