qlever 0.5.21__py3-none-any.whl → 0.5.22__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.

Potentially problematic release.


This version of qlever might be problematic. Click here for more details.

qlever/__init__.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
+ from importlib import import_module
4
5
  from pathlib import Path
5
6
 
6
7
 
@@ -10,9 +11,19 @@ def snake_to_camel(str):
10
11
  return "".join([w.capitalize() for w in str.replace("-", "_").split("_")])
11
12
 
12
13
 
14
+ # Get the name of the script (without the path and without the extension).
15
+ script_name = Path(sys.argv[0]).stem
16
+
17
+ ENGINE_NAMES = {
18
+ "qlever": "QLever",
19
+ "qmdb": "MillenniumDB",
20
+ }
21
+ # Default engine_name = script_name without starting 'q' and capitalized
22
+ engine_name = ENGINE_NAMES.get(script_name, script_name[1:].capitalize())
23
+
13
24
  # Each module in `qlever/commands` corresponds to a command. The name
14
25
  # of the command is the base name of the module file.
15
- package_path = Path(__file__).parent
26
+ package_path = Path(__file__).parent.parent / script_name
16
27
  command_names = [
17
28
  Path(p).stem
18
29
  for p in package_path.glob("commands/*.py")
@@ -22,19 +33,15 @@ command_names = [
22
33
  # Dynamically load all the command classes and create an object for each.
23
34
  command_objects = {}
24
35
  for command_name in command_names:
25
- module_path = f"qlever.commands.{command_name}"
26
- class_name = snake_to_camel(command_name) + "Command"
36
+ module_path = f"{script_name}.commands.{command_name}"
27
37
  try:
28
- module = __import__(module_path, fromlist=[class_name])
38
+ module = import_module(module_path)
29
39
  except ImportError as e:
30
40
  raise Exception(
31
- f"Could not import class {class_name} from module "
32
- f"{module_path} for command {command_name}: {e}"
33
- )
41
+ f"Could not import module {module_path} for {script_name}: {e}"
42
+ ) from e
34
43
  # Create an object of the class and store it in the dictionary. For the
35
44
  # commands, take - instead of _.
45
+ class_name = snake_to_camel(command_name) + "Command"
36
46
  command_class = getattr(module, class_name)
37
47
  command_objects[command_name.replace("_", "-")] = command_class()
38
-
39
- # Get the name of the script (without the path and without the extension).
40
- script_name = Path(sys.argv[0]).stem
qlever/command.py CHANGED
@@ -81,7 +81,7 @@ class QleverCommand(ABC):
81
81
  log.info("")
82
82
  if only_show:
83
83
  log.info(
84
- 'You called "qlever ... --show", therefore the command '
84
+ 'You passed the argument "--show", therefore the command '
85
85
  'is only shown, but not executed (omit the "--show" to '
86
86
  "execute it)"
87
87
  )
@@ -28,7 +28,7 @@ class CacheStatsCommand(QleverCommand):
28
28
  def additional_arguments(self, subparser) -> None:
29
29
  subparser.add_argument("--server-url",
30
30
  help="URL of the QLever server, default is "
31
- "localhost:{port}")
31
+ "{host_name}:{port}")
32
32
  subparser.add_argument("--detailed",
33
33
  action="store_true",
34
34
  default=False,
@@ -37,7 +37,7 @@ class CacheStatsCommand(QleverCommand):
37
37
  def execute(self, args) -> bool:
38
38
  # Construct the two curl commands.
39
39
  server_url = (args.server_url if args.server_url
40
- else f"localhost:{args.port}")
40
+ else f"{args.host_name}:{args.port}")
41
41
  cache_stats_cmd = (f"curl -s {server_url} "
42
42
  f"--data-urlencode \"cmd=cache-stats\"")
