Skip to content

Commit d81b013

Browse files
authored
Merge pull request #466 from cat-bro/bioblend_galaxy_container_resolution
Add container_resolution
2 parents 6295735 + cada187 commit d81b013

File tree

3 files changed

+336
-0
lines changed

3 files changed

+336
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""
2+
Test functions in bioblend.galaxy.tool_dependencies
3+
"""
4+
from . import (
5+
GalaxyTestBase,
6+
test_util,
7+
)
8+
9+
10+
class TestGalaxyContainerResolution(GalaxyTestBase.GalaxyTestBase):
11+
@test_util.skip_unless_galaxy("release_22.05")
12+
def test_get_container_resolvers(self):
13+
container_resolvers = self.gi.container_resolution.get_container_resolvers()
14+
assert isinstance(container_resolvers, list)
15+
assert len(container_resolvers) > 0
16+
assert isinstance(container_resolvers[0], dict)
17+
assert container_resolvers[0]["model_class"] == "ExplicitContainerResolver"
18+
assert container_resolvers[0]["resolver_type"] == "explicit"
19+
assert container_resolvers[0]["can_uninstall_dependencies"] is False
20+
assert container_resolvers[0]["builds_on_resolution"] is False
21+
22+
@test_util.skip_unless_galaxy("release_22.05")
23+
def test_show_container_resolver(self):
24+
container_resolver = self.gi.container_resolution.show_container_resolver(0)
25+
print(container_resolver)
26+
assert isinstance(container_resolver, dict)
27+
assert container_resolver["model_class"] == "ExplicitContainerResolver"
28+
assert container_resolver["resolver_type"] == "explicit"
29+
assert container_resolver["can_uninstall_dependencies"] is False
30+
assert container_resolver["builds_on_resolution"] is False
31+
32+
@test_util.skip_unless_galaxy("release_22.05")
33+
def test_resolve(self):
34+
tool = self.gi.container_resolution.resolve(tool_id="CONVERTER_parquet_to_csv")
35+
print(tool)
36+
assert isinstance(tool, dict)
37+
38+
tool_requirements_only = self.gi.container_resolution.resolve(
39+
tool_id="CONVERTER_parquet_to_csv", requirements_only=True
40+
)
41+
assert isinstance(tool_requirements_only, dict)
42+
43+
@test_util.skip_unless_galaxy("release_22.05")
44+
def test_resolve_toolbox(self):
45+
toolbox = self.gi.container_resolution.resolve_toolbox()
46+
assert isinstance(toolbox, list)
47+
assert len(toolbox) > 0
48+
assert isinstance(toolbox[0], dict)
49+
50+
toolbox_by_tool_ids = self.gi.container_resolution.resolve_toolbox(tool_ids=[toolbox[0]["tool_id"]])
51+
assert isinstance(toolbox_by_tool_ids, list)
52+
assert len(toolbox_by_tool_ids) == 1
53+
assert isinstance(toolbox_by_tool_ids[0], dict)
54+
55+
toolbox_by_resolver_type = self.gi.container_resolution.resolve_toolbox(resolver_type="mulled")
56+
assert isinstance(toolbox_by_resolver_type, list)
57+
assert len(toolbox_by_resolver_type) > 0
58+
assert isinstance(toolbox_by_resolver_type[0], dict)
59+
assert len(toolbox) == len(toolbox_by_resolver_type)
60+
for tool in toolbox_by_resolver_type:
61+
print(tool)
62+
assert (
63+
tool["status"]["dependency_type"] is None
64+
or tool["status"]["container_resolver"]["resolver_type"] == "mulled"
65+
)
66+
67+
toolbox_by_container_type = self.gi.container_resolution.resolve_toolbox(container_type="docker")
68+
assert isinstance(toolbox_by_container_type, list)
69+
assert len(toolbox_by_container_type) > 0
70+
assert isinstance(toolbox_by_container_type[0], dict)
71+
assert len(toolbox) == len(toolbox_by_container_type)
72+
for tool in toolbox_by_container_type:
73+
assert tool["status"]["dependency_type"] is None or tool["status"]["dependency_type"] == "docker"
74+
assert (
75+
tool["status"]["dependency_type"] is None or tool["status"]["container_description"]["type"] == "docker"
76+
)
77+
78+
toolbox_requirements_only = self.gi.container_resolution.resolve_toolbox(requirements_only=True)
79+
assert isinstance(toolbox_requirements_only, list)
80+
assert len(toolbox_requirements_only) > 0
81+
assert isinstance(toolbox_requirements_only[0], dict)
82+
assert len(toolbox) == len(toolbox_requirements_only)
83+
84+
# TODO unless containers are available this may fallback to conda by default?
85+
# depending on Galaxy's config
86+
# toolbox_by_index = self.gi.container_resolution.resolve_toolbox(tool_ids=[toolbox[0]['tool_id']], index=0, install=True)
87+
# assert isinstance(toolbox_by_index, list)
88+
# assert len(toolbox_by_index) > 0
89+
# assert isinstance(toolbox_by_index[0], dict)
90+
91+
# TODO unless containers are available this may fallback to conda by default?
92+
# depending on Galaxy's config
93+
# def test_resolve_toolbox_with_install(self):
94+
# toolbox = self.gi.container_resolution.resolve_toolbox_with_install(tool_ids=[])
95+
# assert isinstance(toolbox, list)
96+
# assert len(toolbox) == 0

bioblend/galaxy/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from bioblend.galaxy import (
77
config,
8+
container_resolution,
89
dataset_collections,
910
datasets,
1011
datatypes,
@@ -93,6 +94,7 @@ def __init__(
9394
self.toolshed = toolshed.ToolShedClient(self)
9495
self.toolShed = self.toolshed # historical alias
9596
self.config = config.ConfigClient(self)
97+
self.container_resolution = container_resolution.ContainerResolutionClient(self)
9698
self.visual = visual.VisualClient(self)
9799
self.quotas = quotas.QuotaClient(self)
98100
self.groups = groups.GroupsClient(self)
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
"""
2+
Contains interactions dealing with Galaxy container resolvers.
3+
Works only with Galaxy > 22.01
4+
"""
5+
from typing import (
6+
List,
7+
Optional,
8+
)
9+
10+
from bioblend.galaxy.client import Client
11+
12+
13+
class ContainerResolutionClient(Client):
14+
module = "container_resolvers"
15+
16+
def get_container_resolvers(self) -> list:
17+
"""
18+
List container resolvers
19+
20+
:rtype: list
21+
return: List of container resolvers
22+
23+
For example:
24+
[{'builds_on_resolution': False,
25+
'can_uninstall_dependencies': False,
26+
'model_class': 'CachedExplicitSingularityContainerResolver',
27+
'resolver_type': 'cached_explicit_singularity'},
28+
{'builds_on_resolution': False,
29+
'can_uninstall_dependencies': False,
30+
'model_class': 'CachedMulledSingularityContainerResolver',
31+
'resolver_type': 'cached_mulled_singularity'},
32+
{'builds_on_resolution': False,
33+
'can_uninstall_dependencies': False,
34+
'model_class': 'MulledSingularityContainerResolver',
35+
'resolver_type': 'mulled_singularity'}] {'builds_on_resolution': False,
36+
"""
37+
url = self._make_url()
38+
return self._get(url=url)
39+
40+
def show_container_resolver(self, index: int) -> dict:
41+
"""
42+
Show container resolver
43+
44+
:type index: int
45+
:param index: index of the dependency resolver with respect to
46+
the dependency resolvers config file
47+
48+
:rtype: dict
49+
return: Dict of properties of a given container resolver
50+
51+
{'builds_on_resolution': False,
52+
'can_uninstall_dependencies': False,
53+
'model_class': 'CachedMulledSingularityContainerResolver',
54+
'resolver_type': 'cached_mulled_singularity'}
55+
"""
56+
url = f"{self._make_url()}/{index}"
57+
return self._get(url=url)
58+
59+
def resolve(
60+
self,
61+
tool_id: str,
62+
index: Optional[int] = None,
63+
resolver_type: Optional[str] = None,
64+
container_type: Optional[str] = None,
65+
requirements_only: bool = False,
66+
install: bool = False,
67+
) -> dict:
68+
"""
69+
Resolve described requirement against specified container resolvers.
70+
71+
:type index: int
72+
:param index: index of the dependency resolver with respect to
73+
the dependency resolvers config file
74+
75+
:type tool_id: str
76+
:param tool_id: tool id to resolve against containers
77+
78+
:type resolver_type: str
79+
:param resolver_type: restrict to specified resolver type
80+
81+
:type container_type: str
82+
:param container_type: restrict to specified container type
83+
84+
:type requirements_only: bool
85+
:param requirements_only: ignore tool containers, properties - just search based on tool requirements set to True to mimic default behavior of tool dependency API.
86+
87+
:type install: bool
88+
:param install: allow installation of new containers (for build_mulled containers) the way job resolution will operate, defaults to False
89+
90+
:rtype: dict
91+
92+
For example:
93+
{
94+
'requirements': [{'name': 'pyarrow', 'specs': [], 'type': 'package', 'version': '4.0.1'}],
95+
'status': {
96+
'cacheable': False,
97+
'container_description': {'identifier': 'quay.io/biocontainers/pyarrow:4.0.1', 'resolve_dependencies': False, 'shell': '/bin/bash', 'type': 'docker'},
98+
'container_resolver': {'builds_on_resolution': False, 'can_uninstall_dependencies': False, 'model_class': 'MulledDockerContainerResolver', 'resolver_type': 'mulled'},
99+
'dependency_type': 'docker',
100+
...
101+
},
102+
'tool_id': 'CONVERTER_parquet_to_csv'
103+
}
104+
"""
105+
params = {}
106+
if tool_id:
107+
params["tool_id"] = tool_id
108+
if resolver_type:
109+
params["resolver_type"] = resolver_type
110+
if container_type:
111+
params["container_type"] = container_type
112+
params["requirements_only"] = str(requirements_only)
113+
params["install"] = str(install)
114+
if index is not None:
115+
url = "/".join((self._make_url(), str(index), "resolve"))
116+
else:
117+
url = "/".join((self._make_url(), "resolve"))
118+
return self._get(url=url, params=params)
119+
120+
def resolve_toolbox(
121+
self,
122+
index: Optional[int] = None,
123+
tool_ids: Optional[List[str]] = None,
124+
resolver_type: Optional[str] = None,
125+
container_type: Optional[str] = None,
126+
requirements_only: bool = False,
127+
install: bool = False,
128+
) -> list:
129+
"""
130+
Apply resolve() to each tool in the toolbox and return the results as a list.
131+
See documentation for resolve() for a description of parameters that can be
132+
consumed and a description of the resulting items.
133+
134+
:type index: int
135+
:param index: index of the dependency resolver with respect to
136+
the dependency resolvers config file
137+
138+
:type tool_ids: list
139+
:param tool_ids: tool_ids to filter toolbox on
140+
141+
:type resolver_type: str
142+
:param resolver_type: restrict to specified resolver type
143+
144+
:type container_type: str
145+
:param container_type: restrict to specified container type
146+
147+
:type requirements_only: bool
148+
:param requirements_only: ignore tool containers, properties - just search based on tool requirements set to True to mimic default behavior of tool dependency API.
149+
150+
:type install: bool
151+
:param install: allow installation of new containers (for build_mulled containers) the way job resolution will operate, defaults to False
152+
153+
:rtype: list
154+
For example::
155+
[{'tool_id': 'upload1', 'status': {'model_class': 'NullDependency', 'dependency_type': None, 'exact': True, 'name': None, 'version': None, 'cacheable': False}, 'requirements': []}, ...]
156+
"""
157+
params = {}
158+
if tool_ids:
159+
params["tool_ids"] = ",".join(tool_ids)
160+
if resolver_type:
161+
params["resolver_type"] = resolver_type
162+
if container_type:
163+
params["container_type"] = container_type
164+
params["requirements_only"] = str(requirements_only)
165+
params["install"] = str(install)
166+
if index is not None:
167+
url = "/".join((self._make_url(), str(index), "toolbox"))
168+
else:
169+
url = "/".join((self._make_url(), "toolbox"))
170+
return self._get(url=url, params=params)
171+
172+
def resolve_toolbox_with_install(
173+
self,
174+
index: Optional[int] = None,
175+
tool_ids: Optional[List[str]] = None,
176+
resolver_type: Optional[str] = None,
177+
container_type: Optional[str] = None,
178+
requirements_only: bool = False,
179+
) -> list:
180+
"""
181+
Do the resolution of dependencies like resolve_toolbox(), but allow building and installing new containers.
182+
183+
:type index: int
184+
:param index: index of the dependency resolver with respect to
185+
the dependency resolvers config file
186+
187+
:type tool_ids: list
188+
:param tool_ids: tool_ids to filter toolbox on
189+
190+
:type resolver_type: str
191+
:param resolver_type: restrict to specified resolver type
192+
193+
:type container_type: str
194+
:param container_type: restrict to specified container type
195+
196+
:type requirements_only: bool
197+
:param requirements_only: ignore tool containers, properties - just search based on tool requirements set to True to mimic default behavior of tool dependency API.
198+
199+
200+
:rtype: list of dicts
201+
:returns: dictified descriptions of the dependencies, with attribute
202+
`dependency_type: None` if no match was found.
203+
For example::
204+
205+
[{'requirements': [{'name': 'canu',
206+
'specs': [],
207+
'type': 'package',
208+
'version': '2.2'}],
209+
'status': {'cacheable': False,
210+
'container_description': {'identifier': 'docker://quay.io/biocontainers/canu:2.2--ha47f30e_0',
211+
'resolve_dependencies': False,
212+
'shell': '/bin/bash',
213+
'type': 'singularity'},
214+
'container_resolver': {'builds_on_resolution': False,
215+
'can_uninstall_dependencies': False,
216+
'model_class': 'MulledSingularityContainerResolver',
217+
'resolver_type': 'mulled_singularity'},
218+
'dependency_type': 'singularity',
219+
'environment_path': 'docker://quay.io/biocontainers/canu:2.2--ha47f30e_0',
220+
'exact': True,
221+
'model_class': 'ContainerDependency',
222+
'name': None,
223+
'version': None},
224+
'tool_id': 'toolshed.g2.bx.psu.edu/repos/bgruening/canu/canu/2.2+galaxy0'}]
225+
"""
226+
params = {}
227+
if tool_ids:
228+
params["tool_ids"] = ",".join(tool_ids)
229+
if resolver_type:
230+
params["resolver_type"] = resolver_type
231+
if container_type:
232+
params["container_type"] = container_type
233+
params["requirements_only"] = str(requirements_only)
234+
if index is not None:
235+
url = "/".join((self._make_url(), str(index), "toolbox", "install"))
236+
else:
237+
url = "/".join((self._make_url(), "toolbox", "install"))
238+
return self._post(url=url, payload=params)

0 commit comments

Comments
 (0)