Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
22 changes: 22 additions & 0 deletions src/qeval/base_commands/benchmark_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from abc import abstractmethod

from qeval.base_commands.index_stats import BaseIndexStatsCommand
from qlever.commands.benchmark_queries import (
BenchmarkQueriesCommand as QleverBenchmarkQueriesCommand,
)


class BaseBenchmarkQueriesCommand(QleverBenchmarkQueriesCommand):
"""
Run benchmark queries against the SPARQL endpoint of an engine.
"""

@abstractmethod
def index_stats_command(self) -> BaseIndexStatsCommand:
"""The command that reads the index time and size of the engine."""

def can_clear_cache(self, args, engine: str | None) -> bool:
# Clearing the cache before each query only works for QLever.
return False
80 changes: 80 additions & 0 deletions src/qeval/base_commands/index_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from __future__ import annotations

from abc import abstractmethod
from pathlib import Path

from qlever.commands.index_stats import (
IndexStatsCommand as QleverIndexStatsCommand,
)
from qlever.commands.index_stats import (
get_size_unit,
get_size_unit_factor,
get_time_unit,
get_time_unit_factor,
)
from qlever.log import log
from qlever.util import get_total_file_size


def read_index_log(log_file_name: str | Path) -> str | None:
"""
The text of the index log, or None if it cannot be read, which is
reported as an error.
"""
try:
return Path(log_file_name).read_text(errors="replace")
except OSError as e:
log.error(f"Problem reading index log file {log_file_name}: {e}")
return None


class BaseIndexStatsCommand(QleverIndexStatsCommand):
"""
Show how long the index build of an engine took and how much space its
index uses, from the durations in its index log and the size of its
index files.
"""

@abstractmethod
def index_size_patterns(self, args) -> list[str]:
"""The glob patterns of the index files, whose sizes add up."""

@abstractmethod
def parse_index_durations(self, log_text: str) -> dict[str, float]:
"""
How long each phase of the build took in seconds, from the text of
the index log, in the order to show them.
"""

def index_durations(self, log_file_name: str | Path) -> dict[str, float]:
"""
Read the index log and parse it. Empty if the log cannot be read.
"""
log_text = read_index_log(log_file_name)
if log_text is None:
return {}
return self.parse_index_durations(log_text)

def execute_time(
self, args, log_file_name: str
) -> dict[str, tuple[float | None, str]]:
"""
The duration of each phase, all in the same unit, picked to suit
the longest one.
"""
durations = self.index_durations(log_file_name)
if not durations:
return {}
time_unit = get_time_unit(args.time_unit, max(durations.values()))
unit_factor = get_time_unit_factor(time_unit)
return {
heading: (seconds / unit_factor, time_unit)
for heading, seconds in durations.items()
}

def execute_space(self, args) -> dict[str, tuple[float, str]]:
"""The total size of the index files, in a unit that suits it."""
index_size = get_total_file_size(self.index_size_patterns(args))
size_unit = get_size_unit(args.size_unit, index_size)
unit_factor = get_size_unit_factor(size_unit)
return {"TOTAL size": (index_size / unit_factor, size_unit)}
25 changes: 25 additions & 0 deletions src/qeval/base_commands/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from qlever.commands.query import QueryCommand as QleverQueryCommand


class BaseQueryCommand(QleverQueryCommand):
"""
Send a query to the SPARQL endpoint of an engine. QLever's option to pin
the result to its cache, and the access token that this needs, are left
out.
"""

def relevant_qleverfile_arguments(self) -> dict[str, list[str]]:
return {"server": ["host_name", "port"]}

def accept_header_choices(self) -> list[str]:
return [
"text/tab-separated-values",
"text/csv",
"application/sparql-results+json",
"application/sparql-results+xml",
]

def can_pin_to_cache(self) -> bool:
return False
82 changes: 82 additions & 0 deletions src/qeval/base_commands/setup_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

from abc import abstractmethod
from pathlib import Path

from qlever.commands.setup_config import (
SetupConfigCommand as QleverSetupConfigCommand,
)
from qlever.commands.setup_config import check_qleverfile_exists
from qlever.log import log
from qlever.qleverfile import Qleverfile


class BaseSetupConfigCommand(QleverSetupConfigCommand):
"""
Create a Qleverfile for an engine from one of QLever's pre-configured
Qleverfiles: keep only the parts that every engine needs, then add the
engine's image and its own settings.
"""

def filter_criteria(self) -> dict[str, list[str] | None]:
"""
The sections and options of a pre-configured Qleverfile to keep, by
default those that every engine needs. `None` keeps every option of
the section. An engine that needs more can add to what this returns.
"""
return {
"data": None,
"index": ["INPUT_FILES"],
"server": ["PORT"],
"runtime": ["SYSTEM", "IMAGE"],
"ui": ["UI_CONFIG"],
}

@abstractmethod
def image(self) -> str:
"""The container image of the engine."""

@abstractmethod
def engine_settings(self, args) -> dict[str, dict[str, str]]:
"""The engine's own Qleverfile values, as {section: {option: value}}."""