43
43
  cache_settings_cmd = (f"curl -s {server_url} "
@@ -17,22 +17,25 @@ class ClearCacheCommand(QleverCommand):
17
17
  pass
18
18
 
19
19
  def description(self) -> str:
20
- return ("Clear the query processing cache")
20
+ return "Clear the query processing cache"
21
21
 
22
22
  def should_have_qleverfile(self) -> bool:
23
23
  return True
24
24
 
25
- def relevant_qleverfile_arguments(self) -> dict[str: list[str]]:
26
- return {"server": ["port", "access_token"]}
25
+ def relevant_qleverfile_arguments(self) -> dict[str : list[str]]:
26
+ return {"server": ["host_name", "port", "access_token"]}
27
27
 
28
28
  def additional_arguments(self, subparser) -> None:
29
- subparser.add_argument("--server-url",
30
- help="URL of the QLever server, default is "
31
- "localhost:{port}")
32
- subparser.add_argument("--complete", action="store_true",
33
- default=False,
34
- help="Clear the cache completely, including "
35
- "the pinned queries")
29
+ subparser.add_argument(
30
+ "--server-url",
31
+ help="URL of the QLever server, default is {host_name}:{port}",
32
+ )
33
+ subparser.add_argument(
34
+ "--complete",
35
+ action="store_true",
36
+ default=False,
37
+ help="Clear the cache completely, including the pinned queries",
38
+ )
36
39
 
37
40
  def execute(self, args) -> bool:
38
41
  # Construct command line and show it.
@@ -40,22 +43,27 @@ class ClearCacheCommand(QleverCommand):
40
43
  if args.server_url:
41
44
  clear_cache_cmd += f" {args.server_url}"
42
45
  else:
43
- clear_cache_cmd += f" localhost:{args.port}"
46
+ clear_cache_cmd += f" {args.host_name}:{args.port}"
44
47
  cmd_val = "clear-cache-complete" if args.complete else "clear-cache"
45
- clear_cache_cmd += f" --data-urlencode \"cmd={cmd_val}\""
48
+ clear_cache_cmd += f' --data-urlencode "cmd={cmd_val}"'
46
49
  if args.complete:
47
- clear_cache_cmd += (f" --data-urlencode access-token="
48
- f"\"{args.access_token}\"")
50
+ clear_cache_cmd += (
51
+ f" --data-urlencode access-token=" f'"{args.access_token}"'
52
+ )
49
53
  self.show(clear_cache_cmd, only_show=args.show)
50
54
  if args.show:
51
55
  return True
52
56
 
53
57
  # Execute the command.
54
58
  try:
55
- clear_cache_cmd += " -w \" %{http_code}\""
56
- result = subprocess.run(clear_cache_cmd, shell=True,
57
- capture_output=True, text=True,
58
- check=True).stdout
59
+ clear_cache_cmd += ' -w " %{http_code}"'
60
+ result = subprocess.run(
61
+ clear_cache_cmd,
62
+ shell=True,
63
+ capture_output=True,
64
+ text=True,
65
+ check=True,
66
+ ).stdout
59
67
  match = re.match(r"^(.*) (\d+)$", result, re.DOTALL)
60
68
  if not match:
61
69
  raise Exception(f"Unexpected output:\n{result}")
@@ -77,6 +85,8 @@ class ClearCacheCommand(QleverCommand):
77
85
  log.info("")
78
86
  args.detailed = False
79
87
  if not CacheStatsCommand().execute(args):
80
- log.error("Clearing the cache was successful, but showing the "
81
- "cache stats failed {e}")
88
+ log.error(
89
+ "Clearing the cache was successful, but showing the "
90
+ "cache stats failed {e}"
91
+ )
82
92
  return True
@@ -31,7 +31,7 @@ class ExampleQueriesCommand(QleverCommand):
31
31
  return False
32
32
 
33
33
  def relevant_qleverfile_arguments(self) -> dict[str : list[str]]:
34
- return {"server": ["port"], "ui": ["ui_config"]}
34
+ return {"server": ["host_name", "port"], "ui": ["ui_config"]}
35
35
 
36
36
  def additional_arguments(self, subparser) -> None:
37
37
  subparser.add_argument(
@@ -240,7 +240,7 @@ class ExampleQueriesCommand(QleverCommand):
240
240
  sparql_endpoint = (
241
241
  args.sparql_endpoint
242
242
  if args.sparql_endpoint
243
- else f"localhost:{args.port}"
243
+ else f"{args.host_name}:{args.port}"
244
244
  )
245
245
  self.show(
246
246
  f"Obtain queries via: {get_queries_cmd}\n"
qlever/commands/index.py CHANGED
@@ -9,6 +9,7 @@ from qlever.command import QleverCommand
9
9
  from qlever.containerize import Containerize
10
10
  from qlever.log import log
11
11
  from qlever.util import (
12
+ binary_exists,
12
13
  get_existing_index_files,
13
14
  get_total_file_size,
14
15
  run_command,
@@ -267,16 +268,7 @@ class IndexCommand(QleverCommand):
267
268
 
268
269
  # When running natively, check if the binary exists and works.
269
270
  if args.system == "native":
270
- try:
271
- run_command(f"{args.index_binary} --help")
272
- except Exception as e:
273
- log.error(
274
- f'Running "{args.index_binary}" failed, '
275
- f"set `--index-binary` to a different binary or "
276
- f"set `--system to a container system`"
277
- )
278
- log.info("")
279
- log.info(f"The error message was: {e}")
271
+ if not binary_exists(args.index_binary, "index-binary"):
280
272
  return False
281
273
 
282
274
  # Check if all of the input files exist.
qlever/commands/query.py CHANGED
@@ -38,7 +38,7 @@ class QueryCommand(QleverCommand):
38
38
  return False
39
39
 
40
40
  def relevant_qleverfile_arguments(self) -> dict[str : list[str]]:
41
- return {"server": ["port", "access_token"]}
41
+ return {"server": ["host_name", "port", "access_token"]}
42
42
 
43
43
  def additional_arguments(self, subparser) -> None:
44
44
  subparser.add_argument(
@@ -109,7 +109,7 @@ class QueryCommand(QleverCommand):
109
109
  sparql_endpoint = (
110
110
  args.sparql_endpoint
111
111
  if args.sparql_endpoint
112
- else f"localhost:{args.port}"
112
+ else f"{args.host_name}:{args.port}"
113
113
  )
114
114
  curl_cmd = (
115
115
  f"curl -s {sparql_endpoint}"
@@ -32,6 +32,7 @@ class SettingsCommand(QleverCommand):
32
32
  "cache-max-num-entries",
33
33
  "cache-max-size",
34
34
  "cache-max-size-single-entry",
35
+ "cache-service-results",
35
36
  "default-query-timeout",
36
37
  "group-by-disable-index-scan-optimizations",
37
38
  "group-by-hash-map-enabled",
qlever/commands/start.py CHANGED
@@ -10,7 +10,7 @@ from qlever.commands.stop import StopCommand
10
10
  from qlever.commands.warmup import WarmupCommand
11
11
  from qlever.containerize import Containerize
12
12
  from qlever.log import log
13
- from qlever.util import is_qlever_server_alive, run_command
13
+ from qlever.util import binary_exists, is_qlever_server_alive, run_command
14
14
 
15
15
 
16
16
  # Construct the command line based on the config file.
@@ -71,22 +71,6 @@ def wrap_command_in_container(args, start_cmd) -> str:
71
71
  return start_cmd
72
72
 
73
73
 
74
- # When running natively, check if the binary exists and works.
75
- def check_binary(binary) -> bool:
76
- try:
77
- run_command(f"{binary} --help")
78
- return True
79
- except Exception as e:
80
- log.error(
81
- f'Running "{binary}" failed, '
82
- f"set `--server-binary` to a different binary or "
83
- f"set `--system to a container system`"
84
- )
85
- log.info("")
86
- log.info(f"The error message was: {e}")
87
- return False
88
-
89
-
90
74
  # Set the index description.
91
75
  def set_index_description(access_arg, port, desc) -> bool:
92
76
  curl_cmd = (
@@ -160,12 +144,6 @@ class StartCommand(QleverCommand):
160
144
  }
161
145
 
162
146
  def additional_arguments(self, subparser) -> None:
163
- # subparser.add_argument("--kill-existing-with-same-name",
164
- # action="store_true",
165
- # default=False,
166
- # help="If a QLever server is already running "
167
- # "with the same name, kill it before "
168
- # "starting a new server")
169
147
  subparser.add_argument(
170
148
  "--kill-existing-with-same-port",
171
149
  action="store_true",
@@ -226,12 +204,11 @@ class StartCommand(QleverCommand):
226
204
 
227
205
  # When running natively, check if the binary exists and works.
228
206
  if args.system == "native":
229
- ret = check_binary(args.server_binary)
230
- if not ret:
207
+ if not binary_exists(args.server_binary, "server-binary"):
231
208
  return False
232
209
 
233
210
  # Check if a QLever server is already running on this port.
234
- endpoint_url = f"http://localhost:{args.port}"
211
+ endpoint_url = f"http://{args.host_name}:{args.port}"
235
212
  if is_qlever_server_alive(endpoint_url):
236
213
  log.error(f"QLever server already running on {endpoint_url}")
237
214
  log.info("")
qlever/commands/stop.py CHANGED
@@ -1,37 +1,25 @@
1
1
  from __future__ import annotations
2
- import re
3
- import psutil
2
+
4
3
  from qlever.command import QleverCommand
5
4
  from qlever.commands.status import StatusCommand
6
5
  from qlever.containerize import Containerize
7
6
  from qlever.log import log
8
- from qlever.util import show_process_info
9
-
10
- # try to kill the given process, return true iff it was killed successfully.
11
- # the process_info is used for logging.
12
- def stop_process(proc, pinfo):
13
- try:
14
- proc.kill()
15
- log.info(f"Killed process {pinfo['pid']}")
16
- return True
17
- except Exception as e:
18
- log.error(f"Could not kill process with PID "
19
- f"{pinfo['pid']} ({e}) ... try to kill it "
20
- f"manually")
21
- log.info("")
22
- show_process_info(proc, "", show_heading=True)
23
- return False
7
+ from qlever.util import stop_process_with_regex
24
8
 
25
9
 
26
- # try to stop and remove container. return True iff it was stopped
27
- # successfully. Gives log info accordingly.
28
- def stop_container(server_container):
10
+ def stop_container(server_container: str) -> bool:
11
+ """
12
+ Try to stop and remove container. return True iff it was stopped
13
+ successfully. Gives log info accordingly.
14
+ """
29
15
  for container_system in Containerize.supported_systems():
30
16
  if Containerize.stop_and_remove_container(
31
- container_system, server_container):
32
- log.info(f"{container_system.capitalize()} container with "
33
- f"name \"{server_container}\" stopped "
34
- f" and removed")
17
+ container_system, server_container
18
+ ):
19
+ log.info(
20
+ f"{container_system.capitalize()} container with "
21
+ f'name "{server_container}" stopped and removed'
22
+ )
35
23
  return True
36
24
  return False
37
25
 
@@ -45,7 +33,7 @@ class StopCommand(QleverCommand):
45
33
  pass
46
34
 
47
35
  def description(self) -> str:
48
- return "Stop QLever server for a given datasedataset or port"
36
+ return "Stop QLever server for a given dataset or port"
49
37
 
50
38
  def should_have_qleverfile(self) -> bool:
51
39
  return True
@@ -85,21 +73,9 @@ class StopCommand(QleverCommand):
85
73
  # Check if there is a process running on the server port using psutil.
86
74
  # NOTE: On MacOS, some of the proc's returned by psutil.process_iter()
87
75
  # no longer exist when we try to access them, so we just skip them.
88
- stop_process_results = []
89
- for proc in psutil.process_iter():
90
- try:
91
- pinfo = proc.as_dict(
92
- attrs=['pid', 'username', 'create_time',
93
- 'memory_info', 'cmdline'])
94
- cmdline = " ".join(pinfo['cmdline'])
95
- except Exception as e:
96
- log.debug(f"Error getting process info: {e}")
97
- return False
98
- if re.search(cmdline_regex, cmdline):
99
- log.info(f"Found process {pinfo['pid']} from user "
100
- f"{pinfo['username']} with command line: {cmdline}")
101
- log.info("")
102
- stop_process_results.append(stop_process(proc, pinfo))
76
+ stop_process_results = stop_process_with_regex(cmdline_regex)
77
+ if stop_process_results is None:
78
+ return False
103
79
  if len(stop_process_results) > 0:
104
80
  return all(stop_process_results)
105
81
 
qlever/commands/ui.py CHANGED
@@ -1,12 +1,37 @@
1
1
  from __future__ import annotations
2
2
 
3
- import subprocess
4
3
  from os import environ
4
+ from pathlib import Path
5
+
6
+ import yaml
5
7
 
6
8
  from qlever.command import QleverCommand
7
9
  from qlever.containerize import Containerize
8
10
  from qlever.log import log
9
- from qlever.util import is_port_used
11
+ from qlever.util import is_port_used, run_command
12
+
13
+
14
+ # Return a YAML string for the given dictionary. Format values with
15
+ # newlines using the "|" style.
16
+ def dict_to_yaml(dictionary):
17
+ # Custom representer for yaml, which uses the "|" style only for
18
+ # multiline strings.
19
+ #
20
+ # NOTE: We replace all `\r\n` with `\n` because otherwise the `|` style
21
+ # does not work as expected.
22
+ class MultiLineDumper(yaml.Dumper):
23
+ def represent_scalar(self, tag, value, style=None):
24
+ value = value.replace("\r\n", "\n")
25
+ if isinstance(value, str) and "\n" in value:
26
+ style = "|"
27
+ return super().represent_scalar(tag, value, style)
28
+
29
+ # Dump as yaml.
30
+ return yaml.dump(
31
+ dictionary,
32
+ sort_keys=False,
33
+ Dumper=MultiLineDumper,
34
+ )
10
35
 
11
36
 
12
37
  class UiCommand(QleverCommand):
@@ -37,7 +62,30 @@ class UiCommand(QleverCommand):
37
62
  }
38
63
 
39
64
  def additional_arguments(self, subparser) -> None:
40
- pass
65
+ subparser.add_argument(
66
+ "--ui-config-file",
67
+ default="Qleverfile-ui.yml",
68
+ help="Name of the config file for the QLever UI "
69
+ "(default: Qleverfile-ui.yml)",
70
+ )
71
+ subparser.add_argument(
72
+ "--ui-db-file",
73
+ help="Name of the database file for the QLever UI "
74
+ "(default: {name}.ui-db.sqlite3)",
75
+ )
76
+ subparser.add_argument(
77
+ "--no-pull-latest",
78
+ action="store_true",
79
+ default=False,
80
+ help="Do not pull the latest image for the QLever UI "
81
+ "(default: pull the latest image if image name contains '/')",
82
+ )
83
+ subparser.add_argument(
84
+ "--stop",
85
+ action="store_true",
86
+ default=False,
87
+ help="Stop the running container",
88
+ )
41
89
 
42
90
  def execute(self, args) -> bool:
43
91
  # If QLEVER_OVERRIDE_DISABLE_UI is set, this command is disabled.
@@ -59,57 +107,161 @@ class UiCommand(QleverCommand):
59
107
  log.info("")
60
108
 
61
109
  # Construct commands and show them.
62
- server_url = f"http://{args.host_name}:{args.port}"
110
+ pull_latest_image = "/" in args.ui_image and not args.no_pull_latest
111
+ ui_config_name = args.name
112
+ ui_db_file = args.ui_db_file or f"{args.name}.ui-db.sqlite3"
113
+ ui_db_file_from_image = "qleverui.sqlite3"
114
+ ui_config_file = args.ui_config_file
115
+ sparql_endpoint = f"http://{args.host_name}:{args.port}"
63
116
  ui_url = f"http://{args.host_name}:{args.ui_port}"
64
117
  pull_cmd = f"{args.ui_system} pull -q {args.ui_image}"
65
- run_cmd = (
118
+ get_db_cmd = (
119
+ f"{args.ui_system} create "
120
+ f"--name {args.ui_container} "
121
+ f"{args.ui_image} "
122
+ f"&& {args.ui_system} cp "
123
+ f"{args.ui_container}:/app/db/{ui_db_file_from_image} {ui_db_file} "
124
+ f"&& {args.ui_system} rm -f {args.ui_container}"
125
+ )
126
+ start_ui_cmd = (
66
127
  f"{args.ui_system} run -d "
128
+ f"--volume $(pwd):/app/db "
129
+ f"--env QLEVERUI_DATABASE_URL=sqlite:////app/db/{ui_db_file} "
67
130
  f"--publish {args.ui_port}:7000 "
68
131
  f"--name {args.ui_container} "
69
132
  f"{args.ui_image}"
70
133
  )
71
- exec_cmd = (
72
- f"{args.ui_system} exec -it "
134
+ get_config_cmd = (
135
+ f"{args.ui_system} exec -i "
73
136
  f"{args.ui_container} "
74
- f'bash -c "python manage.py configure '
75
- f'{args.ui_config} {server_url}"'
137
+ f'bash -c "python manage.py config {ui_config_name}"'
76
138
  )
77
- self.show(
78
- "\n".join(
79
- ["Stop running containers", pull_cmd, run_cmd, exec_cmd]
80
- ),
81
- only_show=args.show,
139
+ set_config_cmd = (
140
+ f"{args.ui_system} exec -i "
141
+ f"{args.ui_container} "
142
+ f'bash -c "python manage.py config {ui_config_name} '
143
+ f'/app/db/{ui_config_file} --hide-all-other-backends"'
82
144
  )
145
+ commands_to_show = []
146
+ if not args.stop:
147
+ if pull_latest_image:
148
+ commands_to_show.append(pull_cmd)
149
+ if not Path(ui_db_file).exists():
150
+ commands_to_show.append(get_db_cmd)
151
+ commands_to_show.append(start_ui_cmd)
152
+ if not Path(ui_config_file).exists():
153
+ commands_to_show.append(get_config_cmd)
154
+ else:
155
+ commands_to_show.append(set_config_cmd)
156
+ self.show("\n".join(commands_to_show), only_show=args.show)
83
157
  if qlever_is_running_in_container:
84
158
  return False
85
159
  if args.show:
86
160
  return True
87
161
 
88
162
  # Stop running containers.
163
+ was_found_and_stopped = False
89
164
  for container_system in Containerize.supported_systems():
90
- Containerize.stop_and_remove_container(
165
+ was_found_and_stopped |= Containerize.stop_and_remove_container(
91
166
  container_system, args.ui_container
92
167
  )
168
+ if was_found_and_stopped:
169
+ log.debug(f"Stopped and removed container `{args.ui_container}`")
170
+ else:
171
+ log.debug(f"No container with name `{args.ui_container}` found")
172
+ if args.stop:
173
+ return True
174
+
175
+ # Pull the latest image.
176
+ if pull_latest_image:
177
+ log.debug(f"Pulling image `{args.ui_image}` for QLever UI")
178
+ run_command(pull_cmd)
93
179
 
94
180
  # Check if the UI port is already being used.
95
181
  if is_port_used(args.ui_port):
96
182
  log.warning(
97
- f"It looks like the specified port for the UI ({args.ui_port}) is already in use. You can set another port in the Qleverfile in the [ui] section with the UI_PORT variable."
183
+ f"It looks like port {args.ui_port} for the QLever UI "
184
+ f"is already in use. You can set another port in the "
185
+ f" Qleverfile in the [ui] section with the UI_PORT variable."
98
186
  )
99
187
 
100
- # Try to start the QLever UI.
188
+ # Get the QLever UI database from the image, unless it already exists.
189
+ if Path(ui_db_file).exists():
190
+ log.debug(f"Found QLever UI database `{ui_db_file}`, reusing it")
191
+ else:
192
+ log.debug(f"Getting QLever UI database `{ui_db_file}` from image")
193
+ try:
194
+ run_command(get_db_cmd)
195
+ except Exception as e:
196
+ log.error(
197
+ f"Failed to get {ui_db_file} from {args.ui_image} "
198
+ f"({e})"
199
+ )
200
+ return False
201
+
202
+ # Start the QLever UI.
203
+ try:
204
+ log.debug(
205
+ f"Starting new container with name `{args.ui_container}`"
206
+ )
207
+ run_command(start_ui_cmd)
208
+ except Exception as e:
209
+ log.error(f"Failed to start container `{args.ui_container}` ({e})")
210
+ return False
211
+
212
+ # Check if config file with name `ui_config_file` exists. If not, try
213
+ # to obtain it via `get_config_cmd` and set it as default.
214
+ if Path(ui_config_file).exists():
215
+ log.info(f"Found config file `{ui_config_file}` and reusing it")
216
+ else:
217
+ try:
218
+ log.info(
219
+ f"Get default config file `{ui_config_file}` from image "
220
+ f"`{args.ui_image}` and set endpoint to `{sparql_endpoint}`"
221
+ )
222
+ config_yaml = run_command(get_config_cmd, return_output=True)
223
+ config_dict = yaml.safe_load(config_yaml)
224
+ except Exception as e:
225
+ log.error("")
226
+ log.error(
227
+ f"An error occured while getting and parsing the "
228
+ f"config file ({e})"
229
+ )
230
+ return False
231
+ try:
232
+ config_dict["config"]["backend"]["isDefault"] = True
233
+ config_dict["config"]["backend"]["baseUrl"] = sparql_endpoint
234
+ config_dict["config"]["backend"]["sortKey"] = 1
235
+ config_yaml = dict_to_yaml(config_dict)
236
+ with open(ui_config_file, "w") as f:
237
+ f.write(config_yaml)
238
+ except Exception as e:
239
+ log.error("")
240
+ log.error(
241
+ f"An error occured while modifying and writing the "
242
+ f"config file ({e})"
243
+ )
244
+ return False
245
+
246
+ # Configure the QLever UI.
101
247
  try:
102
- subprocess.run(pull_cmd, shell=True, stdout=subprocess.DEVNULL)
103
- subprocess.run(run_cmd, shell=True, stdout=subprocess.DEVNULL)
104
- subprocess.run(exec_cmd, shell=True, stdout=subprocess.DEVNULL)
105
- except subprocess.CalledProcessError as e:
106
- log.error(f"Failed to start the QLever UI ({e})")
248
+ run_command(set_config_cmd)
249
+ except Exception as e:
250
+ log.error(f"Failed to configure the QLever UI ({e})")
107
251
  return False
108
252
 
109
- # Success.
253
+ # If we come this far, everything should work.
254
+ log.info("")
255
+ log.info(
256
+ f"The QLever UI should now be up at {ui_url}/{ui_config_name}"
257
+ )
258
+ log.info("")
259
+ log.debug(
260
+ "If you must, you can log in as QLever UI admin with "
261
+ 'username and password "demo"'
262
+ )
110
263
  log.info(
111
- f"The QLever UI should now be up at {ui_url} ..."
112
- f"You can log in as QLever UI admin with username and "
113
- f'password "demo"'
264
+ f"You can modify the config file at `{ui_config_file}` "
265
+ f"and then just run `qlever ui` again"
114
266
  )
115
267
  return True
qlever/config.py CHANGED
@@ -9,7 +9,7 @@ from pathlib import Path
9
9
  import argcomplete
10
10
  from termcolor import colored
11
11
 
12
- from qlever import command_objects, script_name
12
+ from qlever import command_objects, engine_name, script_name
13
13
  from qlever.log import log, log_levels
14
14
  from qlever.qleverfile import Qleverfile
15
15
 
@@ -176,11 +176,18 @@ class QleverConfig:
176
176
  # are defined in the modules in `qlever/commands`. In `__init__.py`
177
177
  # an object of each class is created and stored in `command_objects`.
178
178
  parser = argparse.ArgumentParser(
179
- description=colored("This is the qlever command line tool, "
180
- "it's all you need to work with QLever",
181
- attrs=["bold"]))
182
- parser.add_argument("--version", action="version",
183
- version=f"%(prog)s {version('qlever')}")
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
+ )
184
191
  add_qleverfile_option(parser)
185
192
  subparsers = parser.add_subparsers(dest='command')
186
193
  subparsers.required = True
qlever/containerize.py CHANGED
@@ -9,7 +9,7 @@ import subprocess
9
9
  from typing import Optional
10
10
 
11
11
  from qlever.log import log
12
- from qlever.util import run_command, get_random_string
12
+ from qlever.util import get_random_string, run_command
13
13
 
14
14
 
15
15
  class ContainerizeException(Exception):
@@ -40,6 +40,7 @@ class Containerize:
40
40
  volumes: list[tuple[str, str]] = [],
41
41
  ports: list[tuple[int, int]] = [],
42
42
  working_directory: Optional[str] = None,
43
+ use_bash: bool = True,
43
44
  ) -> str:
44
45
  """
45
46
  Get the command to run `cmd` with the given `container_system` and the
@@ -80,11 +81,15 @@ class Containerize:
80
81
  f"{volume_options}"
81
82
  f"{port_options}"
82
83
  f"{working_directory_option}"
84
+ f" --name {container_name}"
83
85
  f" --init"
84
- f" --entrypoint bash"
85
- f" --name {container_name} {image_name}"
86
- f" -c {shlex.quote(cmd)}"
87
86
  )
87
+ if use_bash:
88
+ containerized_cmd += (
89
+ f" --entrypoint bash {image_name} -c {shlex.quote(cmd)}"
90
+ )
91
+ else:
92
+ containerized_cmd += f" {image_name} {cmd}"
88
93
  return containerized_cmd
89
94
 
90
95
  @staticmethod
qlever/qlever_main.py CHANGED
@@ -12,7 +12,7 @@ import traceback
12
12
 
13
13
  from termcolor import colored
14
14
 
15
- from qlever import command_objects
15
+ from qlever import command_objects, script_name
16
16
  from qlever.config import ConfigException, QleverConfig
17
17
  from qlever.log import log, log_levels
18
18
 
@@ -35,9 +35,9 @@ def main():
35
35
  log.info("")
36
36
  log.info(colored(f"Command: {args.command}", attrs=["bold"]))
37
37
  log.info("")
38
- commandWasSuccesful = command_object.execute(args)
38
+ command_successful = command_object.execute(args)
39
39
  log.info("")
40
- if not commandWasSuccesful:
40
+ if not command_successful:
41
41
  exit(1)
42
42
  except KeyboardInterrupt:
43
43
  log.info("")
@@ -47,9 +47,14 @@ def main():
47
47
  except Exception as e:
48
48
  # Check if it's a certain kind of `AttributeError` and give a hint in
49
49
  # that case.
50
+ log.debug(
51
+ "Command failed with exception, full traceback: "
52
+ f"{traceback.format_exc()}"
53
+ )
50
54
  match_error = re.search(r"object has no attribute '(.+)'", str(e))
51
55
  match_trace = re.search(
52
- r"(qlever/commands/.+\.py)\", line (\d+)", traceback.format_exc()
56
+ rf"({script_name}/commands/.+\.py)\", line (\d+)",
57
+ traceback.format_exc(),
53
58
  )
54
59
  if isinstance(e, AttributeError) and match_error and match_trace:
55
60
  attribute = match_error.group(1)
qlever/qleverfile.py CHANGED
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import re
4
+ import socket
4
5
  import subprocess
5
- from configparser import ConfigParser, ExtendedInterpolation
6
+ from configparser import ConfigParser, ExtendedInterpolation, RawConfigParser
7
+ from pathlib import Path
6
8
 
9
+ from qlever import script_name
7
10
  from qlever.containerize import Containerize
8
11
  from qlever.log import log
9
12
 
@@ -298,12 +301,12 @@ class Qleverfile:
298
301
  runtime_args["index_container"] = arg(
299
302
  "--index-container",
300
303
  type=str,
301
- help="The name of the container used by `qlever index`",
304
+ help=f"The name of the container used by `{script_name} index`",
302
305
  )
303
306
  runtime_args["server_container"] = arg(
304
307
  "--server-container",
305
308
  type=str,
306
- help="The name of the container used by `qlever start`",
309
+ help=f"The name of the container used by `{script_name} start`",
307
310
  )
308
311
 
309
312
  ui_args["ui_port"] = arg(
@@ -400,9 +403,9 @@ class Qleverfile:
400
403
  name = config["data"]["name"]
401
404
  runtime = config["runtime"]
402
405
  if "server_container" not in runtime:
403
- runtime["server_container"] = f"qlever.server.{name}"
406
+ runtime["server_container"] = f"{script_name}.server.{name}"
404
407
  if "index_container" not in runtime:
405
- runtime["index_container"] = f"qlever.index.{name}"
408
+ runtime["index_container"] = f"{script_name}.index.{name}"
406
409
  if "ui_container" not in config["ui"]:
407
410
  config["ui"]["ui_container"] = f"qlever.ui.{name}"
408
411
  index = config["index"]
@@ -414,5 +417,49 @@ class Qleverfile:
414
417
  if index.get("text_index", "none") != "none":
415
418
  server["use_text_index"] = "yes"
416
419
 
420
+ # Add other non-trivial default values.
421
+ try:
422
+ config["server"]["host_name"] = socket.gethostname()
423
+ except Exception:
424
+ log.warning(
425
+ "Could not get the hostname, using `localhost` as default"
426
+ )
427
+ pass
428
+
417
429
  # Return the parsed Qleverfile with the added inherited values.
418
430
  return config
431
+
432
+ @staticmethod
433
+ def filter(
434
+ qleverfile_path: Path, options_included: dict[str, list[str]]
435
+ ) -> RawConfigParser:
436
+ """
437
+ Given a filter criteria (key: section_header, value: list[options]),
438
+ return a RawConfigParser object to create a new filtered Qleverfile
439
+ with only the specified sections and options (selects all options if
440
+ list[options] is empty). Mainly to be used by non-qlever scripts for
441
+ the setup-config command
442
+ """
443
+ # Read the Qleverfile.
444
+ config = RawConfigParser()
445
+ config.optionxform = str # Preserve case sensitivity of keys
446
+ config.read(qleverfile_path)
447
+
448
+ filtered_config = RawConfigParser()
449
+ filtered_config.optionxform = str
450
+
451
+ for section, desired_fields in options_included.items():
452
+ if config.has_section(section):
453
+ filtered_config.add_section(section)
454
+
455
+ # If the list is empty, copy all fields
456
+ if not desired_fields:
457
+ for field, value in config.items(section):
458
+ filtered_config.set(section, field, value)
459
+ else:
460
+ for desired_field in desired_fields:
461
+ if config.has_option(section, desired_field):
462
+ value = config.get(section, desired_field)
463
+ filtered_config.set(section, desired_field, value)
464
+
465
+ return filtered_config
qlever/util.py CHANGED
@@ -10,7 +10,9 @@ import string
10
10
  import subprocess
11
11
  from datetime import date, datetime
12
12
  from pathlib import Path
13
- from typing import Optional
13
+ from typing import Any, Optional
14
+
15
+ import psutil
14
16
 
15
17
  from qlever.log import log
16
18
 
@@ -33,6 +35,7 @@ def run_command(
33
35
  cmd: str,
34
36
  return_output: bool = False,
35
37
  show_output: bool = False,
38
+ show_stderr: bool = False,
36
39
  use_popen: bool = False,
37
40
  ) -> Optional[str | subprocess.Popen]:
38
41
  """
@@ -50,7 +53,7 @@ def run_command(
50
53
  "shell": True,
51
54
  "text": True,
52
55
  "stdout": None if show_output else subprocess.PIPE,
53
- "stderr": subprocess.PIPE,
56
+ "stderr": None if show_stderr else subprocess.PIPE,
54
57
  }
55
58
 
56
59
  # With `Popen`, the command runs in the current shell and a process object
@@ -72,8 +75,8 @@ def run_command(
72
75
  raise Exception(result.stderr.replace("\n", " ").strip())
73
76
  else:
74
77
  raise Exception(
75
- f"Command failed with exit code {result.returncode}"
76
- f" but nothing written to stderr"
78
+ f"Command failed with exit code {result.returncode}, "
79
+ f" nothing written to stderr"
77
80
  )
78
81
  # Optionally, return what was written to `stdout`.
79
82
  if return_output:
@@ -242,3 +245,85 @@ def format_size(bytes, suffix="B"):
242
245
  if bytes < factor:
243
246
  return f"{bytes:.2f} {unit}{suffix}"
244
247
  bytes /= factor
248
+
249
+
250
+ def stop_process(proc: psutil.Process, pinfo: dict[str, Any]) -> bool:
251
+ """
252
+ Try to kill the given process, return True iff it was killed
253
+ successfully. The process_info is used for logging.
254
+ """
255
+ try:
256
+ proc.kill()
257
+ log.info(f"Killed process {pinfo['pid']}")
258
+ return True
259
+ except Exception as e:
260
+ log.error(
261
+ f"Could not kill process with PID "
262
+ f"{pinfo['pid']} ({e}) ... try to kill it "
263
+ f"manually"
264
+ )
265
+ log.info("")
266
+ show_process_info(proc, "", show_heading=True)
267
+ return False
268
+
269
+
270
+ def stop_process_with_regex(cmdline_regex: str) -> list[bool] | None:
271
+ """
272
+ Given a cmdline_regex for a native process, try to kill the processes that
273
+ match the regex and return a list of their stopped status (bool).
274
+ Show the matched processes as log info.
275
+ """
276
+ stop_process_results = []
277
+ for proc in psutil.process_iter():
278
+ try:
279
+ pinfo = proc.as_dict(
280
+ attrs=[
281
+ "pid",
282
+ "username",
283
+ "create_time",
284
+ "memory_info",
285
+ "cmdline",
286
+ ]
287
+ )
288
+ cmdline = " ".join(pinfo["cmdline"])
289
+ except Exception as e:
290
+ log.debug(f"Error getting process info: {e}")
291
+ return None
292
+ if re.search(cmdline_regex, cmdline):
293
+ log.info(
294
+ f"Found process {pinfo['pid']} from user "
295
+ f"{pinfo['username']} with command line: {cmdline}"
296
+ )
297
+ log.info("")
298
+ stop_process_results.append(stop_process(proc, pinfo))
299
+ return stop_process_results
300
+
301
+
302
+ def binary_exists(binary: str, cmd_arg: str) -> bool:
303
+ """
304
+ When a command is run natively, check if the binary exists on the system
305
+ """
306
+ try:
307
+ run_command(f"{binary} --help")
308
+ return True
309
+ except Exception as e:
310
+ log.error(
311
+ f'Running "{binary}" failed, '
312
+ f"set `--{cmd_arg}` to a different binary or "
313
+ f"set `--system to a container system`"
314
+ )
315
+ log.info("")
316
+ log.info(f"The error message was: {e}")
317
+ return False
318
+
319
+
320
+ def is_server_alive(url: str) -> bool:
321
+ """
322
+ Check if the server is already alive at the given endpoint url
323
+ """
324
+ check_server_cmd = f"curl -s {url}"
325
+ try:
326
+ run_command(check_server_cmd)
327
+ return True
328
+ except Exception:
329
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qlever
3
- Version: 0.5.21
3
+ Version: 0.5.22
4
4
  Summary: Command-line tool for using the QLever graph database
5
5
  Author-email: Hannah Bast <bast@cs.uni-freiburg.de>
6
6
  License: Apache-2.0
@@ -14,6 +14,7 @@ License-File: LICENSE
14
14
  Requires-Dist: psutil
15
15
  Requires-Dist: termcolor
16
16
  Requires-Dist: argcomplete
17
+ Requires-Dist: pyyaml
17
18
  Dynamic: license-file
18
19
 
19
20
  # QLever
@@ -1,11 +1,11 @@
1
- qlever/__init__.py,sha256=GlDcYX0TzV-7nAFaBzPc1Y6cNwz0rsLJFNfLNXl9UiM,1410
2
- qlever/command.py,sha256=oxsMtu445OnoZ6kKRJCUbyAGbavWcu39gLor8hWuLjQ,2765
3
- qlever/config.py,sha256=qYPy-MQ7BwGrvKSazQWhs0lnlOFqm-d47mpZhc3fptc,10254
4
- qlever/containerize.py,sha256=bnHmjKIFEe-NcuAMRNnXAFjRVLcLnk9f5JspCCyhgt8,5210
1
+ qlever/__init__.py,sha256=ZbzivHALRlzAUoFBBSbbkKFcvATBX7yVR6Q9JyEQ8ig,1631
2
+ qlever/command.py,sha256=nI-UG1GWrZ24DxD0Ly8dvupCpsD9e3n9-eskkCMTx6c,2767
3
+ qlever/config.py,sha256=gNw2_-jj1TjzhzqLOuUI_Dh19q_ViCiArrtrgXL2F4E,10354
4
+ qlever/containerize.py,sha256=G1_ei9nBnYl5-7miiy0eWjb9HMnt06X21P7iU8bm6A0,5369
5
5
  qlever/log.py,sha256=WLscWV4fFF_w_uXSOfvWzhyzRM7t_61inE2ks3zf6Gw,1317
6
- qlever/qlever_main.py,sha256=-E4W8YdZ_teszGwXu6bQgBcH3y47TFJU8JLPIDwc_-g,2449
7
- qlever/qleverfile.py,sha256=DRS-SdbNoGZKAuxUQDDYmB6yKU-uNERrZRdgdRCejdw,15573
8
- qlever/util.py,sha256=xAK9GT8SgU3z65F1dFXazxsd60letqLQqQAZ81mdJSY,8374
6
+ qlever/qlever_main.py,sha256=mkNeQ6UOX-cIR5dVwwS12qc0gsJh4b6yL0LTdU-8oN4,2612
7
+ qlever/qleverfile.py,sha256=DZ--LJ4TxWqs08bKHmGQlXQW6MPyOB--r7eMKs7E59s,17449
8
+ qlever/util.py,sha256=Gv9fE-SOZSx1Kez5z3pcukniZnDDQEKx7j9LV-iP7LM,10939
9
9
  qlever/Qleverfiles/Qleverfile.dblp,sha256=oVVPFMpKX0Lfe0HDYPuL3qYhlC-3Lz18AT2tHmJ32WE,1282
10
10
  qlever/Qleverfiles/Qleverfile.dblp-plus,sha256=TJHxp8I1P6JKJjbuAllEpB32-huuY1gH0FlenqPVJ5g,1334
11
11
  qlever/Qleverfiles/Qleverfile.dbpedia,sha256=aaNZZayE-zVePGSwPzXemkX__Ns8-kP_E7DNNKZPnqg,1160
@@ -28,26 +28,26 @@ qlever/Qleverfiles/Qleverfile.wikipathways,sha256=UFEVLrtOBiSQfibBN9xc2wDXrnWcnx
28
28
  qlever/Qleverfiles/Qleverfile.yago-4,sha256=hAS_2ZmC1zxNsKXip7t1F_iqu3CC-6O7v6HZhuFbnWY,1819
29
29
  qlever/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  qlever/commands/add_text_index.py,sha256=xJ49Iq1-CszXjHDvOpllqLw1J1kCxQl7H848XD1vEz0,3820
31
- qlever/commands/cache_stats.py,sha256=zGPLSWDNn7dwAAt2o-2ExqHmw1FeN8i6nEQbaaqF830,4156
32
- qlever/commands/clear_cache.py,sha256=n52ohE1EE4M_B4kId_v8tbAlW-BGwG1K-ZYQ9QgxIJU,2974
33
- qlever/commands/example_queries.py,sha256=4zhvrbH8IJIKy5HB-guafY8SmLFb1OwnjELnaMD8ChI,24031
31
+ qlever/commands/cache_stats.py,sha256=15C0iIahwPuxTvMDyB0UDmo45iBk97hWzVZkEMHjMA8,4165
32
+ qlever/commands/clear_cache.py,sha256=kwNayV4qtgqh_Gf5SjS6WzmfgUsJ-9YhPoWYWGoNNn8,2967
33
+ qlever/commands/example_queries.py,sha256=4oKCBYYO_uhBNGrSyAd69UA1--bPPcjp88aULaNaT-o,24051
34
34
  qlever/commands/extract_queries.py,sha256=TZBmZLz_NknU1LKbl9nPmxdb82lsPeDhTWjIo81llvA,3942
35
35
  qlever/commands/get_data.py,sha256=nHOHMjv0tSLWJDOR0ba_LK-Bk-mcGnphb8hbqcVYFhE,1411
36
- qlever/commands/index.py,sha256=H0FtJ4HBVH8ESPxmYzb--k3qR5M7PfE4A-QlovVTP_o,13400
36
+ qlever/commands/index.py,sha256=NJ6hdlYEc-4AOANON3Bpl-vhgOs21HiPQnUrAE1Flfk,13055
37
37
  qlever/commands/index_stats.py,sha256=9EBo1Oq5PGjajrvWJNafJ-Wg_d90DaO5AGq9a5plSRM,11720
38
38
  qlever/commands/log.py,sha256=vLqkgtx1udnQqoUBMWB5G9rwr-l7UKrDpyFYSMuoXWw,1987
39
- qlever/commands/query.py,sha256=lqQR3wiDLAzxg3Da5Xim6gxkAeEexPJxldoTfB9U4H0,4588
40
- qlever/commands/settings.py,sha256=cpRhD6CnQmDuzGg28zO7QTASDBGR7_PT0GRGtEVgY_g,3776
39
+ qlever/commands/query.py,sha256=IBIi1NT1n9xDobqNIXlV50ap3o958g5KHMiFJqw89z0,4608
40
+ qlever/commands/settings.py,sha256=LpfF0jhttJsqKUh9hO4mdW3SVE3361wvb2SeQWPUB7w,3813
41
41
  qlever/commands/setup_config.py,sha256=wEy1LAunpOnqrUCbazMpt1u9HJCKgXJEMxF3zjh0jb0,3344
42
- qlever/commands/start.py,sha256=9Rjn8KTkAPkOkBwlFP1vE4LrrtaJW24OuPqAdQFBYTk,11695
42
+ qlever/commands/start.py,sha256=g_5-BUiSYJjL10ae91jMA5SgI0zk4O4gPMN_BOuERmc,10854
43
43
  qlever/commands/status.py,sha256=TtnBqcdkF3zTDKft07zpVcIX7kFu7d_nOy9b6Ohh9vQ,1650
44
- qlever/commands/stop.py,sha256=z25gWfLA9qIJ7F3zWZa0p0oVnt61kHcsB1evjgXhKX4,4557
44
+ qlever/commands/stop.py,sha256=5BNKArOzoJ8kYiTVAmtN81w7nQ42fkxISgsxL-qJpO0,3463
45
45
  qlever/commands/system_info.py,sha256=I84EKgMO5J8pvsTDhkVKHzsRLtPajNg9KTQN5kWjqLU,4660
46
- qlever/commands/ui.py,sha256=T2Sl6w9tXNpZf-Zv3A9SLmarujszTIjGOYyPfyT4sX4,3821
46
+ qlever/commands/ui.py,sha256=TtFvXD1mMP0dRdsMo50wAlTffcInX0lHTJnUjtowzQ0,9757
47
47
  qlever/commands/warmup.py,sha256=kJHzS7HJo8pD2CphJuaXDj_CYP02YDo2DVM-pun3A80,1029
48
- qlever-0.5.21.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
49
- qlever-0.5.21.dist-info/METADATA,sha256=uZV8sIEroOozEZgBowu22IKPLeOztE_wjCNV0S4fJSE,4630
50
- qlever-0.5.21.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
51
- qlever-0.5.21.dist-info/entry_points.txt,sha256=U_1U6SFIEZ-AnNlvk2nzcL0e4jnjEpuSbxYZ_E0XpEg,51
52
- qlever-0.5.21.dist-info/top_level.txt,sha256=kd3zsYqiFd0--Czh5XTVkfEq6XR-XgRFW35X0v0GT-c,7
53
- qlever-0.5.21.dist-info/RECORD,,
48
+ qlever-0.5.22.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
49
+ qlever-0.5.22.dist-info/METADATA,sha256=tIwV9xjXfQA3fZ4_oyBZZC1pTmzK2Wpm-gTPvQKEg0A,4652
50
+ qlever-0.5.22.dist-info/WHEEL,sha256=GHB6lJx2juba1wDgXDNlMTyM13ckjBMKf-OnwgKOCtA,91
51
+ qlever-0.5.22.dist-info/entry_points.txt,sha256=U_1U6SFIEZ-AnNlvk2nzcL0e4jnjEpuSbxYZ_E0XpEg,51
52
+ qlever-0.5.22.dist-info/top_level.txt,sha256=kd3zsYqiFd0--Czh5XTVkfEq6XR-XgRFW35X0v0GT-c,7
53
+ qlever-0.5.22.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (78.1.0)
2
+ Generator: setuptools (80.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5