import errno
import logging
import os
import time
from typing import (
Any,
Dict,
List,
Optional,
)
from galaxy.tool_shed.galaxy_install.client import (
DataManagerInterface,
InstallationTarget,
)
from galaxy.util import (
Element,
etree,
parse_xml_string,
xml_to_string,
)
from galaxy.util.path import StrPath
from galaxy.util.renamed_temporary_file import RenamedTemporaryFile
from galaxy.util.tool_shed.xml_util import parse_xml
from . import tool_panel_manager
log = logging.getLogger(__name__)
SHED_DATA_MANAGER_CONF_XML = """<?xml version="1.0"?>
<data_managers>
</data_managers>
"""
[docs]class DataManagerHandler:
app: InstallationTarget
root: Optional[Element] = None
[docs] def __init__(self, app: InstallationTarget):
self.app = app
@property
def data_managers_path(self) -> Optional[str]:
tree, error_message = parse_xml(self.app.config.shed_data_manager_config_file)
if tree:
root = tree.getroot()
return root.get("tool_path", None)
return None
def _data_manager_config_elems_to_xml_file(self, config_elems: List[Element], config_filename: StrPath) -> None:
"""
Persist the current in-memory list of config_elems to a file named by the value
of config_filename.
"""
if data_managers_path := self.data_managers_path:
root_str = f'<?xml version="1.0"?><data_managers tool_path="{data_managers_path}"></data_managers>'
else:
root_str = '<?xml version="1.0"?><data_managers></data_managers>'
root = parse_xml_string(root_str)
for elem in config_elems:
root.append(elem)
try:
with RenamedTemporaryFile(config_filename, mode="w") as fh:
fh.write(xml_to_string(root))
except Exception:
log.exception("Exception in DataManagerHandler._data_manager_config_elems_to_xml_file")
[docs] def install_data_managers(
self,
shed_data_manager_conf_filename: StrPath,
metadata_dict: Dict[str, Any],
shed_config_dict: Dict[str, Any],
relative_install_dir: StrPath,
repository,
repository_tools_tups,
) -> List["DataManagerInterface"]:
rval: List["DataManagerInterface"] = []
if "data_manager" in metadata_dict:
tpm = tool_panel_manager.ToolPanelManager(self.app)
repository_tools_by_guid = {}
for tool_tup in repository_tools_tups:
repository_tools_by_guid[tool_tup[1]] = dict(tool_config_filename=tool_tup[0], tool=tool_tup[2])
# Load existing data managers.
try:
tree, error_message = parse_xml(shed_data_manager_conf_filename, check_exists=False)
except OSError as exc:
if exc.errno == errno.ENOENT:
with open(shed_data_manager_conf_filename, "w") as fh:
fh.write(SHED_DATA_MANAGER_CONF_XML)
tree, error_message = parse_xml(shed_data_manager_conf_filename)
else:
raise
if tree is None:
return rval
config_elems = list(
tree.getroot().__iter__()
) # `.__iter__()` is a workaround for lxml-stubs declaring _Element a subclass of Iterable["_Element"]
repo_data_manager_conf_filename = metadata_dict["data_manager"].get("config_filename", None)
if repo_data_manager_conf_filename is None:
log.debug("No data_manager_conf.xml file has been defined.")
return rval
data_manager_config_has_changes = False
relative_repo_data_manager_dir = os.path.join(shed_config_dict.get("tool_path", ""), relative_install_dir)
repo_data_manager_conf_filename = os.path.join(
relative_repo_data_manager_dir, repo_data_manager_conf_filename
)
tree, error_message = parse_xml(repo_data_manager_conf_filename)
if tree is None:
return rval
root = tree.getroot()
for elem in root:
if elem.tag == "data_manager":
data_manager_id = elem.get("id", None)
if data_manager_id is None:
log.error(
"A data manager was defined that does not have an id and will not be installed:\n%s",
xml_to_string(elem),
)
continue
data_manager_dict = (
metadata_dict["data_manager"].get("data_managers", {}).get(data_manager_id, None)
)
if data_manager_dict is None:
log.error(f"Data manager metadata is not defined properly for '{data_manager_id}'.")
continue
guid = data_manager_dict.get("guid", None)
if guid is None:
log.error(f"Data manager guid '{guid}' is not set in metadata for '{data_manager_id}'.")
continue
elem.set("guid", guid)
tool_guid = data_manager_dict.get("tool_guid", None)
if tool_guid is None:
log.error(
f"Data manager tool guid '{tool_guid}' is not set in metadata for '{data_manager_id}'."
)
continue
tool_dict = repository_tools_by_guid.get(tool_guid, None)
if tool_dict is None:
log.error(
"Data manager tool guid '%s' could not be found for '%s'. Perhaps the tool is invalid?",
tool_guid,
data_manager_id,
)
continue
tool = tool_dict.get("tool", None)
if tool is None:
log.error(
"Data manager tool with guid '%s' could not be found for '%s'. Perhaps the tool is invalid?",
tool_guid,
data_manager_id,
)
continue
tool_config_filename = tool_dict.get("tool_config_filename", None)
if tool_config_filename is None:
log.error(f"Data manager metadata is missing 'tool_config_file' for '{data_manager_id}'.")
continue
elem.set("shed_conf_file", shed_config_dict["config_filename"])
if elem.get("tool_file", None) is not None:
del elem.attrib["tool_file"] # remove old tool_file info
tool_elem = tpm.generate_tool_elem(
repository.tool_shed,
repository.name,
repository.installed_changeset_revision,
repository.owner,
tool_config_filename,
tool,
None,
)
elem.insert(0, tool_elem)
data_manager = self.app.data_managers.load_manager_from_elem(
elem, tool_path=shed_config_dict.get("tool_path", "")
)
if data_manager:
rval.append(data_manager)
elif elem.tag is etree.Comment: # type: ignore[comparison-overlap]
pass
else:
log.warning(f"Encountered unexpected element '{elem.tag}':\n{xml_to_string(elem)}")
config_elems.append(elem)
data_manager_config_has_changes = True
# Persist the altered shed_data_manager_config file.
if data_manager_config_has_changes:
reload_count = self.app.data_managers._reload_count
self._data_manager_config_elems_to_xml_file(config_elems, shed_data_manager_conf_filename)
while self.app.data_managers._reload_count <= reload_count:
time.sleep(0.1) # Wait for shed_data_manager watcher thread to pick up changes
return rval
[docs] def remove_from_data_manager(self, repository):
metadata_dict = repository.metadata_
if metadata_dict and "data_manager" in metadata_dict:
shed_data_manager_conf_filename = self.app.config.shed_data_manager_config_file
tree, error_message = parse_xml(shed_data_manager_conf_filename)
if tree:
root = tree.getroot()
assert (
root.tag == "data_managers"
), f"The file provided ({shed_data_manager_conf_filename}) for removing data managers from is not a valid data manager xml file."
guids = [
data_manager_dict.get("guid")
for data_manager_dict in metadata_dict.get("data_manager", {}).get("data_managers", {}).values()
if "guid" in data_manager_dict
]
load_old_data_managers_by_guid = {}
data_manager_config_has_changes = False
config_elems = []
for elem in root:
# Match Data Manager elements by guid and installed_changeset_revision
elem_matches_removed_data_manager = False
if elem.tag == "data_manager":
guid = elem.get("guid", None)
if guid in guids:
tool_elem = elem.find("tool")
if tool_elem is not None:
installed_changeset_revision_elem = tool_elem.find("installed_changeset_revision")
if installed_changeset_revision_elem is not None:
if (
installed_changeset_revision_elem.text
== repository.installed_changeset_revision
):
elem_matches_removed_data_manager = True
else:
# This is a different version, which had been previously overridden
load_old_data_managers_by_guid[guid] = elem
if elem_matches_removed_data_manager:
data_manager_config_has_changes = True
else:
config_elems.append(elem)
# Remove data managers from in memory
self.app.data_managers.remove_manager(guids)
# Load other versions of any now uninstalled data managers, if any
for elem in load_old_data_managers_by_guid.values():
self.app.data_managers.load_manager_from_elem(elem)
# Persist the altered shed_data_manager_config file.
if data_manager_config_has_changes:
self._data_manager_config_elems_to_xml_file(config_elems, shed_data_manager_conf_filename)