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
@@ -0,0 +1,41 @@
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 WarmupCommand(QleverCommand):
10
+ """
11
+ Class for executing the `warmup` command.
12
+ """
13
+
14
+ def __init__(self):
15
+ pass
16
+
17
+ def description(self) -> str:
18
+ return ("Execute WARMUP_CMD")
19
+
20
+ def should_have_qleverfile(self) -> bool:
21
+ return True
22
+
23
+ def relevant_qleverfile_arguments(self) -> dict[str: list[str]]:
24
+ return {"server": ["port", "warmup_cmd"]}
25
+
26
+ def additional_arguments(self, subparser) -> None:
27
+ pass
28
+
29
+ def execute(self, args) -> bool:
30
+ # Show what the command is doing.
31
+ self.show(args.warmup_cmd, only_show=args.show)
32
+ if args.show:
33
+ return True
34
+
35
+ # Execute the command.
36
+ try:
37
+ subprocess.run(args.warmup_cmd, shell=True, check=True)
38
+ except Exception as e:
39
+ log.error(f"{e.output if hasattr(e, 'output') else e}")
40
+ return False
41
+ return True
qlever/config.py ADDED
@@ -0,0 +1,223 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import traceback
6
+ from importlib.metadata import version
7
+ from pathlib import Path
8
+
9
+ import argcomplete
10
+ from termcolor import colored
11
+
12
+ from qlever import command_objects, engine_name, script_name
13
+ from qlever.log import log, log_levels
14
+ from qlever.qleverfile import Qleverfile
15
+
16
+
17
+ # Simple exception class for configuration errors (the class need not do
18
+ # anything, we just want a distinct exception type).
19
+ class ConfigException(Exception):
20
+ def __init__(self, message):
21
+ stack = traceback.extract_stack()[-2] # Caller's frame.
22
+ self.filename = stack.filename
23
+ self.lineno = stack.lineno
24
+ full_message = f"{message} [in {self.filename}:{self.lineno}]"
25
+ super().__init__(full_message)
26
+
27
+
28
+ class QleverConfig:
29
+ """
30
+ Class that manages all config parameters, and overwrites them with the
31
+ settings from a Qleverfile.
32
+
33
+ IMPORTANT: An instance of this class is created for each execution of
34
+ the `qlever` script, in particular, each time the user triggers
35
+ autocompletion. Therefore, pay attention that no unnecessary work is done
36
+ before the call to `argcomplete.autocomplete(...)`. In particular, avoid
37
+ parsing the Qleverfile before that point, it's not needed for
38
+ autocompletion.
39
+ """
40
+
41
+ def add_subparser_for_command(self, subparsers, command_name,
42
+ command_object, all_qleverfile_args,
43
+ qleverfile_config=None):
44
+ """
45
+ Add subparser for the given command. Take the arguments from
46
+ `command_object.relevant_qleverfile_arguments()` and report an error if
47
+ one of them is not contained in `all_qleverfile_args`. Overwrite the
48
+ default values with the values from `qleverfile_config` if specified.
49
+ """
50
+
51
+ arg_names = command_object.relevant_qleverfile_arguments()
52
+
53
+ # Helper function that shows a detailed error messahe when an argument
54
+ # from `relevant_qleverfile_arguments` is not contained in
55
+ # `all_qleverfile_args`.
56
+ def argument_error(prefix):
57
+ log.info("")
58
+ log.error(f"{prefix} in `Qleverfile.all_arguments()` for command "
59
+ f"`{command_name}`")
60
+ log.info("")
61
+ log.info(f"Value of `relevant_qleverfile_arguments` for "
62
+ f"command `{command_name}`:")
63
+ log.info("")
64
+ log.info(f"{arg_names}")
65
+ log.info("")
66
+ exit(1)
67
+
68
+ # Add the subparser.
69
+ description = command_object.description()
70
+ subparser = subparsers.add_parser(command_name,
71
+ description=description,
72
+ help=description)
73
+
74
+ # Add the arguments relevant for the command.
75
+ for section in arg_names:
76
+ if section not in all_qleverfile_args:
77
+ argument_error(f"Section `{section}` not found")
78
+ for arg_name in arg_names[section]:
79
+ if arg_name not in all_qleverfile_args[section]:
80
+ argument_error(f"Argument `{arg_name}` of section "
81
+ f"`{section}` not found")
82
+ args, kwargs = all_qleverfile_args[section][arg_name]
83
+ kwargs_copy = kwargs.copy()
84
+ # If `qleverfile_config` is given, add info about default
85
+ # values to the help string.
86
+ if qleverfile_config is not None:
87
+ default_value = kwargs.get("default", None)
88
+ qleverfile_value = qleverfile_config.get(
89
+ section, arg_name, fallback=None)
90
+ if qleverfile_value is not None:
91
+ kwargs_copy["default"] = qleverfile_value
92
+ kwargs_copy["required"] = False
93
+ kwargs_copy["help"] += (f" [default, from Qleverfile:"
94
+ f" {qleverfile_value}]")
95
+ else:
96
+ kwargs_copy["help"] += f" [default: {default_value}]"
97
+ subparser.add_argument(*args, **kwargs_copy)
98
+
99
+ # Additional arguments that are shared by all commands.
100
+ command_object.additional_arguments(subparser)
101
+ subparser.add_argument("--show", action="store_true",
102
+ default=False,
103
+ help="Only show what would be executed"
104
+ ", but don't execute it")
105
+ subparser.add_argument("--log-level",
106
+ choices=log_levels.keys(),
107
+ default="INFO",
108
+ help="Set the log level")
109
+
110
+ def parse_args(self):
111
+ # Determine whether we are in autocomplete mode or not.
112
+ autocomplete_mode = "COMP_LINE" in os.environ
113
+
114
+ # Check if the user has registered this script for argcomplete.
115
+ argcomplete_check_off = os.environ.get("QLEVER_ARGCOMPLETE_CHECK_OFF")
116
+ argcomplete_enabled = os.environ.get("QLEVER_ARGCOMPLETE_ENABLED")
117
+ if not argcomplete_enabled and not argcomplete_check_off:
118
+ log.info("")
119
+ log.warn(f"To enable autocompletion, run the following command, "
120
+ f"and consider adding it to your `.bashrc` or `.zshrc`:"
121
+ f"\n\n"
122
+ f"eval \"$(register-python-argcomplete {script_name})\""
123
+ f" && export QLEVER_ARGCOMPLETE_ENABLED=1")
124
+ log.info("")
125
+
126
+ # Create a temporary parser only to parse the `--qleverfile` option, in
127
+ # case it is given, and to determine whether a command was given that
128
+ # requires a Qleverfile. This is because in the actual parser below we
129
+ # want the values from the Qleverfile to be shown in the help strings,
130
+ # but only if this is actually necessary.
131
+ def add_qleverfile_option(parser):
132
+ parser.add_argument("--qleverfile", "-q", type=str,
133
+ default="Qleverfile")
134
+ qleverfile_parser = argparse.ArgumentParser(add_help=False)
135
+ add_qleverfile_option(qleverfile_parser)
136
+ qleverfile_parser.add_argument("command", type=str, nargs="?")
137
+ qleverfile_args, _ = qleverfile_parser.parse_known_args()
138
+ qleverfile_path_name = qleverfile_args.qleverfile
139
+ # command = qleverfile_args.command
140
+ # should_have_qleverfile = command in command_objects \
141
+ # and command_objects[command].should_have_qleverfile()
142
+
143
+ # Check if the Qleverfile exists and if we are using the default name.
144
+ # We need this again further down in the code, so remember it.
145
+ qleverfile_path = Path(qleverfile_path_name)
146
+ qleverfile_exists = qleverfile_path.is_file()
147
+ qleverfile_is_default = qleverfile_path_name \
148
+ == qleverfile_parser.get_default("qleverfile")
149
+ # If a Qleverfile with a non-default name was specified, but it does
150
+ # not exist, that's an error.
151
+ if not qleverfile_exists and not qleverfile_is_default:
152
+ raise ConfigException(f"Qleverfile with non-default name "
153
+ f"`{qleverfile_path_name}` specified, "
154
+ f"but it does not exist")
155
+ # If it exists and we are not in the autocompletion mode, parse it.
156
+ #
157
+ # IMPORTANT: No need to parse the Qleverfile in autocompletion mode and
158
+ # it would be unnecessarily expensive to do so.
159
+ #
160
+ # TODO: What if `command.should_have_qleverfile()` is `False`, should
161
+ # we then parse the Qleverfile or not.
162
+ if qleverfile_exists and not autocomplete_mode:
163
+ try:
164
+ qleverfile_config = Qleverfile.read(qleverfile_path)
165
+ except Exception as e:
166
+ log.info("")
167
+ log.error(f"Error parsing Qleverfile `{qleverfile_path}`"
168
+ f": {e}")
169
+ log.info("")
170
+ exit(1)
171
+ else:
172
+ qleverfile_config = None
173
+
174
+ # Now the regular parser with commands and a subparser for each
175
+ # command. We have a dedicated class for each command. These classes
176
+ # are defined in the modules in `qlever/commands`. In `__init__.py`
177
+ # an object of each class is created and stored in `command_objects`.
178
+ parser = argparse.ArgumentParser(
179
+ description=colored(
180
+ f"This is the {script_name} command line tool, "
181
+ f"it's all you need to work with {engine_name}",
182
+ attrs=["bold"],
183
+ )
184
+ )
185
+ if script_name == "qlever":
186
+ parser.add_argument(
187
+ "--version",
188
+ action="version",
189
+ version=f"%(prog)s {version('qlever')}",
190
+ )
191
+ add_qleverfile_option(parser)
192
+ subparsers = parser.add_subparsers(dest='command')
193
+ subparsers.required = True
194
+ all_args = Qleverfile.all_arguments()
195
+ for command_name, command_object in command_objects.items():
196
+ self.add_subparser_for_command(
197
+ subparsers, command_name, command_object,
198
+ all_args, qleverfile_config)
199
+
200
+ # Enable autocompletion for the commands and their options.
201
+ #
202
+ # NOTE: All code executed before this line should be relatively cheap
203
+ # because it is executed whenever the user triggers the autocompletion.
204
+ argcomplete.autocomplete(parser, always_complete_options="long")
205
+
206
+ # If called without arguments, show the help message.
207
+ if len(os.sys.argv) == 1:
208
+ parser.print_help()
209
+ exit(0)
210
+
211
+ # Parse the command line arguments.
212
+ args = parser.parse_args()
213
+
214
+ # If the command says that we should have a Qleverfile, but we don't,
215
+ # issue a warning.
216
+ if command_objects[args.command].should_have_qleverfile():
217
+ if not qleverfile_exists:
218
+ log.warning(f"Invoking command `{args.command}` without a "
219
+ "Qleverfile. You have to specify all required "
220
+ "arguments on the command line. This is possible, "
221
+ "but not recommended.")
222
+
223
+ return args
qlever/containerize.py ADDED
@@ -0,0 +1,167 @@
1
+ # Copyright 2024, University of Freiburg,
2
+ # Chair of Algorithms and Data Structures
3
+ # Author: Hannah Bast <bast@cs.uni-freiburg.de>
4
+
5
+ from __future__ import annotations
6
+
7
+ import shlex
8
+ import subprocess
9
+ from typing import Optional
10
+
11
+ from qlever.log import log
12
+ from qlever.util import get_random_string, run_command
13
+
14
+
15
+ class ContainerizeException(Exception):
16
+ pass
17
+
18
+
19
+ class Containerize:
20
+ """
21
+ This class contains functions specific for running commands with various
22
+ container engines.
23
+ """
24
+
25
+ @staticmethod
26
+ def supported_systems() -> list[str]:
27
+ """
28
+ Return a list of the supported container systems. Make sure that they
29
+ are all indeed supported by `containerize_command` below.
30
+ """
31
+ return ["docker", "podman"]
32
+
33
+ @staticmethod
34
+ def containerize_command(
35
+ cmd: str,
36
+ container_system: str,
37
+ run_subcommand: str,
38
+ image_name: str,
39
+ container_name: str,
40
+ volumes: list[tuple[str, str]] = [],
41
+ ports: list[tuple[int, int]] = [],
42
+ working_directory: Optional[str] = None,
43
+ use_bash: bool = True,
44
+ ) -> str:
45
+ """
46
+ Get the command to run `cmd` with the given `container_system` and the
47
+ given options.
48
+ """
49
+
50
+ # Check that `container_system` is supported.
51
+ if container_system not in Containerize.supported_systems():
52
+ return ContainerizeException(
53
+ f'Invalid container system "{container_system}"'
54
+ f" (must be one of {Containerize.supported_systems()})"
55
+ )
56
+
57
+ # Set user and group ids. This is important so that the files created
58
+ # by the containerized command are owned by the user running the
59
+ # command.
60
+ if container_system == "docker":
61
+ user_option = " -u $(id -u):$(id -g)"
62
+ elif container_system == "podman":
63
+ user_option = " -u root"
64
+ else:
65
+ user_option = ""
66
+
67
+ # Options for mounting volumes, setting ports, and setting the working
68
+ # dir.
69
+ volume_options = "".join(
70
+ [
71
+ f' --mount type=bind,src="{v1}",target={v2}'
72
+ for v1, v2 in volumes
73
+ ]
74
+ )
75
+ port_options = "".join([f" -p {p1}:{p2}" for p1, p2 in ports])
76
+ working_directory_option = (
77
+ f" -w {working_directory}" if working_directory is not None else ""
78
+ )
79
+
80
+ # Construct the command that runs `cmd` with the given container
81
+ # system.
82
+ containerized_cmd = (
83
+ f"{container_system} {run_subcommand}"
84
+ f"{user_option}"
85
+ f" -v /etc/localtime:/etc/localtime:ro"
86
+ f"{volume_options}"
87
+ f"{port_options}"
88
+ f"{working_directory_option}"
89
+ f" --name {container_name}"
90
+ f" --init"
91
+ )
92
+ if use_bash:
93
+ containerized_cmd += (
94
+ f" --entrypoint bash {image_name} -c {shlex.quote(cmd)}"
95
+ )
96
+ else:
97
+ containerized_cmd += f" {image_name} {cmd}"
98
+ return containerized_cmd
99
+
100
+ @staticmethod
101
+ def is_running(container_system: str, container_name: str) -> bool:
102
+ # Note: the `{{{{` and `}}}}` result in `{{` and `}}`, respectively.
103
+ containers = (
104
+ run_command(
105
+ f'{container_system} ps --format="{{{{.Names}}}}"',
106
+ return_output=True,
107
+ )
108
+ .strip()
109
+ .splitlines()
110
+ )
111
+ return container_name in containers
112
+
113
+ @staticmethod
114
+ def stop_and_remove_container(
115
+ container_system: str, container_name: str
116
+ ) -> bool:
117
+ """
118
+ Stop the container with the given name using the given system. Return
119
+ `True` if a container with that name was found and stopped, `False`
120
+ otherwise.
121
+ """
122
+
123
+ # Check that `container_system` is supported.
124
+ if container_system not in Containerize.supported_systems():
125
+ return ContainerizeException(
126
+ f'Invalid container system "{container_system}"'
127
+ f" (must be one of {Containerize.supported_systems()})"
128
+ )
129
+
130
+ # Construct the command that stops the container.
131
+ stop_cmd = (
132
+ f"{container_system} stop {container_name} && "
133
+ f"{container_system} rm {container_name}"
134
+ )
135
+
136
+ # Run the command.
137
+ try:
138
+ subprocess.run(
139
+ stop_cmd,
140
+ shell=True,
141
+ check=True,
142
+ stdout=subprocess.DEVNULL,
143
+ stderr=subprocess.DEVNULL,
144
+ )
145
+ return True
146
+ except Exception as e:
147
+ log.debug(f'Error running "{stop_cmd}": {e}')
148
+ return False
149
+
150
+ @staticmethod
151
+ def run_in_container(cmd: str, args) -> Optional[str]:
152
+ """
153
+ Run an arbitrary command in the qlever container and return its output.
154
+ """
155
+ if args.system in Containerize.supported_systems():
156
+ if not args.server_container:
157
+ args.server_container = get_random_string(20)
158
+ run_cmd = Containerize().containerize_command(
159
+ cmd,
160
+ args.system,
161
+ 'run --rm -it --entrypoint "" ',
162
+ args.image,
163
+ args.server_container,
164
+ volumes=[("$(pwd)", "/index")],
165
+ working_directory="/index",
166
+ )
167
+ return run_command(run_cmd, return_output=True)
qlever/log.py ADDED
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from contextlib import contextmanager
5
+
6
+ from termcolor import colored
7
+
8
+
9
+ class QleverLogFormatter(logging.Formatter):
10
+ """
11
+ Custom formatter for logging.
12
+ """
13
+
14
+ def format(self, record):
15
+ message = record.getMessage()
16
+ if record.levelno == logging.DEBUG:
17
+ return colored(f"{message}", "magenta")
18
+ elif record.levelno == logging.WARNING:
19
+ return colored(f"{message}", "yellow")
20
+ elif record.levelno in [logging.CRITICAL, logging.ERROR]:
21
+ return colored(f"{message}", "red")
22
+ else:
23
+ return message
24
+
25
+
26
+ # Custom logger.
27
+ log = logging.getLogger("qlever")
28
+ log.setLevel(logging.INFO)
29
+ handler = logging.StreamHandler()
30
+ handler.setFormatter(QleverLogFormatter())
31
+ log.addHandler(handler)
32
+ log_levels = {
33
+ "DEBUG": logging.DEBUG,
34
+ "INFO": logging.INFO,
35
+ "WARNING": logging.WARNING,
36
+ "ERROR": logging.ERROR,
37
+ "CRITICAL": logging.CRITICAL,
38
+ "NO_LOG": logging.CRITICAL + 1,
39
+ }
40
+
41
+
42
+ @contextmanager
43
+ def mute_log(level=logging.ERROR):
44
+ """
45
+ Temporarily mute the log, simply works as follows:
46
+
47
+ with mute_log():
48
+ ...
49
+ """
50
+ original_level = log.getEffectiveLevel()
51
+ log.setLevel(level)
52
+ try:
53
+ yield
54
+ finally:
55
+ log.setLevel(original_level)
qlever/qlever_main.py ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # PYTHON_ARGCOMPLETE_OK
3
+
4
+ # Copyright 2024, University of Freiburg,
5
+ # Chair of Algorithms and Data Structures
6
+ # Author: Hannah Bast <bast@cs.uni-freiburg.de>
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import traceback
12
+
13
+ from termcolor import colored
14
+
15
+ from qlever import command_objects, script_name
16
+ from qlever.config import ConfigException, QleverConfig
17
+ from qlever.log import log, log_levels
18
+
19
+
20
+ def main():
21
+ # Parse the command line arguments and read the Qleverfile.
22
+ try:
23
+ qlever_config = QleverConfig()
24
+ args = qlever_config.parse_args()
25
+ except ConfigException as e:
26
+ log.error(e)
27
+ log.info("")
28
+ log.info(traceback.format_exc())
29
+ exit(1)
30
+
31
+ # Execute the command.
32
+ command_object = command_objects[args.command]
33
+ log.setLevel(log_levels[args.log_level])
34
+ try:
35
+ log.info("")
36
+ log.info(colored(f"Command: {args.command}", attrs=["bold"]))
37
+ log.info("")
38
+ command_successful = command_object.execute(args)
39
+ log.info("")
40
+ if not command_successful:
41
+ exit(1)
42
+ except KeyboardInterrupt:
43
+ log.warn("\rCtrl-C pressed, exiting ...")
44
+ log.info("")
45
+ exit(1)
46
+ except Exception as e:
47
+ # Check if it's a certain kind of `AttributeError` and give a hint in
48
+ # that case.
49
+ log.debug(
50
+ "Command failed with exception, full traceback: "
51
+ f"{traceback.format_exc()}"
52
+ )
53
+ match_error = re.search(r"object has no attribute '(.+)'", str(e))
54
+ match_trace = re.search(
55
+ rf"({script_name}/commands/.+\.py)\", line (\d+)",
56
+ traceback.format_exc(),
57
+ )
58
+ if isinstance(e, AttributeError) and match_error and match_trace:
59
+ attribute = match_error.group(1)
60
+ trace_command = match_trace.group(1)
61
+ trace_line = match_trace.group(2)
62
+ log.error(f"{e} in `{trace_command}` at line {trace_line}")
63
+ log.info("")
64
+ log.info(
65
+ f"Likely cause: you used `args.{attribute}`, but it was "
66
+ f"neither defined in `relevant_qleverfile_arguments` "
67
+ f"nor in `additional_arguments`"
68
+ )
69
+ log.info("")
70
+ log.info(
71
+ f"If you did not implement `{trace_command}` yourself, "
72
+ f"please report this issue"
73
+ )
74
+ log.info("")
75
+ else:
76
+ log.error(f"An unexpected error occurred: {e}")
77
+ log.info("")
78
+ log.info(traceback.format_exc())
79
+ exit(1)