Coverage for src/time_agnostic_library/qlever_setup.py: 31%
48 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-10 10:04 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-10-10 10:04 +0000
1# SPDX-FileCopyrightText: 2026 Arcangelo Massari <arcangelo.massari@unibo.it>
2#
3# SPDX-License-Identifier: ISC
5import argparse
6import logging
7import shutil
8import subprocess
9from collections.abc import Iterator
10from pathlib import Path
11from typing import cast
13from pyoxigraph import Literal, NamedNode, Quad, RdfFormat, parse, serialize
14from sparqlite import SPARQLClient
16from time_agnostic_library.prov_entity import ProvEntity
17from time_agnostic_library.qlever import generate_qlever_index
18from time_agnostic_library.support import generate_config_file
20logger = logging.getLogger(__name__)
23def _iter_qlever_input(
24 dataset_files: list[Path], provenance_files: list[Path]
25) -> Iterator[Quad]:
26 predicate = NamedNode(ProvEntity.iri_has_update_query)
27 graph = NamedNode("urn:tal:qlever:prov/")
28 for path in provenance_files:
29 for quad in parse(path=path, format=RdfFormat.N_QUADS):
30 yield quad
31 if quad.predicate == predicate:
32 associations = generate_qlever_index(
33 [
34 (
35 cast("NamedNode", quad.subject).value,
36 cast("Literal", quad.object).value,
37 )
38 ]
39 )
40 for s, p, o in associations:
41 yield Quad(
42 NamedNode(str(s)), NamedNode(str(p)), Literal(str(o)), graph
43 )
44 for path in dataset_files:
45 yield from parse(path=path, format=RdfFormat.N_QUADS)
48def setup_qlever(
49 dataset_files: list[Path],
50 provenance_files: list[Path],
51 output_dir: Path,
52 port: int = 7000,
53) -> Path:
54 qlever = shutil.which("qlever")
55 if qlever is None:
56 msg = 'Install QLever support with uv add "time-agnostic-library[qlever]"'
57 raise FileNotFoundError(msg)
58 for path in [*dataset_files, *provenance_files]:
59 if not path.is_file():
60 raise FileNotFoundError(path)
61 output_dir = output_dir.resolve()
62 output_dir.mkdir(parents=True)
63 logger.info("Preparing dataset, provenance and snapshot URI associations")
64 serialize(
65 _iter_qlever_input(dataset_files, provenance_files),
66 output=output_dir / "tal.nq",
67 format=RdfFormat.N_QUADS,
68 )
69 logger.info("Building QLever index in %s", output_dir)
70 subprocess.run( # noqa: S603
71 [
72 qlever,
73 "index",
74 "--name",
75 "tal",
76 "--format",
77 "nq",
78 "--input-files",
79 "tal.nq",
80 "--cat-input-files",
81 "cat tal.nq",
82 "--settings-json",
83 '{"prefixes-external": []}',
84 "--system",
85 "docker",
86 ],
87 cwd=output_dir,
88 check=True,
89 )
90 logger.info("Starting QLever on port %s", port)
91 subprocess.run( # noqa: S603
92 [
93 qlever,
94 "start",
95 "--name",
96 "tal",
97 "--description",
98 "TAL dataset",
99 "--host-name",
100 "localhost",
101 "--port",
102 str(port),
103 "--server-container",
104 f"tal-qlever-{port}",
105 "--access-token",
106 "",
107 "--system",
108 "docker",
109 "--no-warmup",
110 ],
111 cwd=output_dir,
112 check=True,
113 )
114 endpoint = f"http://localhost:{port}"
115 with SPARQLClient(endpoint) as client:
116 client.ask("ASK { ?s ?p ?o }")
117 config_path = output_dir / "config.json"
118 generate_config_file(
119 str(config_path),
120 dataset_urls=[endpoint],
121 provenance_urls=[endpoint],
122 qlever_full_text_search=True,
123 )
124 logger.info("Endpoint: %s; TAL configuration: %s", endpoint, config_path)
125 return config_path
128def main(): # pragma: no cover
129 parser = argparse.ArgumentParser(
130 description="Build and serve a QLever index for Time Agnostic Library."
131 )
132 parser.add_argument(
133 "--dataset",
134 nargs="+",
135 type=Path,
136 required=True,
137 help="Current dataset files in N-Quads format",
138 )
139 parser.add_argument(
140 "--provenance",
141 nargs="+",
142 type=Path,
143 required=True,
144 help="OCDM provenance files in N-Quads format",
145 )
146 parser.add_argument(
147 "--output-dir",
148 type=Path,
149 required=True,
150 help="New directory for the index and TAL configuration",
151 )
152 parser.add_argument("--port", type=int, default=7000)
153 args = parser.parse_args()
154 logging.basicConfig(level=logging.INFO, format="%(message)s")
155 setup_qlever(args.dataset, args.provenance, args.output_dir, args.port)
158if __name__ == "__main__": # pragma: no cover
159 main()