Skip to content

Commit e22c52a

Browse files
committed
feat: add SPARQL service
1 parent a0c2150 commit e22c52a

File tree

3 files changed

+190
-1
lines changed

3 files changed

+190
-1
lines changed

pdm.lock

Lines changed: 54 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies = [
1212
"pydantic>=2.7.1",
1313
"litestar[standard]>=2.8.3",
1414
"advanced-alchemy>=0.11.0",
15+
"sparqlwrapper>=2.0.0",
1516
]
1617
requires-python = "==3.11.*"
1718
readme = "README.md"

src/services/sparql.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from collections.abc import Mapping, Sequence
2+
from typing import Any, Literal, cast
3+
4+
from SPARQLWrapper import JSON, SPARQLWrapper
5+
6+
URL = "https://demo.vocabs.ardc.edu.au/repository/api/sparql/appf_appf-test-project-2_appf-test-project-2-v0-1"
7+
8+
SKOS_CONCEPT_PROPERTY = Literal[
9+
"altLabel",
10+
"hiddenLabel",
11+
"definition",
12+
"notation",
13+
"example",
14+
"scopeNote",
15+
"broader",
16+
"narrower",
17+
"related",
18+
"exactMatch",
19+
"closeMatch",
20+
"broaderMatch",
21+
"relatedMatch",
22+
]
23+
24+
ALL_PROPERTIES = [
25+
"altLabel",
26+
"hiddenLabel",
27+
"definition",
28+
"notation",
29+
"example",
30+
"scopeNote",
31+
"broader",
32+
"narrower",
33+
"related",
34+
"exactMatch",
35+
"closeMatch",
36+
"broaderMatch",
37+
"relatedMatch",
38+
]
39+
40+
41+
class QueryFactory:
42+
@staticmethod
43+
def concept_by_uri(uri: str) -> str:
44+
stmt = ""
45+
stmt += "PREFIX dcterms:<http://purl.org/dc/terms/>\n"
46+
stmt += "PREFIX skos:<http://www.w3.org/2004/02/skos/core#>\n\n"
47+
stmt += "SELECT * \n\n"
48+
stmt += "WHERE {{\n"
49+
for prop in ALL_PROPERTIES:
50+
stmt += f" OPTIONAL{{?concept skos:{prop} ?{prop}}} .\n"
51+
stmt += " ?concept skos:prefLabel ?prefLabel .\n"
52+
stmt += " ?concept skos:inScheme ?scheme .\n"
53+
stmt += " ?scheme dcterms:title ?schemeTitle .\n"
54+
stmt += f" FILTER(?concept = <{uri}>) .\n"
55+
stmt += "}}\n\n"
56+
return stmt
57+
58+
@staticmethod
59+
def all_leaf_concepts(scheme: str, limit: int | None = None, offset: int | None = None) -> str:
60+
return QueryFactory.leaf_concept(
61+
*ALL_PROPERTIES, # type: ignore[arg-type]
62+
scheme=scheme,
63+
limit=limit,
64+
offset=offset,
65+
)
66+
67+
@staticmethod
68+
def leaf_concept(
69+
*properties: SKOS_CONCEPT_PROPERTY,
70+
scheme: str,
71+
pref_label: str | None = None,
72+
limit: int | None = None,
73+
offset: int | None = None,
74+
) -> str:
75+
stmt = ""
76+
stmt += "PREFIX dcterms:<http://purl.org/dc/terms/>\n"
77+
stmt += "PREFIX skos:<http://www.w3.org/2004/02/skos/core#>\n\n"
78+
stmt += "SELECT * \n\n"
79+
stmt += "WHERE {{\n"
80+
for prop in properties:
81+
stmt += f" OPTIONAL{{?concept skos:{prop} ?{prop}}} .\n"
82+
stmt += " OPTIONAL{{?concept skos:narrower ?narrower}}\n"
83+
stmt += " FILTER(!BOUND(?narrower)) .\n"
84+
stmt += " ?concept skos:prefLabel ?prefLabel .\n"
85+
if pref_label:
86+
stmt += f' FILTER(REGEX(?prefLabel, "{pref_label}", "i" )) .\n'
87+
stmt += " ?concept skos:inScheme ?scheme .\n"
88+
stmt += " ?scheme dcterms:title ?schemeTitle .\n"
89+
stmt += f' FILTER(REGEX(?schemeTitle, "{scheme}", "i" )) .\n'
90+
stmt += "}}\n\n"
91+
if limit:
92+
stmt += f"LIMIT {limit}\n\n"
93+
if offset:
94+
stmt += f"OFFSET {offset}\n\n"
95+
return stmt
96+
97+
98+
class SPARQLService:
99+
def __init__(self, url: str = URL) -> None:
100+
self.url = url
101+
self._service = SPARQLWrapper(endpoint=url)
102+
self._service.setReturnFormat(JSON)
103+
self._last_stmt = ""
104+
105+
@property
106+
def last_stmt(self) -> str:
107+
return self._last_stmt
108+
109+
def _execute_query(self, query: str) -> Sequence[Mapping[SKOS_CONCEPT_PROPERTY, Any]]:
110+
self._service.setQuery(query)
111+
self._last_stmt = query
112+
try:
113+
ret = cast(dict[str, Any], self._service.queryAndConvert())
114+
rows = cast(list[dict[str, Any]], ret["results"]["bindings"])
115+
rows = [{key: value.get("value", None) for key, value in row.items()} for row in rows]
116+
return cast(Sequence[Mapping[SKOS_CONCEPT_PROPERTY, Any]], rows)
117+
118+
except Exception as e:
119+
raise e
120+
121+
def get_leaf_concepts(
122+
self, scheme: str, limit: int | None = None, offset: int | None = None, props: list[SKOS_CONCEPT_PROPERTY] = []
123+
) -> Sequence[Mapping[SKOS_CONCEPT_PROPERTY, Any]]:
124+
stmt = QueryFactory.all_leaf_concepts(*props, scheme=scheme, limit=limit, offset=offset) # type: ignore
125+
return self._execute_query(stmt)
126+
127+
def get_concept_by_id(self, uri: str) -> Sequence[Mapping[SKOS_CONCEPT_PROPERTY, Any]]:
128+
stmt = QueryFactory.concept_by_uri(uri)
129+
return self._execute_query(stmt)
130+
131+
def get_concept_by_pref_label(
132+
self, scheme: str, label: str, limit: int | None = None, offset: int | None = None
133+
) -> Sequence[Mapping[SKOS_CONCEPT_PROPERTY, Any]]:
134+
stmt = QueryFactory.leaf_concept(scheme=scheme, pref_label=label, limit=limit, offset=offset)
135+
return self._execute_query(stmt)

0 commit comments

Comments
 (0)