def execute(self, args) -> bool:
template_path = (
self.qleverfiles_path / f"Qleverfile.{args.config_name}"
)
self.show(
f"Create a Qleverfile from {template_path}, keeping only the "
"parts that apply to this graph database",
only_show=args.show,
)
if args.show:
return True

if check_qleverfile_exists(args.main_command_name):
return False

# Each step can override the values of the steps before it.
try:
qleverfile = Qleverfile.filter(
template_path, self.filter_criteria()
)
qleverfile.set("runtime", "IMAGE", self.image())
for section, options in self.engine_settings(args).items():
# Skip sections that the pre-configured Qleverfile lacks.
if qleverfile.has_section(section):
for option, value in options.items():
qleverfile.set(section, option, value)
for section, arg_name in self.override_args:
if arg_value := getattr(args, arg_name):
qleverfile.set(section, arg_name.upper(), str(arg_value))
with Path("Qleverfile").open("w") as qleverfile_file:
qleverfile.write(qleverfile_file)
except Exception as e:
log.error(f"Could not create the Qleverfile: {e}")
return False

log.info(
f'Created Qleverfile for config "{args.config_name}" in the '
"current directory"
)
return True
20 changes: 20 additions & 0 deletions src/qeval/base_commands/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from abc import abstractmethod

from qlever.commands.status import StatusCommand as QleverStatusCommand


class BaseStatusCommand(QleverStatusCommand):
"""
Show the processes of an engine.
"""

@abstractmethod
def default_regex(self) -> str:
"""Regex for the command lines of the engine's processes."""

def description(self) -> str:
return (
"Show the processes of this graph database running on this machine"
)
86 changes: 86 additions & 0 deletions src/qeval/base_commands/stop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from __future__ import annotations

from abc import abstractmethod

from qeval.base_commands.status import BaseStatusCommand
from qlever.command import QleverCommand
from qlever.commands.stop import stop_container
from qlever.containerize import Containerize
from qlever.log import log
from qlever.util import stop_process_with_regex


class BaseStopCommand(QleverCommand):
"""
Stop the server of an engine for a given dataset: natively by killing
the processes that match a regex, in a container by stopping and
removing the server container.
"""

def __init__(self):
pass

@abstractmethod
def default_regex(self) -> str:
"""
Regex for the command line of the engine's server, where `%%NAME%%`
stands for the dataset name.
"""

@abstractmethod
def status_command(self) -> BaseStatusCommand:
"""The engine's `status` command, shown when no process matches."""

def description(self) -> str:
return "Stop the server of this graph database for a given dataset"

def should_have_qleverfile(self) -> bool:
return True

def relevant_qleverfile_arguments(self) -> dict[str, list[str]]:
return {
"data": ["name"],
"runtime": ["system", "server_container"],
}

def additional_arguments(self, subparser) -> None:
subparser.add_argument(
"--cmdline-regex",
default=self.default_regex(),
help="Stop the processes whose command line matches this regex",
)

def execute(self, args) -> bool:
# Only match the server of this dataset.
cmdline_regex = args.cmdline_regex.replace("%%NAME%%", args.name)
is_containerized = args.system in Containerize.supported_systems()
description = (
f"Checking for container with name {args.server_container}"
if is_containerized
else f'Checking for processes matching "{cmdline_regex}"'
)
self.show(description, only_show=args.show)
if args.show:
return True

if is_containerized:
if stop_container(args.server_container):
return True
log.error(
f'No container with name "{args.server_container}" found'
)
return False

stop_process_results = stop_process_with_regex(cmdline_regex)
if stop_process_results is None:
return False
if len(stop_process_results) > 0:
return all(stop_process_results)

# Nothing matched, so show what is running instead.
log.error("No matching process found")
status_command = self.status_command()
args.cmdline_regex = status_command.default_regex()
log.info("")
status_command.execute(args)
return True
3 changes: 2 additions & 1 deletion src/qeval/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def parse_command_line() -> argparse.Namespace:
tool_description = (
f"{SCRIPT_NAME} sets up, indexes, queries, and benchmarks "
"graph databases in a uniform way. "
f"Supported engines: {supported_engines}"
f"Supported graph databases: {supported_engines}"
)
parser = argparse.ArgumentParser(
description=colored(tool_description, attrs=["bold"])
Expand Down Expand Up @@ -99,6 +99,7 @@ def parse_command_line() -> argparse.Namespace:
script_name=SCRIPT_NAME,
engine_short_name=engine,
engine_display_name=engine_info.display_name,
endpoint_path=engine_info.endpoint_path,
main_command_name=main_command_name,
)
subparsers = engine_parser.add_subparsers(
Expand Down
9 changes: 6 additions & 3 deletions src/qeval/engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@

class Engine(NamedTuple):
"""
The package with the commands of an engine, and the name of the engine as
shown to the user.
The package with the commands of an engine, the name of the engine as
shown to the user, and the path of its SPARQL endpoint on the server.
"""

package: str
display_name: str
endpoint_path: str


ENGINES = {
"qlever": Engine(package="qlever", display_name="QLever"),
"qlever": Engine(
package="qlever", display_name="QLever", endpoint_path="/"
),
}


Expand Down
Loading
Loading