Coverage for oc_meta / lib / sparql.py: 75%
92 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-25 10:39 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-07-25 10:39 +0000
1# SPDX-FileCopyrightText: 2026 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
5from __future__ import annotations
7import multiprocessing
8import time
9from concurrent.futures import ProcessPoolExecutor, as_completed
10from typing import Callable
11from urllib.error import URLError
12from urllib.parse import parse_qs, urlparse
14from SPARQLWrapper import GET, JSON, POST, URLENCODED, SPARQLWrapper
15from SPARQLWrapper.SPARQLExceptions import EndPointInternalError, QueryBadFormed
17from oc_meta.constants import QLEVER_MAX_WORKERS, QLEVER_QUERIES_PER_GROUP
20def _make_sparql_client(endpoint_url: str, timeout: int = 3600) -> SPARQLWrapper:
21 parsed = urlparse(endpoint_url)
22 base_url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
23 sparql = SPARQLWrapper(base_url)
24 for key, values in parse_qs(parsed.query).items():
25 sparql.addParameter(key, values[0])
26 sparql.setReturnFormat(JSON)
27 sparql.setTimeout(timeout)
28 return sparql
31def execute_sparql(
32 endpoint_url: str,
33 query: str,
34 max_retries: int = 5,
35 backoff_factor: float = 5,
36 timeout: int = 3600,
37 *,
38 method: str = GET,
39) -> dict:
40 sparql = _make_sparql_client(endpoint_url, timeout)
41 sparql.setMethod(method)
42 if method == POST:
43 sparql.setRequestMethod(URLENCODED)
44 last_error: Exception | None = None
45 for attempt in range(max_retries + 1):
46 if attempt > 0:
47 time.sleep(backoff_factor * (2**attempt))
48 try:
49 sparql.setQuery(query)
50 return sparql.queryAndConvert() # type: ignore[return-value]
51 except QueryBadFormed:
52 raise
53 except (EndPointInternalError, URLError) as e:
54 last_error = e
55 raise last_error # type: ignore[misc]
58def execute_sparql_update(
59 endpoint_url: str,
60 query: str,
61 max_retries: int = 5,
62 backoff_factor: float = 5,
63 timeout: int = 3600,
64) -> None:
65 sparql = _make_sparql_client(endpoint_url, timeout)
66 sparql.setMethod(POST)
67 last_error: Exception | None = None
68 for attempt in range(max_retries + 1):
69 if attempt > 0:
70 time.sleep(backoff_factor * (2**attempt))
71 try:
72 sparql.setQuery(query)
73 sparql.query()
74 return
75 except QueryBadFormed:
76 raise
77 except (EndPointInternalError, URLError) as e:
78 last_error = e
79 raise last_error # type: ignore[misc]
82def execute_sparql_queries(
83 endpoint_url: str,
84 queries: list[str],
85 max_retries: int = 5,
86 backoff_factor: float = 5,
87 timeout: int = 3600,
88) -> list[list[dict[str, dict[str, str]]]]:
89 results: list[list[dict[str, dict[str, str]]]] = []
90 sparql = _make_sparql_client(endpoint_url, timeout)
91 for query in queries:
92 last_error: Exception | None = None
93 for attempt in range(max_retries + 1):
94 if attempt > 0:
95 time.sleep(backoff_factor * (2**attempt))
96 try:
97 sparql.setQuery(query)
98 result: dict[str, dict[str, list[dict[str, dict[str, str]]]]] = (
99 sparql.queryAndConvert()
100 ) # type: ignore[assignment]
101 results.append(result["results"]["bindings"])
102 break
103 except QueryBadFormed:
104 raise
105 except (EndPointInternalError, URLError) as e:
106 last_error = e
107 else:
108 raise last_error # type: ignore[misc]
109 return results
112def run_queries_parallel(
113 endpoint_url: str,
114 batch_queries: list[str],
115 batch_sizes: list[int],
116 workers: int = QLEVER_MAX_WORKERS,
117 progress_callback: Callable[[int], None] | None = None,
118 max_retries: int = 5,
119 backoff_factor: int = 5,
120 timeout: int = 3600,
121) -> list[list]:
122 if not batch_queries:
123 return []
125 all_bindings: list[list] = []
127 if len(batch_queries) > 1 and workers > 1:
128 query_groups: list[list[str]] = []
129 grouped_sizes: list[int] = []
130 for i in range(0, len(batch_queries), QLEVER_QUERIES_PER_GROUP):
131 query_groups.append(batch_queries[i : i + QLEVER_QUERIES_PER_GROUP])
132 grouped_sizes.append(sum(batch_sizes[i : i + QLEVER_QUERIES_PER_GROUP]))
134 with ProcessPoolExecutor(
135 max_workers=min(len(query_groups), workers),
136 mp_context=multiprocessing.get_context("forkserver"),
137 ) as executor:
138 future_to_size = {
139 executor.submit(
140 execute_sparql_queries,
141 endpoint_url=endpoint_url,
142 queries=group,
143 max_retries=max_retries,
144 backoff_factor=backoff_factor,
145 timeout=timeout,
146 ): size
147 for group, size in zip(query_groups, grouped_sizes)
148 }
149 for future in as_completed(future_to_size):
150 all_bindings.extend(future.result())
151 if progress_callback:
152 progress_callback(future_to_size[future])
153 else:
154 results = execute_sparql_queries(
155 endpoint_url=endpoint_url,
156 queries=batch_queries,
157 max_retries=max_retries,
158 backoff_factor=backoff_factor,
159 timeout=timeout,
160 )
161 all_bindings.extend(results)
162 if progress_callback:
163 progress_callback(sum(batch_sizes))
165 return all_bindings