qlever 0.2.5__py3-none-any.whl → 0.5.41__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. qlever/Qleverfiles/Qleverfile.dblp +36 -0
  2. qlever/Qleverfiles/Qleverfile.dblp-plus +33 -0
  3. qlever/Qleverfiles/Qleverfile.dbpedia +30 -0
  4. qlever/Qleverfiles/Qleverfile.default +51 -0
  5. qlever/Qleverfiles/Qleverfile.dnb +40 -0
  6. qlever/Qleverfiles/Qleverfile.fbeasy +29 -0
  7. qlever/Qleverfiles/Qleverfile.freebase +28 -0
  8. qlever/Qleverfiles/Qleverfile.imdb +36 -0
  9. qlever/Qleverfiles/Qleverfile.ohm-planet +41 -0
  10. qlever/Qleverfiles/Qleverfile.olympics +31 -0
  11. qlever/Qleverfiles/Qleverfile.orkg +30 -0
  12. qlever/Qleverfiles/Qleverfile.osm-country +39 -0
  13. qlever/Qleverfiles/Qleverfile.osm-planet +39 -0
  14. qlever/Qleverfiles/Qleverfile.osm-planet-from-pbf +42 -0
  15. qlever/Qleverfiles/Qleverfile.pubchem +131 -0
  16. qlever/Qleverfiles/Qleverfile.scientists +29 -0
  17. qlever/Qleverfiles/Qleverfile.uniprot +74 -0
  18. qlever/Qleverfiles/Qleverfile.vvz +31 -0
  19. qlever/Qleverfiles/Qleverfile.wikidata +42 -0
  20. qlever/Qleverfiles/Qleverfile.wikipathways +40 -0
  21. qlever/Qleverfiles/Qleverfile.yago-4 +33 -0
  22. qlever/__init__.py +44 -1380
  23. qlever/command.py +87 -0
  24. qlever/commands/__init__.py +0 -0
  25. qlever/commands/add_text_index.py +115 -0
  26. qlever/commands/benchmark_queries.py +1019 -0
  27. qlever/commands/cache_stats.py +125 -0
  28. qlever/commands/clear_cache.py +88 -0
  29. qlever/commands/extract_queries.py +120 -0
  30. qlever/commands/get_data.py +48 -0
  31. qlever/commands/index.py +333 -0
  32. qlever/commands/index_stats.py +306 -0
  33. qlever/commands/log.py +66 -0
  34. qlever/commands/materialized_view.py +110 -0
  35. qlever/commands/query.py +142 -0
  36. qlever/commands/rebuild_index.py +176 -0
  37. qlever/commands/reset_updates.py +59 -0
  38. qlever/commands/settings.py +115 -0
  39. qlever/commands/setup_config.py +97 -0
  40. qlever/commands/start.py +336 -0
  41. qlever/commands/status.py +50 -0
  42. qlever/commands/stop.py +90 -0
  43. qlever/commands/system_info.py +130 -0
  44. qlever/commands/ui.py +271 -0
  45. qlever/commands/update.py +90 -0
  46. qlever/commands/update_wikidata.py +1204 -0
  47. qlever/commands/warmup.py +41 -0
  48. qlever/config.py +223 -0
  49. qlever/containerize.py +167 -0
  50. qlever/log.py +55 -0
  51. qlever/qlever_main.py +79 -0
  52. qlever/qleverfile.py +530 -0
  53. qlever/util.py +330 -0
  54. qlever-0.5.41.dist-info/METADATA +127 -0
  55. qlever-0.5.41.dist-info/RECORD +59 -0
  56. {qlever-0.2.5.dist-info → qlever-0.5.41.dist-info}/WHEEL +1 -1
  57. qlever-0.5.41.dist-info/entry_points.txt +2 -0
  58. qlever-0.5.41.dist-info/top_level.txt +1 -0
  59. build/lib/qlever/__init__.py +0 -1383
  60. build/lib/qlever/__main__.py +0 -4
  61. qlever/__main__.py +0 -4
  62. qlever-0.2.5.dist-info/METADATA +0 -277
  63. qlever-0.2.5.dist-info/RECORD +0 -12
  64. qlever-0.2.5.dist-info/entry_points.txt +0 -2
  65. qlever-0.2.5.dist-info/top_level.txt +0 -4
  66. src/qlever/__init__.py +0 -1383
  67. src/qlever/__main__.py +0 -4
  68. {qlever-0.2.5.dist-info → qlever-0.5.41.dist-info/licenses}/LICENSE +0 -0
