Coverage for src / time_agnostic_library / sparql.py: 100%
179 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-06-12 21:46 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-06-12 21:46 +0000
1# SPDX-FileCopyrightText: 2021-2026 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
6import atexit
7import threading
8import zipfile
10from rdflib import Dataset
11from rdflib.term import Literal, URIRef
12from sparqlite import SPARQLClient
14from time_agnostic_library.prov_entity import ProvEntity
16__all__ = [
17 "Sparql",
18 "_binding_to_n3",
19 "_n3_to_binding",
20 "_n3_value",
21]
23CONFIG_PATH = "./config.json"
25_PROV_PROPERTY_STRINGS: tuple[str, ...] = tuple(ProvEntity.get_prov_properties())
27_client_cache: dict[tuple[str, int], SPARQLClient] = {}
28_client_lock = threading.Lock()
31def _get_client(url: str) -> SPARQLClient:
32 key = (url, threading.get_ident())
33 with _client_lock:
34 client = _client_cache.get(key)
35 if client is None:
36 client = SPARQLClient(url)
37 _client_cache[key] = client
38 return client
41def _close_all_clients() -> None:
42 with _client_lock:
43 for client in _client_cache.values():
44 client.close()
45 _client_cache.clear()
48atexit.register(_close_all_clients)
51def _escape_n3(v: str) -> str:
52 return (
53 v.replace("\\", "\\\\")
54 .replace('"', '\\"')
55 .replace("\n", "\\n")
56 .replace("\r", "\\r")
57 )
60def _binding_to_n3(val: dict) -> str:
61 if val["type"] == "uri":
62 return f"<{val['value']}>"
63 if val["type"] == "bnode":
64 return f"_:{val['value']}"
65 escaped = _escape_n3(val["value"])
66 if "datatype" in val:
67 return f'"{escaped}"^^<{val["datatype"]}>'
68 if "xml:lang" in val:
69 return f'"{escaped}"@{val["xml:lang"]}'
70 return f'"{escaped}"'
73def _find_closing_quote(n3: str) -> int:
74 pos = n3.find('"', 1)
75 while pos > 0:
76 num_backslashes = 0
77 check = pos - 1
78 while check >= 1 and n3[check] == "\\":
79 num_backslashes += 1
80 check -= 1
81 if num_backslashes % 2 == 0:
82 return pos
83 pos = n3.find('"', pos + 1)
84 return -1
87def _unescape_n3(raw: str) -> str:
88 out: list[str] = []
89 i = 0
90 while i < len(raw):
91 if raw[i] == "\\" and i + 1 < len(raw):
92 nxt = raw[i + 1]
93 if nxt == "n":
94 out.append("\n")
95 elif nxt == "r":
96 out.append("\r")
97 elif nxt == '"':
98 out.append('"')
99 elif nxt == "\\":
100 out.append("\\")
101 else:
102 out.append(raw[i])
103 out.append(nxt)
104 i += 2
105 else:
106 out.append(raw[i])
107 i += 1
108 return "".join(out)
111def _parse_n3_literal(n3: str) -> tuple[str, str]:
112 quote_end = _find_closing_quote(n3)
113 if quote_end == -1:
114 return n3, ""
115 raw = n3[1:quote_end]
116 return _unescape_n3(raw), n3[quote_end + 1 :]
119def _n3_value(n3: str) -> str:
120 if n3.startswith("<") and n3.endswith(">"):
121 return n3[1:-1]
122 if n3.startswith("_:"):
123 return n3[2:]
124 value, _ = _parse_n3_literal(n3)
125 return value
128def _n3_to_binding(n3: str) -> dict:
129 if n3.startswith("<") and n3.endswith(">"):
130 return {"type": "uri", "value": n3[1:-1]}
131 if n3.startswith("_:"):
132 return {"type": "bnode", "value": n3[2:]}
133 value, rest = _parse_n3_literal(n3)
134 if rest.startswith("^^<") and rest.endswith(">"):
135 return {"type": "literal", "value": value, "datatype": rest[3:-1]}
136 if rest.startswith("@"):
137 return {"type": "literal", "value": value, "xml:lang": rest[1:]}
138 return {"type": "literal", "value": value}
141class Sparql:
142 def __init__(self, query: str, config: dict):
143 self.query = query
144 self.config = config
145 if any(uri in query for uri in _PROV_PROPERTY_STRINGS):
146 self.storer: dict = config["provenance"]
147 else:
148 self.storer: dict = config["dataset"]
150 def run_select_query(self) -> dict:
151 output = {"head": {"vars": []}, "results": {"bindings": []}}
152 if self.storer["file_paths"]:
153 output = self._get_results_from_files(output)
154 if self.storer["triplestore_urls"]:
155 output = self._get_results_from_triplestores(output)
156 return output
158 def _get_results_from_files(self, output: dict) -> dict:
159 storer: list[str] = self.storer["file_paths"]
160 for file_path in storer:
161 file_cg = Dataset(default_union=True)
162 if file_path.endswith(".zip"):
163 with (
164 zipfile.ZipFile(file_path, "r") as z,
165 z.open(z.namelist()[0]) as file,
166 ):
167 file_cg.parse(file=file, format="json-ld") # type: ignore[arg-type]
168 else:
169 file_cg.parse(location=file_path, format="json-ld")
170 query_results = file_cg.query(self.query)
171 vars_list = [str(var) for var in query_results.vars or []]
172 output["head"]["vars"] = vars_list
173 for result in query_results:
174 binding = {}
175 for var in vars_list:
176 value = result[var] # type: ignore[index]
177 if value is not None:
178 binding[var] = self._format_result_value(value)
179 output["results"]["bindings"].append(binding)
180 return output
182 def _get_results_from_triplestores(self, output: dict) -> dict:
183 storer = self.storer["triplestore_urls"]
184 for url in storer:
185 results = _get_client(url).query(self.query)
186 if not output["head"]["vars"]:
187 output["head"]["vars"] = results["head"]["vars"]
188 output["results"]["bindings"].extend(results["results"]["bindings"])
189 return output
191 @staticmethod
192 def _format_result_value(value) -> dict:
193 if isinstance(value, URIRef):
194 return {"type": "uri", "value": str(value)}
195 if isinstance(value, Literal):
196 result = {"type": "literal", "value": str(value)}
197 if value.datatype:
198 result["datatype"] = str(value.datatype)
199 if value.language:
200 result["xml:lang"] = value.language
201 return result
202 return {"type": "literal", "value": str(value)}
204 def run_select_to_quad_set(self) -> set[tuple[str, ...]]:
205 results = self.run_select_query()
206 output: set[tuple[str, ...]] = set()
207 vars_list = results["head"]["vars"]
208 for binding in results["results"]["bindings"]:
209 components: list[str] = []
210 skip = False
211 for var in vars_list:
212 if var not in binding:
213 skip = True
214 break
215 components.append(_binding_to_n3(binding[var]))
216 if not skip:
217 output.add(tuple(components))
218 return output
220 def run_ask_query(self) -> bool:
221 storer = self.storer["triplestore_urls"]
222 for url in storer:
223 return _get_client(url).ask(self.query)
224 return False
226 @classmethod
227 def _get_tuples_set(cls, result_dict: dict, output: set, vars_list: list) -> None:
228 results_list = []
229 for var in vars_list:
230 if str(var) in result_dict:
231 val = result_dict[str(var)]
232 if isinstance(val, dict) and "value" in val:
233 results_list.append(str(val["value"]))
234 else:
235 results_list.append(str(val))
236 else:
237 results_list.append(None)
238 output.add(tuple(results_list))