qlever/commands/log.py ADDED
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+ from qlever.command import QleverCommand
6
+ from qlever.log import log
7
+
8
+
9
+ class LogCommand(QleverCommand):
10
+ """
11
+ Class for executing the `log` command.
12
+ """
13
+
14
+ def __init__(self):
15
+ pass
16
+
17
+ def description(self) -> str:
18
+ return ("Show the last lines of the server log file and follow it")
19
+
20
+ def should_have_qleverfile(self) -> bool:
21
+ return False
22
+
23
+ def relevant_qleverfile_arguments(self) -> dict[str: list[str]]:
24
+ return {"data": ["name"]}
25
+
26
+ def additional_arguments(self, subparser) -> None:
27
+ subparser.add_argument("--tail-num-lines", type=int, default=20,
28
+ help="Show this many of the last lines of the "
29
+ "log file")
30
+ subparser.add_argument("--from-beginning", action="store_true",
31
+ default=False,
32
+ help="Show all lines of the log file")
33
+ subparser.add_argument("--no-follow", action="store_true",
34
+ default=False,
35
+ help="Don't follow the log file")
36
+
37
+ def execute(self, args) -> bool:
38
+ # Construct the command and show it.
39
+ log_cmd = "tail"
40
+ if args.from_beginning:
41
+ log_cmd += " -n +1"
42
+ else:
43
+ log_cmd += f" -n {args.tail_num_lines}"
44
+ if not args.no_follow:
45
+ log_cmd += " -f"
46
+ log_file = f"{args.name}.server-log.txt"
47
+ log_cmd += f" {log_file}"
48
+ self.show(log_cmd, only_show=args.show)
49
+ if args.show:
50
+ return True
51
+
52
+ # Execute the command.
53
+ log.info(f"Follow log file {log_file}, press Ctrl-C to stop"
54
+ f" following (will not stop the server)")
55
+ log.info("")
56
+ try:
57
+ subprocess.run(log_cmd, shell=True)
58
+ return True
59
+ except Exception as e:
60
+ log.error(e)
61
+ return False
62
+
63
+
64
+
65
+
66
+
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import shlex
6
+ import time
7
+
8
+ from qlever.command import QleverCommand
9
+ from qlever.log import log
10
+ from qlever.util import (
11
+ run_command,
12
+ )
13
+
14
+
15
+ class MaterializedViewCommand(QleverCommand):
16
+ """
17
+ Class for executing the `materialized-view` command.
18
+ """
19
+
20
+ def __init__(self):
21
+ self.materialized_view_name_regex = r"^[A-Za-z0-9-]+$"
22
+ pass
23
+
24
+ def description(self) -> str:
25
+ return "Create a materialized view from the given query"
26
+
27
+ def should_have_qleverfile(self) -> bool:
28
+ return True
29
+
30
+ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]:
31
+ return {
32
+ "data": ["name"],
33
+ "server": ["host_name", "port", "access_token"],
34
+ }
35
+
36
+ def additional_arguments(self, subparser) -> None:
37
+ subparser.add_argument(
38
+ "view_name",
39
+ type=str,
40
+ help="Name of the materialized view",
41
+ )
42
+ subparser.add_argument(
43
+ "view_query",
44
+ type=str,
45
+ help="SPARQL query from which to create the materialized view",
46
+ )
47
+ subparser.add_argument(
48
+ "--sparql-endpoint",
49
+ type=str,
50
+ help="URL of the SPARQL endpoint (default: <host_name>:<port>)",
51
+ )
52
+
53
+ def execute(self, args) -> bool:
54
+ # SPARQL endpoint to use.
55
+ sparql_endpoint = (
56
+ args.sparql_endpoint
57
+ if args.sparql_endpoint is not None
58
+ else f"{args.host_name}:{args.port}"
59
+ )
60
+
61
+ # Check that the name of the materialized view is valid.
62
+ if not re.match(self.materialized_view_name_regex, args.view_name):
63
+ log.error(
64
+ f"The name for the materialized view must match "
65
+ f"the regex {self.materialized_view_name_regex}"
66
+ )
67
+ return False
68
+
69
+ # Command for building the materialized view.
70
+ url = (
71
+ f"{sparql_endpoint}"
72
+ f"?cmd=write-materialized-view"
73
+ f"&view-name={args.view_name}"
74
+ )
75
+ materialized_view_cmd = (
76
+ f"curl -s {shlex.quote(url)} "
77
+ f"-H 'Authorization: Bearer {args.access_token}' "
78
+ f"-H 'Content-type: application/sparql-query' "
79
+ f"-d {shlex.quote(args.view_query)}"
80
+ )
81
+ self.show(materialized_view_cmd, only_show=args.show)
82
+ if args.show:
83
+ return True
84
+
85
+ # Run the command (and time it).
86
+ time_start = time.monotonic()
87
+ try:
88
+ log.info("Creating the materialized view ... "
89
+ "(this may take a while, depending on the complexity "
90
+ "of the query and the size of the result)")
91
+ log.info("")
92
+ result = run_command(materialized_view_cmd, return_output=True)
93
+ except Exception as e:
94
+ log.error(f"Creating the materialized view failed: {e}")
95
+ return False
96
+ time_end = time.monotonic()
97
+ duration_seconds = round(time_end - time_start)
98
+
99
+ # Try to parse the result (should be JSON).
100
+ try:
101
+ result_json = json.loads(result)
102
+ view_name = result_json.get("materialized-view-written")
103
+ log.info(
104
+ f"Materialized view '{view_name}' created successfully "
105
+ f"in {duration_seconds:,} seconds"
106
+ )
107
+ except Exception as e:
108
+ log.error(f'Failed to parse JSON from "{result}": {e}')
109
+
110
+ return True
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ import time
5
+ import traceback
6
+
7
+ from qlever.command import QleverCommand
8
+ from qlever.log import log
9
+ from qlever.util import run_command
10
+
11
+
12
+ class QueryCommand(QleverCommand):
13
+ """
14
+ Class for executing the `query` command.
15
+ """
16
+
17
+ def __init__(self):
18
+ self.predefined_queries = {
19
+ "all-predicates": (
20
+ "SELECT (?p AS ?predicate) (COUNT(?p) AS ?count) "
21
+ "WHERE { ?s ?p ?o } "
22
+ "GROUP BY ?p ORDER BY DESC(?count)"
23
+ ),
24
+ "all-graphs": (
25
+ "SELECT ?g (COUNT(?g) AS ?count) "
26
+ "WHERE { GRAPH ?g { ?s ?p ?o } } "
27
+ "GROUP BY ?g ORDER BY DESC(?count)"
28
+ ),
29
+ "ten-random-triples": (
30
+ "SELECT * WHERE { ?s ?p ?o } ORDER BY RAND() LIMIT 10"
31
+ ),
32
+ }
33
+
34
+ def description(self) -> str:
35
+ return "Send a query to a SPARQL endpoint"
36
+
37
+ def should_have_qleverfile(self) -> bool:
38
+ return False
39
+
40
+ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]:
41
+ return {"server": ["host_name", "port", "access_token"]}
42
+
43
+ def additional_arguments(self, subparser) -> None:
44
+ subparser.add_argument(
45
+ "query",
46
+ type=str,
47
+ nargs="?",
48
+ default="SELECT * WHERE { ?s ?p ?o } LIMIT 10",
49
+ help="SPARQL query to send",
50
+ )
51
+ subparser.add_argument(
52
+ "--predefined-query",
53
+ type=str,
54
+ choices=self.predefined_queries.keys(),
55
+ help="Use a predefined query",
56
+ )
57
+ subparser.add_argument(
58
+ "--pin-to-cache",
59
+ action="store_true",
60
+ default=False,
61
+ help="Pin the query to the cache",
62
+ )
63
+ subparser.add_argument(
64
+ "--sparql-endpoint", type=str, help="URL of the SPARQL endpoint"
65
+ )
66
+ subparser.add_argument(
67
+ "--accept",
68
+ type=str,
69
+ choices=[
70
+ "text/tab-separated-values",
71
+ "text/csv",
72
+ "application/sparql-results+json",
73
+ "application/sparql-results+xml",
74
+ "application/qlever-results+json",
75
+ "application/octet-stream",
76
+ ],
77
+ default="text/tab-separated-values",
78
+ help="Accept header for the SPARQL query",
79
+ )
80
+ subparser.add_argument(
81
+ "--no-time",
82
+ action="store_true",
83
+ default=False,
84
+ help="Do not print the (end-to-end) time taken",
85
+ )
86
+
87
+ def execute(self, args) -> bool:
88
+ # Use a predefined query if requested.
89
+ if args.predefined_query:
90
+ args.query = self.predefined_queries[args.predefined_query]
91
+
92
+ # When pinning to the cache, set `send=0` and request media type
93
+ # `application/qlever-results+json` so that we get the result size.
94
+ # Also, we need to provide the access token.
95
+ if args.pin_to_cache:
96
+ args.accept = "application/qlever-results+json"
97
+ curl_cmd_additions = (
98
+ f" --data pin-result=true --data send=0"
99
+ f" --data access-token="
100
+ f"{shlex.quote(args.access_token)}"
101
+ f" | jq .resultsize | numfmt --grouping"
102
+ f" | xargs -I {{}} printf"
103
+ f' "Result pinned to cache,'
104
+ f' number of rows: {{}}\\n"'
105
+ )
106
+ else:
107
+ curl_cmd_additions = ""
108
+
109
+ # Show what the command will do.
110
+ sparql_endpoint = (
111
+ args.sparql_endpoint
112
+ if args.sparql_endpoint
113
+ else f"{args.host_name}:{args.port}"
114
+ )
115
+ curl_cmd = (
116
+ f"curl -s {sparql_endpoint}"
117
+ f' -H "Accept: {args.accept}"'
118
+ f" --data-urlencode query={shlex.quote(args.query)}"
119
+ f"{curl_cmd_additions}"
120
+ )
121
+ self.show(curl_cmd, only_show=args.show)
122
+ if args.show:
123
+ return True
124
+
125
+ # Launch query.
126
+ try:
127
+ start_time = time.time()
128
+ run_command(curl_cmd, show_output=True)
129
+ time_msecs = round(1000 * (time.time() - start_time))
130
+ if not args.no_time and args.log_level != "NO_LOG":
131
+ log.info("")
132
+ log.info(
133
+ f"Query processing time (end-to-end):"
134
+ f" {time_msecs:,d} ms"
135
+ )
136
+ except Exception as e:
137
+ if args.log_level == "DEBUG":
138
+ traceback.print_exc()
139
+ log.error(e)
140
+ return False
141
+
142
+ return True
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from termcolor import colored
8
+
9
+ from qlever.command import QleverCommand
10
+ from qlever.log import log
11
+ from qlever.util import (
12
+ run_command,
13
+ )
14
+
15
+
16
+ class RebuildIndexCommand(QleverCommand):
17
+ """
18
+ Class for executing the `rebuild-index` command.
19
+ """
20
+
21
+ def __init__(self):
22
+ pass
23
+
24
+ def description(self) -> str:
25
+ return "Rebuild the index from the current data (including updates)"
26
+
27
+ def should_have_qleverfile(self) -> bool:
28
+ return True
29
+
30
+ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]:
31
+ return {
32
+ "data": ["name"],
33
+ "server": ["host_name", "port", "access_token"],
34
+ "runtime": ["server_container"],
35
+ }
36
+
37
+ def additional_arguments(self, subparser) -> None:
38
+ subparser.add_argument(
39
+ "--index-dir",
40
+ type=str,
41
+ help="Directory for the new index (default: subdirectory "
42
+ "`rebuild.YYYY-MM-DDTHH:MM` of the current directory)",
43
+ )
44
+ subparser.add_argument(
45
+ "--index-name",
46
+ type=str,
47
+ help="Base name of the new index (default: use the same as the "
48
+ "current index)",
49
+ )
50
+ subparser.add_argument(
51
+ "--restart-when-finished",
52
+ action="store_true",
53
+ default=False,
54
+ help="When the rebuild is finished, stop the server with the old "
55
+ "index and start it again with the new index",
56
+ )
57
+
58
+ def execute(self, args) -> bool:
59
+ # Default values for arguments.
60
+ if args.index_name is None:
61
+ args.index_name = args.name
62
+ if args.index_dir is None:
63
+ timestamp = time.strftime("%Y-%m-%dT%H:%M", time.localtime())
64
+ args.index_dir = f"rebuild.{timestamp}"
65
+ if args.index_dir.endswith("/"):
66
+ args.index_dir = args.index_dir[:-1]
67
+
68
+ # Check that the index directory either does not exist or is empty.
69
+ index_path = Path(args.index_dir)
70
+ if index_path.exists() and any(index_path.iterdir()):
71
+ log.error(
72
+ f"The specified index directory '{args.index_dir}' already "
73
+ "exists and is not empty; please specify an empty or "
74
+ "non-existing directory"
75
+ )
76
+ return False
77
+
78
+ # Split `index_dir` into path and dir name. For example, if `index_dir`
79
+ # is `path/to/index`, then the path is `path/to` and the dir name
80
+ # is `index`.
81
+ #
82
+ # NOTE: We keep this separate because we can always create a
83
+ # subdirectory in the current directory (even when running in a
84
+ # container), but not necessarily a directory at an arbitrary path. If
85
+ # a path outside the current directory is desired, we move the index
86
+ # there after it has been built.
87
+ index_dir_path = str(Path(args.index_dir).parent)
88
+ index_dir_name = str(Path(args.index_dir).name)
89
+ log_file_name = f"{args.index_name}.rebuild-index-log.txt"
90
+
91
+ # Command for rebuilding the index.
92
+ mkdir_cmd = (
93
+ f"mkdir -p {index_dir_name} && "
94
+ f"> {index_dir_name}/{log_file_name} && "
95
+ f"cp -a Qleverfile {index_dir_name}"
96
+ )
97
+ rebuild_index_cmd = (
98
+ f"curl -s {args.host_name}:{args.port} "
99
+ f"-d cmd=rebuild-index "
100
+ f"-d index-name={index_dir_name}/{args.index_name} "
101
+ f"-d access-token={args.access_token}"
102
+ )
103
+ move_index_cmd = f"mv {index_dir_name} {index_dir_path}"
104
+ restart_server_cmd = (
105
+ f"cd {args.index_dir} && "
106
+ f"qlever start --kill-existing-with-same-port"
107
+ )
108
+
109
+ # Show the command lines.
110
+ cmds_to_show = [mkdir_cmd, rebuild_index_cmd]
111
+ if index_dir_path != ".":
112
+ cmds_to_show.append(move_index_cmd)
113
+ if args.restart_when_finished:
114
+ cmds_to_show.append(restart_server_cmd)
115
+ self.show("\n".join(cmds_to_show), only_show=args.show)
116
+ if args.show:
117
+ return True
118
+
119
+ # Create the index directory and the log file.
120
+ try:
121
+ run_command(mkdir_cmd)
122
+ except Exception as e:
123
+ log.error(f"Creating the index directory failed: {e}")
124
+ return False
125
+
126
+ # Show the server log while rebuilding the index.
127
+ #
128
+ # NOTE: This will only work satisfactorily when no other queries are
129
+ # being processed at the same time. It would be better if QLever
130
+ # logged the rebuild-index output to a separate log file.
131
+ tail_cmd = f"exec tail -n 0 -f {index_dir_name}/{log_file_name}"
132
+ tail_proc = subprocess.Popen(tail_cmd, shell=True)
133
+
134
+ # Run the index rebuild command (and time it).
135
+ try:
136
+ time_start = time.monotonic()
137
+ try:
138
+ run_command(rebuild_index_cmd, show_output=False)
139
+ except Exception as e:
140
+ log.error(f"Rebuilding the index failed: {e}")
141
+ return False
142
+ time_end = time.monotonic()
143
+ duration_seconds = round(time_end - time_start)
144
+ log.info("")
145
+ rebuild_done_msg = f"Rebuilt index in {duration_seconds:,} seconds"
146
+ if index_dir_path == ".":
147
+ rebuild_done_msg += (
148
+ f", in the new directory '{args.index_dir}'"
149
+ )
150
+ log.info(rebuild_done_msg)
151
+ finally:
152
+ tail_proc.terminate()
153
+ tail_proc.wait()
154
+
155
+ # Move the new index to the specified directory, if needed.
156
+ if index_dir_path != ".":
157
+ try:
158
+ log.info(f"Moving the new index to {args.index_dir}")
159
+ run_command(move_index_cmd)
160
+ except Exception as e:
161
+ log.error(f"Moving the new index failed: {e}")
162
+ return False
163
+
164
+ # Restart the server with the new index, if requested.
165
+ if args.restart_when_finished:
166
+ try:
167
+ log.info("Restarting the server with the new index ...")
168
+ log.info("")
169
+ log.info(colored("Command: start", attrs=["bold"]))
170
+ log.info("")
171
+ run_command(restart_server_cmd, show_output=True)
172
+ except Exception as e:
173
+ log.error(f"Restarting the server failed: {e}")
174
+ return False
175
+
176
+ return True
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from qlever.command import QleverCommand
6
+ from qlever.log import log
7
+ from qlever.util import run_command
8
+
9
+
10
+ class ResetUpdatesCommand(QleverCommand):
11
+ """
12
+ Class for executing the `reset-updates` command.
13
+ """
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ def description(self) -> str:
19
+ return "Reset the updates on the server"
20
+
21
+ def should_have_qleverfile(self) -> bool:
22
+ return True
23
+
24
+ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]:
25
+ return {"server": ["host_name", "port", "access_token"]}
26
+
27
+ def additional_arguments(self, subparser) -> None:
28
+ subparser.add_argument(
29
+ "--sparql-endpoint",
30
+ help="URL of the QLever server, default is {host_name}:{port}",
31
+ )
32
+
33
+ def execute(self, args) -> bool:
34
+ reset_cmd = "curl -s"
35
+ if args.sparql_endpoint:
36
+ reset_cmd += f" {args.sparql_endpoint}"
37
+ else:
38
+ reset_cmd += f" {args.host_name}:{args.port}"
39
+ reset_cmd += f' --data-urlencode "cmd=clear-delta-triples" --data-urlencode "access-token={args.access_token}"'
40
+ self.show(reset_cmd, only_show=args.show)
41
+ if args.show:
42
+ return True
43
+
44
+ try:
45
+ reset_cmd += ' -w " %{http_code}"'
46
+ result = run_command(reset_cmd, return_output=True)
47
+ match = re.match(r"^(.*) (\d+)$", result, re.DOTALL)
48
+ if not match:
49
+ raise Exception(f"Unexpected output:\n{result}")
50
+ error_message = match.group(1).strip()
51
+ status_code = match.group(2)
52
+ if status_code != "200":
53
+ raise Exception(error_message)
54
+ message = "Updates reset successfully"
55
+ log.info(message)
56
+ return True
57
+ except Exception as e:
58
+ log.error(e)
59
+ return False
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from termcolor import colored
6
+
7
+ from qlever.command import QleverCommand
8
+ from qlever.log import log
9
+ from qlever.qleverfile import Qleverfile
10
+ from qlever.util import run_command
11
+
12
+
13
+ class SettingsCommand(QleverCommand):
14
+ """
15
+ Class for executing the `settings` command.
16
+ """
17
+
18
+ def __init__(self):
19
+ pass
20
+
21
+ def description(self) -> str:
22
+ return "Show or set server settings (after `qlever start`)"
23
+
24
+ def should_have_qleverfile(self) -> bool:
25
+ return True
26
+
27
+ def relevant_qleverfile_arguments(self) -> dict[str, list[str]]:
28
+ return {"server": ["port", "host_name", "access_token"]}
29
+
30
+ def additional_arguments(self, subparser) -> None:
31
+ subparser.add_argument(
32
+ "runtime_parameters",
33
+ nargs="*",
34
+ help="Space-separated list of runtime parameters to set "
35
+ "in the form `key=value`; afterwards shows all settings, "
36
+ "with the changed ones highlighted",
37
+ ).completer = lambda **kwargs: [
38
+ f"{key}=" for key in Qleverfile.SERVER_RUNTIME_PARAMETERS
39
+ ]
40
+ subparser.add_argument(
41
+ "--endpoint_url",
42
+ type=str,
43
+ help="An arbitrary endpoint URL "
44
+ "(overriding the one in the Qleverfile)",
45
+ )
46
+
47
+ def execute(self, args) -> bool:
48
+ # Get endpoint URL from command line or Qleverfile.
49
+ if args.endpoint_url:
50
+ endpoint_url = args.endpoint_url
51
+ else:
52
+ endpoint_url = f"http://{args.host_name}:{args.port}"
53
+
54
+ # Construct the `curl` commands for setting and getting.
55
+ curl_cmds_setting = []
56
+ keys_set = set()
57
+ if args.runtime_parameters:
58
+ for key_value_pair in args.runtime_parameters:
59
+ try:
60
+ key, value = key_value_pair.split("=")
61
+ except ValueError:
62
+ log.error("Runtime parameter must be given as `key=value`")
63
+ return False
64
+ curl_cmds_setting.append(
65
+ f"curl -s {endpoint_url} -w %{{http_code}}"
66
+ f' --data-urlencode "{key}={value}"'
67
+ f' --data-urlencode "access-token={args.access_token}"'
68
+ )
69
+ keys_set.add(key)
70
+ curl_cmd_getting = (
71
+ f"curl -s {endpoint_url} -w %{{http_code}}"
72
+ f" --data-urlencode cmd=get-settings"
73
+ )
74
+ self.show(
75
+ "\n".join(curl_cmds_setting + [curl_cmd_getting]),
76
+ only_show=args.show,
77
+ )
78
+ if args.show:
79
+ return True
80
+
81
+ # Execute the `curl` commands for setting the key-value pairs if any.
82
+ for curl_cmd in curl_cmds_setting:
83
+ try:
84
+ curl_result = run_command(curl_cmd, return_output=True)
85
+ body, http_code = curl_result[:-3], curl_result[-3:]
86
+ if http_code != "200":
87
+ raise Exception(body)
88
+ except Exception as e:
89
+ log.error(
90
+ f"curl command for setting key-value pair failed: {e}"
91
+ )
92
+ return False
93
+
94
+ # Execute the `curl` commands for getting the settings.
95
+ try:
96
+ curl_result = run_command(curl_cmd_getting, return_output=True)
97
+ body, http_code = curl_result[:-3], curl_result[-3:]
98
+ if http_code != "200":
99
+ raise Exception(body)
100
+ settings_dict = json.loads(body)
101
+ if isinstance(settings_dict, list):
102
+ settings_dict = settings_dict[0]
103
+ except Exception as e:
104
+ log.error(f"curl command for getting settings failed: {e}")
105
+ return False
106
+ for key, value in settings_dict.items():
107
+ print(
108
+ colored(
109
+ f"{key:<45}: {value}",
110
+ "blue" if key in keys_set else None,
111
+ )
112
+ )
113
+
114
+ # That's it.
115
+ return True