Merge pull request #12327 from jmchilton/tool_panel_views_0

Implement tool panel views.
This commit is contained in:
Dannon
2021-08-18 18:54:39 -07:00
committed by GitHub
60 changed files with 2041 additions and 321 deletions
+2 -1
View File
@@ -87,7 +87,8 @@
"vueisotope": "^3.1.2",
"vuex": "^3.4.0",
"vuex-cache": "^3.2.0",
"vuex-persist": "^3.1.3"
"vuex-persist": "^3.1.3",
"vuex-persistedstate": "^4.0.0"
},
"scripts": {
"watch": "gulp && yarn run save-build-hash && yarn run webpack-watch",
@@ -0,0 +1,102 @@
<template>
<b-dropdown
no-caret
right
role="button"
title="Panel Views"
variant="link"
aria-label="View all tool panel configurations"
class="tool-panel-dropdown float-right"
size="sm"
v-b-tooltip.hover
>
<template v-slot:button-content>
<span class="fa fa-list" />
<!-- Maybe cog? -->
</template>
<PanelViewMenuItem
:current-panel-view="currentPanelView"
:panel-view="defaultPanelView"
@onSelect="updatePanelView"
/>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-group v-for="group in groupedPanelViews" :key="group.type" :id="group.type" :header="group.title">
<PanelViewMenuItem
v-for="(panelView, key) in group.panelViews"
:key="key"
:current-panel-view="currentPanelView"
:panel-view="panelView"
@onSelect="updatePanelView"
/>
</b-dropdown-group>
<b-dropdown-divider v-if="groupedPanelViews.length > 0"></b-dropdown-divider>
<PanelViewMenuItem
v-for="(panelView, key) in ungroupedPanelViews"
:key="key"
:current-panel-view="currentPanelView"
:panel-view="panelView"
@onSelect="updatePanelView"
/>
</b-dropdown>
</template>
<script>
import PanelViewMenuItem from "./PanelViewMenuItem";
const groupsDefinitions = [
{ type: "ontology", title: "... by Ontology" },
{ type: "activity", title: "... for Activity" },
{ type: "publication", title: "... from Publication" },
{ type: "training", title: "... for Training" },
];
export default {
components: { PanelViewMenuItem },
props: {
panelViews: {
type: Object,
},
currentPanelView: {
type: String,
},
},
computed: {
defaultPanelView() {
return this.panelViewsOfType("default")[0];
},
groupedPanelViews() {
const groups = [];
for (const group of groupsDefinitions) {
const viewType = group.type;
const panelViews = this.panelViewsOfType(viewType);
if (panelViews.length > 0) {
groups.push({
type: viewType,
title: group.title,
panelViews: panelViews,
});
}
}
return groups;
},
ungroupedPanelViews() {
return this.panelViewsOfType("generic");
},
},
methods: {
updatePanelView(panelView) {
this.$emit("updatePanelView", panelView.id);
},
panelViewsOfType(panelViewType) {
const panelViews = [];
for (const panelViewId in this.panelViews) {
const panelView = this.panelViews[panelViewId];
if (panelView.view_type == panelViewType) {
panelViews.push(panelView);
}
}
return panelViews;
},
},
};
</script>
@@ -0,0 +1,52 @@
<template>
<b-dropdown-item
:active="currentPanelView == panelView.id"
:disabled="currentPanelView == panelView.id"
@click="onClick"
>
<span :class="icon" />
<span :title="title" v-localize>{{ name }}</span>
</b-dropdown-item>
</template>
<script>
const types_to_icons = {
default: "fa-backspace",
generic: "fa-filter",
ontology: "fa-sitemap",
activity: "fa-project-diagram",
publication: "fa-newspaper",
training: "fa-graduation-cap",
};
export default {
props: {
currentPanelView: {
type: String,
required: true,
},
panelView: {
type: Object,
required: true,
},
},
computed: {
title() {
return this.panelView.description;
},
icon() {
const viewType = this.panelView.view_type;
const faIcon = types_to_icons[viewType];
return ["fa", faIcon];
},
name() {
return this.panelView.name;
},
},
methods: {
onClick() {
this.$emit("onSelect", this.panelView);
},
},
};
</script>
@@ -14,6 +14,10 @@ export default {
DelayedInput,
},
props: {
currentPanelView: {
type: String,
required: true,
},
query: {
type: String,
default: "",
@@ -46,7 +50,7 @@ export default {
this.loading = true;
axios
.get(`${getAppRoot()}api/tools`, {
params: { q },
params: { q, view: this.currentPanelView },
})
.then((response) => {
this.loading = false;
@@ -0,0 +1,52 @@
<template>
<ConfigProvider class="d-flex flex-column" v-slot="{ config }">
<ToolPanelViewProvider
v-slot="{ currentPanel, currentPanelView }"
:site-default-panel-view="config.default_panel_view"
v-if="config.default_panel_view"
>
<ToolBox
:toolbox="currentPanel"
:panel-views="config.panel_views"
:current-panel-view="currentPanelView"
:stored-workflow-menu-entries="storedWorkflowMenuEntries"
:workflow-title="workflowTitle"
@updatePanelView="updatePanelView"
v-if="currentPanelView"
>
</ToolBox>
</ToolPanelViewProvider>
</ConfigProvider>
</template>
<script>
import ToolBox from "./ToolBox";
import ConfigProvider from "components/providers/ConfigProvider";
import ToolPanelViewProvider from "components/providers/ToolPanelViewProvider";
import _l from "utils/localization";
import { mapActions } from "vuex";
export default {
components: {
ConfigProvider,
ToolBox,
ToolPanelViewProvider,
},
props: {
storedWorkflowMenuEntries: {
type: Array,
required: true,
},
workflowTitle: {
type: String,
default: _l("Workflows"),
},
},
methods: {
updatePanelView(panelView) {
this.setCurrentPanelView(panelView);
},
...mapActions("panels", ["setCurrentPanelView"]),
},
};
</script>
@@ -0,0 +1,72 @@
<template>
<ConfigProvider class="d-flex flex-column" v-slot="{ config }">
<ToolPanelViewProvider
v-slot="{ currentPanel, currentPanelView }"
:site-default-panel-view="config.default_panel_view"
v-if="config.default_panel_view"
>
<ToolBoxWorkflow
:toolbox="currentPanel"
:panel-views="config.panel_views"
:current-panel-view="currentPanelView"
:workflows="workflows"
:data-managers="dataManagers"
:module-sections="moduleSections"
@updatePanelView="updatePanelView"
@onInsertTool="onInsertTool"
@onInsertModule="onInsertModule"
@onInsertWorkflow="onInsertWorkflow"
@onInsertWorkflowSteps="onInsertWorkflowSteps"
v-if="currentPanelView"
>
</ToolBoxWorkflow>
</ToolPanelViewProvider>
</ConfigProvider>
</template>
<script>
import ToolBoxWorkflow from "./ToolBoxWorkflow";
import ConfigProvider from "components/providers/ConfigProvider";
import ToolPanelViewProvider from "components/providers/ToolPanelViewProvider";
import { mapActions } from "vuex";
export default {
components: {
ConfigProvider,
ToolBoxWorkflow,
ToolPanelViewProvider,
},
props: {
workflows: {
type: Array,
required: true,
},
dataManagers: {
type: Array,
required: true,
},
moduleSections: {
type: Array,
required: true,
},
},
methods: {
updatePanelView(panelView) {
this.setCurrentPanelView(panelView);
},
...mapActions("panels", ["setCurrentPanelView"]),
onInsertTool(toolId, toolName) {
this.$emit("onInsertTool", toolId, toolName);
},
onInsertModule(moduleName, moduleTitle) {
this.$emit("onInsertModule", moduleName, moduleTitle);
},
onInsertWorkflow(workflowId, workflowName) {
this.$emit("onInsertWorkflow", workflowId, workflowName);
},
onInsertWorkflowSteps(workflowId, workflowStepCount) {
this.$emit("onInsertWorkflowSteps", workflowId, workflowStepCount);
},
},
};
</script>
+24 -2
View File
@@ -4,12 +4,24 @@
<div class="unified-panel-header-inner">
<div class="panel-header-buttons">
<favorites-button :query="query" @onFavorites="onFavorites" v-if="isUser" />
<panel-view-button
:panel-views="panelViews"
:current-panel-view="currentPanelView"
@updatePanelView="updatePanelView"
v-if="panelViews && Object.keys(panelViews).length > 1"
/>
</div>
<div class="panel-header-text">Tools</div>
</div>
</div>
<div class="unified-panel-controls">
<tool-search :query="query" placeholder="search tools" @onQuery="onQuery" @onResults="onResults" />
<tool-search
:current-panel-view="currentPanelView"
:query="query"
placeholder="search tools"
@onQuery="onQuery"
@onResults="onResults"
/>
<upload-button />
<div class="py-2" v-if="hasResults">
<b-button @click="onToggle" size="sm" class="w-100">
@@ -52,16 +64,17 @@ import ToolSection from "./Common/ToolSection";
import ToolSearch from "./Common/ToolSearch";
import { UploadButton, openGlobalUploadModal } from "components/Upload";
import FavoritesButton from "./Buttons/FavoritesButton";
import PanelViewButton from "./Buttons/PanelViewButton";
import { filterToolSections, filterTools } from "./utilities";
import { getGalaxyInstance } from "app";
import { getAppRoot } from "onload";
import _l from "utils/localization";
export default {
name: "ToolBox",
components: {
UploadButton,
FavoritesButton,
PanelViewButton,
ToolSection,
ToolSearch,
},
@@ -81,6 +94,12 @@ export default {
type: Array,
required: true,
},
panelViews: {
type: Object,
},
currentPanelView: {
type: String,
},
storedWorkflowMenuEntries: {
type: Array,
required: true,
@@ -163,6 +182,9 @@ export default {
this.buttonText = this.showSections ? "Hide Sections" : "Show Sections";
this.buttonIcon = this.showSections ? "fa fa-eye-slash" : "fa fa-eye";
},
updatePanelView(panelView) {
this.$emit("updatePanelView", panelView);
},
},
};
</script>
@@ -2,11 +2,24 @@
<div class="unified-panel">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
<div class="panel-header-buttons">
<panel-view-button
:panel-views="panelViews"
:current-panel-view="currentPanelView"
@updatePanelView="updatePanelView"
v-if="panelViews && Object.keys(panelViews).length > 1"
/>
</div>
<div class="panel-header-text">Tools</div>
</div>
</div>
<div class="unified-panel-controls">
<tool-search placeholder="search tools" @onQuery="onQuery" @onResults="onResults" />
<tool-search
:current-panel-view="currentPanelView"
placeholder="search tools"
@onQuery="onQuery"
@onResults="onResults"
/>
</div>
<div class="unified-panel-body">
<div class="toolMenuContainer">
@@ -58,12 +71,14 @@ import _l from "utils/localization";
import ToolSection from "./Common/ToolSection";
import ToolSearch from "./Common/ToolSearch";
import { filterToolSections } from "./utilities";
import PanelViewButton from "./Buttons/PanelViewButton";
export default {
name: "ToolBox",
components: {
ToolSection,
ToolSearch,
PanelViewButton,
},
data() {
return {
@@ -76,6 +91,12 @@ export default {
type: Array,
required: true,
},
panelViews: {
type: Object,
},
currentPanelView: {
type: String,
},
workflows: {
type: Array,
required: true,
@@ -147,6 +168,9 @@ export default {
onInsertWorkflowSteps(workflow) {
this.$emit("onInsertWorkflowSteps", workflow.id, workflow.step_count);
},
updatePanelView(panelView) {
this.$emit("updatePanelView", panelView);
},
},
};
</script>
@@ -39,8 +39,7 @@
<div v-show="isCanvas">
<SidePanel id="left" side="left">
<template v-slot:panel>
<ToolBoxWorkflow
:toolbox="toolbox"
<ProviderAwareToolBoxWorkflow
:module-sections="moduleSections"
:data-managers="dataManagers"
:workflows="workflows"
@@ -186,7 +185,7 @@ import WorkflowOptions from "./Options";
import FormDefault from "./Forms/FormDefault";
import FormTool from "./Forms/FormTool";
import MarkdownEditor from "components/Markdown/MarkdownEditor";
import ToolBoxWorkflow from "components/Panels/ToolBoxWorkflow";
import ProviderAwareToolBoxWorkflow from "components/Panels/ProviderAwareToolBoxWorkflow";
import SidePanel from "components/Panels/SidePanel";
import { getAppRoot } from "onload/loadConfig";
import reportDefault from "./reportDefault";
@@ -205,7 +204,7 @@ export default {
MarkdownEditor,
SidePanel,
StateUpgradeModal,
ToolBoxWorkflow,
ProviderAwareToolBoxWorkflow,
FormDefault,
FormTool,
WorkflowOptions,
@@ -241,10 +240,6 @@ export default {
type: Array,
required: true,
},
toolbox: {
type: Array,
required: true,
},
},
data() {
return {
@@ -0,0 +1,25 @@
import { mapGetters, mapActions } from "vuex";
export default {
props: {
siteDefaultPanelView: {
type: String,
required: true,
},
},
computed: {
...mapGetters("panels", ["currentPanel", "currentPanelView"]),
},
methods: {
...mapActions("panels", ["initCurrentPanelView"]),
},
created() {
this.initCurrentPanelView(this.siteDefaultPanelView);
},
render() {
return this.$scopedSlots.default({
currentPanel: this.currentPanel,
currentPanelView: this.currentPanelView,
});
},
};
+4 -2
View File
@@ -1,7 +1,7 @@
import Backbone from "backbone";
import _l from "utils/localization";
import { getGalaxyInstance } from "app";
import ToolBox from "../../components/Panels/ToolBox";
import ToolBox from "../../components/Panels/ProviderAwareToolBox";
import SidePanel from "../../components/Panels/SidePanel";
import { mountVueComponent } from "../../utils/mountVueComponent";
@@ -23,7 +23,9 @@ const ToolPanel = Backbone.View.extend({
currentPanel: ToolBox,
currentPanelProperties: {
storedWorkflowMenuEntries: Galaxy.config.stored_workflow_menu_entries,
toolbox: Galaxy.config.toolbox,
// TODO: find a way to seed to Vuex store's default panel view with data
// or just stop including it in the response.
// toolbox: Galaxy.config.toolbox,
},
},
el
+9 -1
View File
@@ -5,6 +5,8 @@
import Vue from "vue";
import Vuex from "vuex";
import createCache from "vuex-cache";
import createPersistedState from "vuex-persistedstate";
import config from "config";
import { gridSearchStore } from "./gridSearchStore";
@@ -20,6 +22,7 @@ import { toolStore } from "./toolStore";
import { datasetPathDestinationStore } from "./datasetPathDestinationStore";
import { datasetExtFilesStore } from "./datasetExtFilesStore";
import { jobStore } from "./jobStore";
import { panelStore } from "./panelStore";
// beta features
import { historyStore as betaHistoryStore } from "components/History/model/historyStore";
@@ -29,13 +32,18 @@ import { syncVuextoGalaxy } from "./syncVuextoGalaxy";
Vue.use(Vuex);
const panelsState = createPersistedState({
paths: ["panels"],
});
export function createStore() {
const storeConfig = {
plugins: [createCache()],
plugins: [createCache(), panelsState],
modules: {
user: userStore,
config: configStore,
betaHistory: betaHistoryStore,
panels: panelStore,
// TODO: please namespace all store modules
gridSearch: gridSearchStore,
+55
View File
@@ -0,0 +1,55 @@
export const state = {
panel: {},
currentPanelView: null,
};
import Vue from "vue";
import { getAppRoot } from "onload/loadConfig";
import axios from "axios";
const getters = {
panel: (state) => (panelView) => {
return state.panel[panelView] || [];
},
currentPanel: (state) => {
const effectiveView = state.currentPanelView || "default";
const val = state.panel[effectiveView] || [];
return val;
},
currentPanelView: (state) => {
return state.currentPanelView;
},
};
const actions = {
initCurrentPanelView: async ({ commit, state }, siteDefaultPanelView) => {
const panelView = state.currentPanelView || siteDefaultPanelView;
if (state.currentPanelView == null) {
commit("setCurrentPanelView", { panelView });
}
const { data } = await axios.get(`${getAppRoot()}api/tools?in_panel=true&view=${panelView}`);
commit("savePanelView", { panelView, panel: data });
},
setCurrentPanelView: async ({ commit }, panelView) => {
commit("setCurrentPanelView", { panelView });
const { data } = await axios.get(`${getAppRoot()}api/tools?in_panel=true&view=${panelView}`);
commit("savePanelView", { panelView, panel: data });
},
};
const mutations = {
savePanelView: (state, { panelView, panel }) => {
Vue.set(state.panel, panelView, panel);
},
setCurrentPanelView: (state, { panelView }) => {
state.currentPanelView = panelView;
},
};
export const panelStore = {
namespaced: true,
state,
getters,
actions,
mutations,
};
+13
View File
@@ -13351,6 +13351,11 @@ shellwords@^0.1.1:
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
shvl@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.3.tgz#eb4bd37644f5684bba1fc52c3010c96fb5e6afd1"
integrity sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==
side-channel@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3"
@@ -15310,6 +15315,14 @@ vuex-persist@^3.1.3:
deepmerge "^4.2.2"
flatted "^3.0.5"
vuex-persistedstate@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.0.0.tgz#ed82f266ca98c869a2aad9cb9880c2f608c05f3a"
integrity sha512-jDs+awbV9YD2A2F6S5zgtYq1Bjd8v0YldOK6HPv1EJZzGMse0FtZTREfXvA7zlVfq9MpmSZJNmYQVylfpZ5znQ==
dependencies:
deepmerge "^4.2.2"
shvl "^2.0.3"
vuex@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.4.0.tgz#20cc086062d750769fce1febb34e7fceeaebde45"
@@ -60,6 +60,7 @@ docopt==0.6.2
docutils==0.16; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
dogpile.cache==1.1.2; python_version >= "3.6"
ecdsa==0.14.1; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0"
edam-ontology==1.25.2
fabric3==1.14.post1
fastapi-utils==0.2.1; python_version >= "3.6" and python_version < "4.0"
fastapi==0.65.2; python_version >= "3.6"
@@ -55,6 +55,7 @@ docopt==0.6.2
docutils==0.16; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
dogpile.cache==1.1.2; python_version >= "3.6"
ecdsa==0.14.1; python_version >= "2.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0"
edam-ontology==1.25.2
fabric3==1.14.post1
fastapi-utils==0.2.1; python_version >= "3.6" and python_version < "4.0"
fastapi==0.65.2; python_version >= "3.6"
+2
View File
@@ -200,6 +200,8 @@ class ConfigSerializer(base.ModelSerializer):
'python': _defaults_to((sys.version_info.major, sys.version_info.minor)),
'select_type_workflow_threshold': _use_config,
'file_sources_configured': lambda config, key, **context: self.app.file_sources.custom_sources_configured,
'panel_views': lambda config, key, **content: self.app.toolbox.panel_view_dicts(),
'default_panel_view': _use_config,
'upload_from_form_button': _use_config,
'release_doc_base_url': _use_config,
'user_library_import_dir_available': lambda config, key, **context: bool(config.get('user_library_import_dir')),
+68
View File
@@ -0,0 +1,68 @@
import io
import os
from typing import Dict, Optional, TextIO
try:
from edam_ontology.streams import tabular_stream
except ImportError:
tabular_stream = None
EDAM_PREFIX = 'http://edamontology.org/'
COLUMN_TERM = 0
COLUMN_LABEL = 1
COLUMN_PARENTS = 7
ROOT_OPERATION = 'operation_0004'
ROOT_TOPIC = 'topic_0003'
def load_edam_tree(path: Optional[str] = None):
if path is not None:
assert os.path.exists(path), f"Failed to load EDAM tabular data at [{path}] path does not exist."
handle = io.open(path, "r")
else:
assert tabular_stream is not None, "Failed to load optional import from edam-onotology package, install using [pip install edam-ontology]."
handle = tabular_stream()
return load_edam_tree_from_tsv_stream(handle)
def load_edam_tree_from_tsv_stream(tsv_stream: TextIO):
edam: Dict[str, Dict] = {}
def _recurse_edam_parents(term, path=None):
if edam[term]['parents'] and len(edam[term]['parents']) > 0:
for parent in edam[term]['parents']:
yield from _recurse_edam_parents(parent, path + [parent])
else:
yield path
for line in tsv_stream.readlines():
fields = line.split('\t')
term = fields[COLUMN_TERM]
if not term.startswith(EDAM_PREFIX):
continue
term_id = term[len(EDAM_PREFIX):]
# Only care about formats and operations
if not (term_id.startswith('operation_') or term_id.startswith('topic_')):
continue
parents = fields[COLUMN_PARENTS].split('|')
edam[term_id] = {
'label': fields[COLUMN_LABEL], # preferred label
'parents': [x[len(EDAM_PREFIX):] for x in parents if x.startswith(EDAM_PREFIX)],
}
for term in sorted(edam.keys()):
tails = []
for x in _recurse_edam_parents(term, path=[]):
if x[-2:] not in tails:
tails.append(x[-2:])
edam[term]['path'] = tails
return edam
__all__ = ('load_edam_tree', 'ROOT_OPERATION', 'ROOT_TOPIC')
+119 -231
View File
@@ -3,9 +3,9 @@ import logging
import os
import string
import time
import urllib.request
from collections import namedtuple
from errno import ENOENT
from typing import Dict, List
from urllib.parse import urlparse
from markupsafe import escape
@@ -14,6 +14,7 @@ from galaxy.exceptions import (
ConfigurationError,
MessageException,
ObjectNotFound,
RequestParameterInvalidException,
)
from galaxy.tool_util.deps import (
build_dependency_manager,
@@ -23,10 +24,8 @@ from galaxy.tool_util.loader_directory import looks_like_a_tool
from galaxy.util import (
etree,
ExecutionTimer,
get_charset_from_http_headers,
listify,
parse_xml,
stream_to_open_named_file,
string_as_bool,
unicodify,
)
@@ -46,6 +45,14 @@ from .parser import (
get_toolbox_parser
)
from .tags import tool_tag_manager
from .views.edam import EdamPanelMode, EdamToolPanelView
from .views.interface import (
ToolBoxRegistry,
ToolPanelView,
ToolPanelViewModel,
ToolPanelViewModelType,
)
from .views.static import StaticToolPanelView
log = logging.getLogger(__name__)
@@ -71,24 +78,43 @@ class ToolConfRepository(_ToolConfRepository):
return self.tool_path, self.repository_path
class ToolBoxRegistryImpl(ToolBoxRegistry):
"""View of ToolBox provided to ToolPanelView to reason about tools loaded."""
def __init__(self, toolbox: 'AbstractToolBox'):
self.__toolbox = toolbox
def has_tool(self, tool_id: str) -> bool:
return tool_id in self.__toolbox._tools_by_id
def get_tool(self, tool_id: str):
return self.__toolbox.get_tool(tool_id)
def get_workflow(self, id: str):
return self.__toolbox._workflows_by_id[id]
def add_tool_to_tool_panel_view(self, tool, tool_panel_component) -> None:
self.__toolbox.add_tool_to_tool_panel_view(tool, tool_panel_component)
class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
"""
Abstract container for managing a ToolPanel - containing tools and
workflows optionally in labelled sections.
"""
def __init__(self, config_filenames, tool_root_dir, app, save_integrated_tool_panel=True):
def __init__(self, config_filenames, tool_root_dir, app, view_sources=None, default_panel_view="default", save_integrated_tool_panel=True):
"""
Create a toolbox from the config files named by `config_filenames`, using
`tool_root_dir` as the base directory for finding individual tool config files.
"""
self._default_panel_view = default_panel_view
# The _dynamic_tool_confs list contains dictionaries storing
# information about the tools defined in each shed-related
# shed_tool_conf.xml file.
self._dynamic_tool_confs = []
self._tools_by_id = {}
self._tools_by_uuid = {}
self._integrated_section_by_tool = {}
# Tool lineages can contain chains of related tools with different ids
# so each will be present once in the above dictionary. The following
# dictionary can instead hold multiple tools with different versions.
@@ -113,64 +139,53 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
self._filter_factory = FilterFactory(self)
self._tool_tag_manager = tool_tag_manager(app)
self._init_tools_from_configs(config_filenames)
self._load_edam()
if self.app.name == 'galaxy' and self._integrated_tool_panel_config_has_contents:
# Load self._tool_panel based on the order in self._integrated_tool_panel.
if self.app.config.enable_beta_edam_toolbox:
self._load_tool_panel_edam()
else:
self._load_tool_panel()
self._load_tool_panel()
toolbox = self
class DefaultToolPanelView(ToolPanelView):
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
return toolbox._tool_panel
def to_model(self) -> ToolPanelViewModel:
model_id = "default"
name = "Full Tool Panel"
description = "Galaxy's fully configured toolbox panel with all sections, tools, and configured workflows loaded."
view_type = ToolPanelViewModelType.default_type
model_class = DefaultToolPanelView.__class__.__name__
return ToolPanelViewModel(
id=model_id,
name=name,
description=description,
model_class=model_class,
view_type=view_type,
searchable=True,
)
tool_panel_views_list = [
DefaultToolPanelView(),
]
for edam_view in listify(self.app.config.edam_panel_views):
mode = EdamPanelMode[edam_view]
tool_panel_views_list.append(EdamToolPanelView(self.app.config.edam_toolbox_ontology_path, mode=mode))
if view_sources is not None:
for definition in view_sources.get_definitions():
tool_panel_views_list.append(StaticToolPanelView(definition))
self._tool_panel_views = {}
for tool_panel_view in tool_panel_views_list:
self._tool_panel_views[tool_panel_view.to_model().id] = tool_panel_view
if self.app.name == 'galaxy':
self._load_tool_panel_views()
if save_integrated_tool_panel:
self._save_integrated_tool_panel()
def _recurse_edam_parents(self, term, path=None):
log.debug(f"term:{term} path:{path} parents:{self.edam[term]['parents']}")
if self.edam[term]['parents'] and len(self.edam[term]['parents']) > 0:
for parent in self.edam[term]['parents']:
yield from self._recurse_edam_parents(parent, path + [parent])
else:
yield path
def _load_edam(self):
if not self.app.config.enable_beta_edam_toolbox:
return
log.debug('Loading EDAM Terms')
if not os.path.exists(self.app.config.beta_edam_toolbox_ontology_path):
log.debug('EDAM ontology file not present, downloading')
page = urllib.request.urlopen('https://edamontology.org/EDAM.tsv')
stream_to_open_named_file(page, os.open(self.app.config.beta_edam_toolbox_ontology_path, os.O_WRONLY | os.O_CREAT), None, source_encoding=get_charset_from_http_headers(page.headers))
log.debug('Processing EDAM Terms')
with open(self.app.config.beta_edam_toolbox_ontology_path, 'r') as handle:
log.debug(f'Processing {handle}')
self.edam = {}
for line in handle.readlines():
fields = line.split('\t')
if not fields[0].startswith('http://edamontology.org/'):
continue
term_id = fields[0][len('http://edamontology.org/'):]
# Only care about formats and operations
if not (term_id.startswith('operation_') or term_id.startswith('topic_')):
continue
parents = fields[7].split('|')
self.edam[term_id] = {
'label': fields[1], # preferred label
'parents': [x[len('http://edamontology.org/'):] for x in parents if x.startswith('http://edamontology.org/')],
}
log.debug(f'Loaded term {term_id} => {fields[1]}')
for term in sorted(self.edam.keys()):
tails = []
for x in self._recurse_edam_parents(term, path=[]):
if x[-2:] not in tails:
tails.append(x[-2:])
self.edam[term]['path'] = tails
def create_tool(self, config_file, tool_shed_repository=None, guid=None, **kwds):
raise NotImplementedError()
@@ -289,6 +304,16 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
return None
def panel_views(self) -> List[ToolPanelViewModel]:
return [v.to_model() for v in self._tool_panel_views.values()]
def panel_view_dicts(self) -> Dict[str, Dict]:
return {m.id: m.dict() for m in self.panel_views()}
def panel_has_tool(self, tool, panel_view_id):
panel_view_rendered = self._tool_panel_view_rendered[panel_view_id]
return panel_view_rendered.has_item_recursive(tool)
def load_dynamic_tool(self, dynamic_tool):
if not dynamic_tool.active:
return None
@@ -370,13 +395,9 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
log.debug(f"Loading new tool panel section: {str(tool_section.name)}")
return tool_section
def get_integrated_section_for_tool(self, tool):
def get_section_for_tool(self, tool):
tool_id = tool.id
if tool_id in self._integrated_section_by_tool:
return self._integrated_section_by_tool[tool_id]
return None, None
return self._tool_panel.get_section_for_tool_id(tool_id)
def __resolve_tool_path(self, tool_path, config_filename):
if not tool_path:
@@ -389,7 +410,12 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
tool_path = string.Template(tool_path).safe_substitute(tool_path_vars)
return tool_path
def __add_tool_to_tool_panel(self, tool, panel_component, section=False):
def add_tool_to_tool_panel_view(self, tool, view_panel_component):
self.__add_tool_to_tool_panel(tool, view_panel_component)
def __add_tool_to_tool_panel(self, tool, panel_component, section=None):
if section is None:
section = isinstance(panel_component, ToolSection)
# See if a version of this tool is already loaded into the tool panel.
# The value of panel_component will be a ToolSection (if the value of
# section=True) or self._tool_panel (if section=False).
@@ -456,140 +482,11 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
if log_msg and (not hasattr(self.app, 'tool_cache') or tool_id in self.app.tool_cache._new_tool_ids):
log.debug(log_msg)
def _get_edam_sec(self, tool):
edam = tool.edam_operations + tool.edam_topics
if len(edam) > 0:
for term in edam:
yield term
else:
yield 'uncategorized'
def _get_section(self, sec_id, sec_nm):
if sec_id not in self._tool_panel:
section = ToolSection({'id': sec_id, 'name': sec_nm, 'version': ''})
self._tool_panel[sec_id] = section
else:
section = self._tool_panel[sec_id]
return section
def _edam_children_of(self, parentTerm):
for term in self.edam.keys():
if parentTerm in self.edam[term]['parents']:
yield term
def _load_tool_panel_edam(self):
execution_timer = ExecutionTimer()
# Find the children of the top level topics
operations = ['operation_0004'] + list(self._edam_children_of('operation_0004'))
topics = ['topic_0003'] + list(self._edam_children_of('topic_0003'))
# Sort them (by english label)
# operations = sorted(operations, key=lambda x: self.edam[x]['label'])
# topics = sorted(topics, key=lambda x: self.edam[x]['label'])
# Convert these to list of dicts, wherein we'll add our tools/etc.
operations = {
x: {}
for x in operations
}
topics = {
x: {}
for x in topics
}
uncategorized = []
for key, item_type, val in self._integrated_tool_panel.panel_items_iter():
if item_type == panel_item_types.TOOL:
tool_id = key.replace('tool_', '', 1)
if tool_id in self._tools_by_id:
if tool_id in self._tools_by_id:
for term in self._get_edam_sec(val):
if term == 'uncategorized':
uncategorized.append((tool_id, key, val, val.name))
else:
for path in self.edam[term]['path']:
if len(path) == 1:
t = term
else:
t = path[0]
if path[0].startswith('operation_'):
operations[t][tool_id] = (term, tool_id, key, val, val.name)
elif path[0].startswith('topic_'):
topics[t][tool_id] = (term, tool_id, key, val, val.name)
# elif item_type == panel_item_types.WORKFLOW:
# workflow_id = key.replace('workflow_', '', 1)
# if workflow_id in self._workflows_by_id:
# workflow = self._workflows_by_id[workflow_id]
# self._tool_panel[key] = workflow
# log.debug(f"Loaded workflow: {workflow_id} {workflow.name}")
elif item_type == panel_item_types.SECTION:
for section_key, section_item_type, section_val in val.panel_items_iter():
if section_item_type == panel_item_types.TOOL:
tool_id = section_key.replace('tool_', '', 1)
if tool_id in self._tools_by_id:
for term in self._get_edam_sec(section_val):
if term == 'uncategorized':
uncategorized.append((tool_id, key, section_val, val.name))
else:
for path in self.edam[term]['path']:
if len(path) == 1:
t = term
else:
t = path[0]
if path[0].startswith('operation_'):
operations[t][tool_id] = (term, tool_id, key, section_val, val.name)
if path[0].startswith('topic_'):
topics[t][tool_id] = (term, tool_id, key, section_val, val.name)
# elif section_item_type == panel_item_types.WORKFLOW:
# workflow_id = section_key.replace('workflow_', '', 1)
# if workflow_id in self._workflows_by_id:
# workflow = self._workflows_by_id[workflow_id]
# section.elems[section_key] = workflow
# log.debug(f"Loaded workflow: {workflow_id} {workflow.name}")
log.debug("Loading tool panel finished %s", execution_timer)
for term in sorted(operations.keys(), key=lambda x: self._sort_edam_key(x)):
if len(operations[term].keys()) == 0:
continue
elem = etree.Element('label')
elem.attrib['text'] = self.edam[term]['label']
elem.attrib['id'] = term
self._tool_panel[f"label_{term}"] = ToolSectionLabel(elem)
for (term, tool_id, key, val, val_name) in operations[term].values():
section = self._get_section(term, self.edam[term]['label'])
self.__add_tool_to_tool_panel(val, section, section=True)
self._integrated_section_by_tool[tool_id] = key, val_name
for term in sorted(topics.keys(), key=lambda x: self._sort_edam_key(x)):
if len(topics[term].keys()) == 0:
continue
elem = etree.Element('label')
elem.attrib['text'] = self.edam[term]['label']
elem.attrib['id'] = term
self._tool_panel[f"label_{term}"] = ToolSectionLabel(elem)
for (term, tool_id, key, val, val_name) in topics[term].values():
section = self._get_section(term, self.edam[term]['label'])
self.__add_tool_to_tool_panel(val, section, section=True)
self._integrated_section_by_tool[tool_id] = key, val_name
section = self._get_section('uncategorized', 'Uncategorized')
for (tool_id, key, val, val_name) in uncategorized:
print(tool_id, key, val, val_name)
self.__add_tool_to_tool_panel(val, section, section=True)
self._integrated_section_by_tool[tool_id] = key, val_name
def _sort_edam_key(self, x):
if x in ('operation_0004', 'topic_0003'):
return f"!{x}"
else:
return self.edam[x]['label']
def _load_tool_panel_views(self):
self._tool_panel_view_rendered = {}
registry = ToolBoxRegistryImpl(self)
for key, view in self._tool_panel_views.items():
self._tool_panel_view_rendered[key] = view.apply_view(self._integrated_tool_panel, registry)
def _load_tool_panel(self):
execution_timer = ExecutionTimer()
@@ -598,7 +495,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
tool_id = key.replace('tool_', '', 1)
if tool_id in self._tools_by_id:
self.__add_tool_to_tool_panel(val, self._tool_panel, section=False)
self._integrated_section_by_tool[tool_id] = '', ''
self._tool_panel.record_section_for_tool_id(tool_id, '', '')
elif item_type == panel_item_types.WORKFLOW:
workflow_id = key.replace('workflow_', '', 1)
if workflow_id in self._workflows_by_id:
@@ -620,7 +517,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
tool_id = section_key.replace('tool_', '', 1)
if tool_id in self._tools_by_id:
self.__add_tool_to_tool_panel(section_val, section, section=True)
self._integrated_section_by_tool[tool_id] = key, val.name
self._tool_panel.record_section_for_tool_id(tool_id, key, val.name)
elif section_item_type == panel_item_types.WORKFLOW:
workflow_id = section_key.replace('workflow_', '', 1)
if workflow_id in self._workflows_by_id:
@@ -930,7 +827,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
tool._lineage = self._lineage_map.register(tool)
self.register_tool(tool)
if load_panel_dict:
self.__add_tool_to_tool_panel(tool, panel_dict, section=isinstance(panel_dict, ToolSection))
self.__add_tool_to_tool_panel(tool, panel_dict)
def _load_workflow_tag_set(self, item, panel_dict, integrated_panel_dict, load_panel_dict, index=None):
try:
@@ -984,7 +881,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
if section_item_type == panel_item_types.TOOL:
if section_item:
tool_id = section_key.replace('tool_', '', 1)
self._integrated_section_by_tool[tool_id] = integrated_section.id, integrated_section.name
self._tool_panel.record_section_for_tool_id(tool_id, integrated_section.id, integrated_section.name)
if load_panel_dict:
self._tool_panel[key] = section
@@ -1007,10 +904,8 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
integrated_elems[key] = tool
if async_load:
if self.app.config.enable_beta_edam_toolbox:
self._load_tool_panel_edam()
else:
self._load_tool_panel()
self._load_tool_panel()
self._load_tool_panel_views()
self._save_integrated_tool_panel()
return tool.id
except Exception:
@@ -1141,15 +1036,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
new_tool.installed_changeset_revision = old_tool.installed_changeset_revision
new_tool.old_id = old_tool.old_id
# Replace old_tool with new_tool in self._tool_panel
tool_key = f"tool_{tool_id}"
for key, val in self._tool_panel.items():
if key == tool_key:
self._tool_panel[key] = new_tool
break
elif key.startswith('section'):
if tool_key in val.elems:
self._tool_panel[key].elems[tool_key] = new_tool
break
self._tool_panel.replace_tool_for_id(tool_id, new_tool)
# (Re-)Register the reloaded tool, this will handle
# _tools_by_id and _tool_versions_by_id
self.register_tool(new_tool)
@@ -1175,15 +1062,7 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
if tool_cache:
tool_cache.expire_tool(tool_id)
if remove_from_panel:
tool_key = f"tool_{tool_id}"
for key, val in self._tool_panel.items():
if key == tool_key:
del self._tool_panel[key]
break
elif key.startswith('section'):
if tool_key in val.elems:
del self._tool_panel[key].elems[tool_key]
break
self._tool_panel.remove_tool(tool_id)
if tool_id in self.data_manager_tools:
del self.data_manager_tools[tool_id]
# TODO: do we need to manually remove from the integrated panel here?
@@ -1195,6 +1074,10 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
return message, status
def get_sections(self):
"""Return section id and name pairs.
Only used by build_tool_panel_section_select_field in tool shed client code.
"""
for v in self._tool_panel.values():
if isinstance(v, ToolSection):
yield (v.id, v.name)
@@ -1226,11 +1109,16 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
stored = self.app.model.context.query(self.app.model.StoredWorkflow).get(id)
return stored.latest_workflow
def tool_panel_contents(self, trans, **kwds):
def tool_panel_contents(self, trans, view=None, **kwds):
""" Filter tool_panel contents for displaying for user.
"""
if view is None:
view = self._default_panel_view
if view not in self._tool_panel_view_rendered:
raise RequestParameterInvalidException(f"No panel view {view} found.")
filter_method = self._build_filter_method(trans)
for _, item_type, elt in self._tool_panel.panel_items_iter():
tool_panel_view = self._tool_panel_view_rendered[view]
for _, item_type, elt in tool_panel_view.panel_items_iter():
elt = filter_method(elt, item_type)
if elt:
yield elt
@@ -1257,14 +1145,14 @@ class AbstractToolBox(Dictifiable, ManagesIntegratedToolPanelMixin):
self._tool_to_dict_cache_admin[tool.id] = to_dict
return to_dict
def to_dict(self, trans, in_panel=True, tool_help=False, **kwds):
def to_dict(self, trans, in_panel=True, tool_help=False, view=None, **kwds):
"""
Create a dictionary representation of the toolbox.
Uses primitive cache for toolbox-specific tool 'to_dict's.
"""
rval = []
if in_panel:
panel_elts = list(self.tool_panel_contents(trans, **kwds))
panel_elts = list(self.tool_panel_contents(trans, view=view, **kwds))
for elt in panel_elts:
# Only use cache for objects that are Tools.
if hasattr(elt, "tool_type"):
@@ -1382,8 +1270,8 @@ class BaseGalaxyToolBox(AbstractToolBox):
shouldn't really depend on.
"""
def __init__(self, config_filenames, tool_root_dir, app, save_integrated_tool_panel=True):
super().__init__(config_filenames, tool_root_dir, app, save_integrated_tool_panel)
def __init__(self, config_filenames, tool_root_dir, app, view_sources=None, default_panel_view=None, save_integrated_tool_panel=True):
super().__init__(config_filenames, tool_root_dir, app, view_sources, default_panel_view, save_integrated_tool_panel)
old_toolbox = getattr(app, 'toolbox', None)
if old_toolbox:
self.dependency_manager = old_toolbox.dependency_manager
@@ -1,3 +1,5 @@
from typing import Dict, Optional
from galaxy.util.tool_version import remove_version_from_guid
from .interface import ToolLineage
@@ -7,16 +9,17 @@ class LineageMap:
"""
def __init__(self, app):
self.lineage_map = {}
self.lineage_map: Dict[str, ToolLineage] = {}
self.app = app
def register(self, tool):
def register(self, tool) -> ToolLineage:
tool_id = tool.id
versionless_tool_id = remove_version_from_guid(tool_id)
lineage = self.lineage_map.get(versionless_tool_id)
if not lineage:
lineage: ToolLineage
if versionless_tool_id not in self.lineage_map:
lineage = ToolLineage.from_tool(tool)
else:
lineage = self.lineage_map[versionless_tool_id]
# A lineage for a tool with the same versionless_tool_id exists,
# but this lineage may not have the current tools' version,
# so we add tool.version to the lineage
@@ -27,7 +30,7 @@ class LineageMap:
self.lineage_map[tool_id] = lineage
return self.lineage_map[tool_id]
def get(self, tool_id):
def get(self, tool_id) -> Optional[ToolLineage]:
"""
Get lineage for `tool_id`.
@@ -46,7 +49,7 @@ class LineageMap:
self.lineage_map[tool_id] = lineage
return self.lineage_map.get(tool_id)
def _get_versionless(self, tool_id):
def _get_versionless(self, tool_id) -> Optional[ToolLineage]:
versionless_tool_id = remove_version_from_guid(tool_id)
return self.lineage_map.get(versionless_tool_id, None)
@@ -1,5 +1,5 @@
import threading
from typing import Dict
from typing import Any, Dict, List
import packaging.version
from sortedcontainers import SortedSet
@@ -44,13 +44,13 @@ class ToolLineage:
self.tool_versions = SortedSet(key=packaging.version.parse)
@property
def tool_ids(self):
def tool_ids(self) -> List[str]:
versionless_tool_id = remove_version_from_guid(self.tool_id)
tool_id = versionless_tool_id or self.tool_id
return [f"{tool_id}/{version}" for version in self.tool_versions]
@staticmethod
def from_tool(tool):
def from_tool(tool) -> 'ToolLineage':
tool_id = tool.id
lineages_by_id = ToolLineage.lineages_by_id
with ToolLineage.lock:
@@ -60,7 +60,7 @@ class ToolLineage:
lineage.register_version(tool.version)
return lineage
def register_version(self, tool_version):
def register_version(self, tool_version) -> None:
assert tool_version is not None
self.tool_versions.add(str(tool_version))
@@ -71,12 +71,12 @@ class ToolLineage:
"""
return [ToolLineageVersion(tool_id, tool_version) for tool_id, tool_version in zip(self.tool_ids, self.tool_versions)]
def get_version_ids(self, reverse=False):
def get_version_ids(self, reverse=False) -> List[str]:
if reverse:
return list(reversed(self.tool_ids))
return self.tool_ids
def to_dict(self):
def to_dict(self) -> Dict[str, Any]:
return dict(
tool_id=self.tool_id,
tool_versions=list(self.tool_versions),
+102 -22
View File
@@ -1,18 +1,17 @@
from abc import abstractmethod
from enum import Enum
from typing import Dict, Optional, Tuple
from galaxy.util import bunch
from galaxy.util.dictifiable import Dictifiable
from galaxy.util.odict import odict
from .parser import ensure_tool_conf_item
panel_item_types = bunch.Bunch(
TOOL="TOOL",
WORKFLOW="WORKFLOW",
SECTION="SECTION",
LABEL="LABEL",
)
class panel_item_types(str, Enum):
TOOL = "TOOL"
WORKFLOW = "WORKFLOW"
SECTION = "SECTION"
LABEL = "LABEL"
class HasPanelItems:
@@ -65,7 +64,7 @@ class ToolSection(Dictifiable, HasPanelItems):
copy.name = self.name
copy.id = self.id
copy.version = self.version
copy.elems = self.elems.copy()
copy.elems.update(self.elems)
return copy
def to_dict(self, trans, link_details=False, tool_help=False, toolbox=None):
@@ -115,62 +114,143 @@ class ToolPanelElements(odict, HasPanelItems):
""" Represents an ordered dictionary of tool entries - abstraction
used both by tool panel itself (normal and integrated) and its sections.
"""
_section_by_tool: Dict[str, Tuple[str, str]] = {}
def update_or_append(self, index, key, value):
def record_section_for_tool_id(self, tool_id: str, key: str, val: str):
self._section_by_tool[tool_id] = (key, val)
def get_section_for_tool_id(self, tool_id: str) -> Tuple[Optional[str], Optional[str]]:
if tool_id in self._section_by_tool:
return self._section_by_tool[tool_id]
return (None, None)
def replace_tool_for_id(self, tool_id: str, new_tool) -> None:
tool_key = f"tool_{tool_id}"
for key, val in self.items():
if key == tool_key:
self[key] = new_tool
break
elif key.startswith('section'):
if tool_key in val.elems:
self[key].elems[tool_key] = new_tool
break
def get_or_create_section(self, sec_id: str, sec_nm: str) -> ToolSection:
if sec_id not in self:
section = ToolSection({'id': sec_id, 'name': sec_nm, 'version': ''})
self[sec_id] = section
else:
section = self[sec_id]
return section
def remove_tool(self, tool_id: str) -> None:
tool_key = f"tool_{tool_id}"
for key, val in self.items():
if key == tool_key:
del self[key]
break
elif key.startswith('section'):
if tool_key in val.elems:
del self[key].elems[tool_key]
break
def update_or_append(self, index: int, key: str, value) -> None:
if key in self or index is None:
self[key] = value
else:
self.insert(index, key, value)
def get_label(self, label):
def get_label(self, label: str) -> Optional[ToolSection]:
for element in self.values():
if isinstance(element, ToolSection) and element.name == label:
return element
return None
def has_tool_with_id(self, tool_id):
def has_tool_with_id(self, tool_id: str) -> bool:
key = f'tool_{tool_id}'
return key in self
def replace_tool(self, previous_tool_id, new_tool_id, tool):
def replace_tool(self, previous_tool_id: str, new_tool_id: str, tool) -> None:
previous_key = f'tool_{previous_tool_id}'
new_key = f'tool_{new_tool_id}'
index = self.keys().index(previous_key)
del self[previous_key]
self.insert(index, new_key, tool)
def index_of_tool_id(self, tool_id):
def index_of_tool_id(self, tool_id: str) -> Optional[int]:
query_key = f'tool_{tool_id}'
for index, target_key in enumerate(self.keys()):
if query_key == target_key:
return index
return None
def insert_tool(self, index, tool):
def insert_tool(self, index: int, tool) -> None:
key = f"tool_{tool.id}"
self.insert(index, key, tool)
def get_tool_with_id(self, tool_id):
def get_tool_with_id(self, tool_id: str):
key = f"tool_{tool_id}"
return self[key]
def append_tool(self, tool):
def append_tool(self, tool) -> None:
key = f"tool_{tool.id}"
self[key] = tool
def stub_tool(self, key):
def stub_tool(self, key: str) -> None:
key = f"tool_{key}"
self[key] = None
def stub_workflow(self, key):
def stub_workflow(self, key: str) -> None:
key = f'workflow_{key}'
self[key] = None
def stub_label(self, key):
def stub_label(self, key: str) -> None:
key = f'label_{key}'
self[key] = None
def append_section(self, key, section_elems):
self[key] = section_elems
def append_section(self, key: str, section: ToolSection) -> None:
self[key] = section
def panel_items(self):
return self
def walk_sections(self):
for key, item in self.items():
if isinstance(item, ToolSection):
yield (key, item)
def closest_section(self, target_section_id: Optional[str], target_section_name: Optional[str]):
for (section_id, section) in self.walk_sections():
if section_id == target_section_id:
return section
for (_, section) in self.walk_sections():
if section.name == target_section_name:
return section
return None
def apply_filter(self, f):
to_remove = []
for key, item in self.items():
if not f(key, item):
to_remove.append(key)
for key in to_remove:
del self[key]
def copy(self) -> 'ToolPanelElements':
the_copy = ToolPanelElements()
the_copy.update(self)
return the_copy
def has_item_recursive(self, item):
"""Check panel and section elements for supplied item."""
for value in self.values():
if value == item:
return True
if isinstance(value, ToolSection):
if item in value.elems.values():
return True
return False
+6
View File
@@ -137,6 +137,12 @@ def ensure_tool_conf_item(xml_or_item):
return None
elif isinstance(xml_or_item, ToolConfItem):
return xml_or_item
elif isinstance(xml_or_item, dict):
# TODO: handle sections...
as_dict = xml_or_item.copy()
type = as_dict.pop("type")
attributes = as_dict
return ToolConfItem(type, attributes, None)
else:
elem = xml_or_item
type = elem.tag
@@ -0,0 +1,155 @@
from enum import Enum
from typing import List, Optional, Union
from pydantic import BaseModel, Field
from typing_extensions import Literal
class StaticToolBoxViewTypeEnum(str, Enum):
generic = 'generic'
activity = 'activity'
publication = 'publication'
training = 'training'
class ExcludeTool(BaseModel):
tool_id: str
class ExcludeToolRegex(BaseModel):
tool_id_regex: str
class ExcludeTypes(BaseModel):
types: List[str]
Exclusions = Union[
ExcludeTool,
ExcludeToolRegex,
ExcludeTypes,
]
OptionalExclusionList = Optional[List[Exclusions]]
class Tool(BaseModel):
content_type: Literal['tool'] = Field(alias="type")
id: str
class Config:
allow_population_by_field_name = True
class Label(BaseModel):
content_type: Literal['label'] = Field(alias="type")
id: Optional[str]
text: str
class Config:
allow_population_by_field_name = True
class LabelShortcut(BaseModel):
content_type = "simple_label"
label: str
class Workflow(BaseModel):
content_type: Literal['workflow'] = Field(alias="type")
id: str
class Config:
allow_population_by_field_name = True
class ItemsFrom(BaseModel):
content_type = "items_from"
items_from: str
excludes: OptionalExclusionList
SectionContent = Union[
Tool,
Label,
LabelShortcut,
Workflow,
ItemsFrom,
]
class HasItems:
@property
def items_expanded(self):
# replace SectionAliases with individual SectionAlias objects
# replace LabelShortcuts with Labels
items = []
for item in self.items:
if isinstance(item, SectionAliases):
for section in item.sections:
section_alias = SectionAlias(
section=section,
excludes=item.excludes,
)
items.append(section_alias)
elif isinstance(item, LabelShortcut):
new_item = Label(
id=None,
text=item.label,
content_type="label",
)
items.append(new_item)
else:
items.append(item)
return items
class Section(BaseModel, HasItems):
content_type: Literal['section'] = Field(alias="type")
id: Optional[str]
name: Optional[str]
items: Optional[List[SectionContent]]
excludes: OptionalExclusionList
class Config:
allow_population_by_field_name = True
class SectionAlias(BaseModel):
content_type = "section_alias"
section: str
excludes: OptionalExclusionList
class SectionAliases(BaseModel):
content_type = "section_aliases"
sections: List[str]
excludes: OptionalExclusionList
RootContent = Union[
Section,
SectionAlias,
SectionAliases,
Tool,
Label,
LabelShortcut,
Workflow,
ItemsFrom,
]
class StaticToolBoxView(BaseModel, HasItems):
id: str
name: str
description: Optional[str]
view_type: StaticToolBoxViewTypeEnum = Field(alias="type")
items: List[RootContent]
excludes: Optional[List[Exclusions]]
@staticmethod
def from_dict(as_dict):
return StaticToolBoxView(**as_dict)
class Config:
allow_population_by_field_name = True
+168
View File
@@ -0,0 +1,168 @@
import logging
import os
from enum import Enum
from typing import Dict, List, Optional, Tuple
from galaxy.tool_util.edam_util import (
load_edam_tree,
ROOT_OPERATION,
ROOT_TOPIC,
)
from galaxy.util import (
etree,
ExecutionTimer,
)
from .interface import (
ToolBoxRegistry,
ToolPanelView,
ToolPanelViewModel,
ToolPanelViewModelType,
walk_loaded_tools,
)
from ..panel import (
ToolPanelElements,
ToolSectionLabel,
)
log = logging.getLogger(__name__)
class EdamPanelMode(str, Enum):
merged = "merged"
topics = "topics"
operations = "operations"
class EdamToolPanelView(ToolPanelView):
def __init__(self, edam_ontology_path: Optional[str], mode: EdamPanelMode = EdamPanelMode.merged):
edam = load_edam_tree(None if not edam_ontology_path or not os.path.exists(edam_ontology_path) else edam_ontology_path)
self.edam = edam
self.mode = mode
self.include_topics = mode in [EdamPanelMode.merged, EdamPanelMode.topics]
self.include_operations = mode in [EdamPanelMode.merged, EdamPanelMode.operations]
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
execution_timer = ExecutionTimer()
# Find the children of the top level topics
operations_list = [ROOT_OPERATION] + list(self._edam_children_of(ROOT_OPERATION))
topics_list = [ROOT_TOPIC] + list(self._edam_children_of(ROOT_TOPIC))
# Sort them (by english label)
# operations = sorted(operations, key=lambda x: self.edam[x]['label'])
# topics = sorted(topics, key=lambda x: self.edam[x]['label'])
# Convert these to list of dicts, wherein we'll add our tools/etc.
operations: Dict[str, Dict] = {
x: {}
for x in operations_list
}
topics: Dict[str, Dict] = {
x: {}
for x in topics_list
}
uncategorized: List[Tuple] = []
for tool_id, key, val, val_name in walk_loaded_tools(base_tool_panel, toolbox_registry):
for term in self._get_edam_sec(val):
if term == 'uncategorized':
uncategorized.append((tool_id, key, val, val_name))
else:
for path in self.edam[term]['path']:
if len(path) == 1:
t = term
else:
t = path[0]
if path[0].startswith('operation_'):
operations[t][tool_id] = (term, tool_id, key, val, val_name)
elif path[0].startswith('topic_'):
topics[t][tool_id] = (term, tool_id, key, val, val_name)
new_panel = ToolPanelElements()
for term in sorted(operations.keys(), key=lambda x: self._sort_edam_key(x)):
if len(operations[term].keys()) == 0:
continue
elem = etree.Element('label')
elem.attrib['text'] = self.edam[term]['label']
elem.attrib['id'] = term
new_panel[f"label_{term}"] = ToolSectionLabel(elem)
for (term, tool_id, key, val, val_name) in operations[term].values():
section = new_panel.get_or_create_section(term, self.edam[term]['label'])
toolbox_registry.add_tool_to_tool_panel_view(val, section)
new_panel.record_section_for_tool_id(tool_id, key, val_name)
for term in sorted(topics.keys(), key=lambda x: self._sort_edam_key(x)):
if len(topics[term].keys()) == 0:
continue
elem = etree.Element('label')
elem.attrib['text'] = self.edam[term]['label']
elem.attrib['id'] = term
new_panel[f"label_{term}"] = ToolSectionLabel(elem)
for (term, tool_id, key, val, val_name) in topics[term].values():
section = new_panel.get_or_create_section(term, self.edam[term]['label'])
toolbox_registry.add_tool_to_tool_panel_view(val, section)
new_panel.record_section_for_tool_id(tool_id, key, val_name)
section = new_panel.get_or_create_section('uncategorized', 'Uncategorized')
for (tool_id, key, val, val_name) in uncategorized:
toolbox_registry.add_tool_to_tool_panel_view(val, section)
new_panel.record_section_for_tool_id(tool_id, key, val_name)
log.debug("Loading EDAM tool panel finished %s", execution_timer)
return new_panel
def _get_edam_sec(self, tool):
edam = []
if self.include_operations:
edam.extend(tool.edam_operations)
if self.include_topics:
edam.extend(tool.edam_topics)
if len(edam) > 0:
for term in edam:
yield term
else:
yield 'uncategorized'
def _sort_edam_key(self, x):
if x in (ROOT_OPERATION, ROOT_TOPIC):
return f"!{x}"
else:
return self.edam[x]['label']
def _edam_children_of(self, parentTerm):
for term in self.edam.keys():
if parentTerm in self.edam[term]['parents']:
yield term
def to_model(self) -> ToolPanelViewModel:
mode = self.mode
if mode == EdamPanelMode.merged:
model_id = "ontology:edam_merged"
name = "EDAM Operations and Topics"
description = "Tools are grouped using both annotated operation and topic information (if availabled)."
elif mode == EdamPanelMode.operations:
model_id = "ontology:edam_operations"
name = "EDAM Operations"
description = "Tools are grouped using annotated EDAM operation information (if availabled)."
elif mode == EdamPanelMode.topics:
model_id = "ontology:edam_topics"
name = "EDAM Topics"
description = "Tools are grouped using annotated EDAM topic information (if availabled)."
else:
raise AssertionError(f"Invalid EDAM mode encountered {mode}")
model_class = self.__class__.__name__
view_type = ToolPanelViewModelType.ontology
return ToolPanelViewModel(
id=model_id,
name=name,
description=description,
model_class=model_class,
view_type=view_type,
searchable=True,
)
@@ -0,0 +1,75 @@
from abc import abstractmethod
from enum import Enum
from typing import Optional
from pydantic import BaseModel
from ..panel import (
HasPanelItems,
panel_item_types,
ToolPanelElements,
)
class ToolPanelViewModelType(str, Enum):
default_type = 'default'
generic = 'generic'
activity = 'activity'
ontology = 'ontology'
publication = 'publication'
training = 'training'
class ToolPanelViewModel(BaseModel):
"""A view of ToolPanelView objects serialized for the API."""
id: str
model_class: str
name: str
description: Optional[str]
view_type: ToolPanelViewModelType
searchable: bool # Allow for more dynamic views that don't plug into fixed search indicies in the future...
class ToolBoxRegistry:
"""View of ToolBox provided to ToolPanelView to reason about tools loaded."""
@abstractmethod
def has_tool(self, tool_id: str) -> bool:
"""Return bool indicating if tool with specified id is loaded."""
@abstractmethod
def get_tool(self, tool_id: str):
"""Return tool with supplied tool id."""
@abstractmethod
def get_workflow(self, id: str):
"""Return workflow from panel with supplied id."""
@abstractmethod
def add_tool_to_tool_panel_view(self, tool, tool_panel_component: HasPanelItems) -> None:
"""Add tool to the tool panel view component (root or section)."""
class ToolPanelView:
@abstractmethod
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
"""Consume tool panel state and return custom tool panel view."""
@abstractmethod
def to_model(self) -> ToolPanelViewModel:
"""Convert abstract description to dictionary description to emit via the API."""
def walk_loaded_tools(tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry):
for key, item_type, val in tool_panel.panel_items_iter():
if item_type == panel_item_types.TOOL:
tool_id = key.replace('tool_', '', 1)
if toolbox_registry.has_tool(tool_id):
yield (tool_id, key, val, val.name)
elif item_type == panel_item_types.SECTION:
for section_key, section_item_type, section_val in val.panel_items_iter():
if section_item_type == panel_item_types.TOOL:
tool_id = section_key.replace('tool_', '', 1)
if toolbox_registry.has_tool(tool_id):
yield (tool_id, key, section_val, val.name)
@@ -0,0 +1,52 @@
import logging
import os
from typing import Dict, List
import yaml
from galaxy.util import config_directories_from_setting
from .definitions import StaticToolBoxView
log = logging.getLogger(__name__)
EXTENSIONS = [".yml", ".yaml", ".json"]
class StaticToolBoxViewSources:
view_directories: List[str]
view_dicts: List[Dict]
def __init__(self, view_directories=None, view_dicts=None):
self.view_directories = config_directories_from_setting(view_directories) or []
self.view_dicts = view_dicts or []
def get_definitions(self) -> List[StaticToolBoxView]:
view_definitions = []
for view_dict in self.view_dicts:
view_definitions.append(StaticToolBoxView.from_dict(view_dict))
for view_directory in self.view_directories:
if not os.path.exists(view_directory):
log.warning(f"Failed to find toolbox view directory {view_directory}")
for filename in os.listdir(view_directory):
if not looks_like_view_source_filename(filename):
continue
view_path = os.path.join(view_directory, filename)
with open(view_path) as f:
view_dict = yaml.safe_load(f)
if "id" not in view_dict:
file_id = os.path.splitext(filename)[0]
view_dict["id"] = file_id
view_definitions.append(StaticToolBoxView.from_dict(view_dict))
return view_definitions
def looks_like_view_source_filename(filename: str) -> bool:
for ext in EXTENSIONS:
if filename.endswith(ext):
return True
return False
@@ -0,0 +1,168 @@
import logging
import re
from .definitions import (
ExcludeTool,
ExcludeToolRegex,
ExcludeTypes,
Section,
StaticToolBoxView,
Workflow,
)
from .interface import (
ToolBoxRegistry,
ToolPanelView,
ToolPanelViewModel,
ToolPanelViewModelType,
)
from ..panel import (
ToolPanelElements,
ToolSection,
ToolSectionLabel,
)
log = logging.getLogger(__name__)
def build_filter(excludes_):
excludes = excludes_ or []
def filter_function(panel_key, panel_value):
for exclude in excludes:
if panel_key.startswith("tool_"):
if isinstance(exclude, ExcludeTool):
if panel_value.id == exclude.tool_id:
return False
if panel_value.old_id == exclude.tool_id:
return False
if isinstance(exclude, ExcludeToolRegex):
if re.match(exclude.tool_id_regex, panel_value.id):
return False
if re.match(exclude.tool_id_regex, panel_value.old_id):
return False
if isinstance(exclude, ExcludeTypes):
if panel_key.startswith("label_") and "label" in exclude.types:
return False
if panel_key.startswith("tool_") and "tool" in exclude.types:
return False
if panel_key.startswith("workflow_") and "workflow" in exclude.types:
return False
return True
return filter_function
class StaticToolPanelView(ToolPanelView):
_definition: StaticToolBoxView
def __init__(self, definition: StaticToolBoxView):
self._definition = definition
def apply_view(self, base_tool_panel: ToolPanelElements, toolbox_registry: ToolBoxRegistry) -> ToolPanelElements:
def apply_filter(definition, elems):
if definition.excludes:
elems.apply_filter(build_filter(definition.excludes))
def definition_with_items_to_panel(definition, allow_sections: bool = True):
new_panel = ToolPanelElements()
for element in definition.items_expanded:
if element.content_type == "section":
assert allow_sections
section_def: Section = element
section: ToolSection
assert section_def.id is not None or section_def.name is not None
if element.items:
panel = definition_with_items_to_panel(section_def, allow_sections=False)
section = ToolSection()
if section_def.name is not None:
name = section_def.name
else:
assert section_def.id is not None
name = section_def.id
section.name = name
if section_def.id is not None:
section.id = section_def.id
else:
# TODO: there has to be tool shed code to do this in a consistent way... where is it?
section.id = name.replace(" ", "-").lower()
section.elems = panel
else:
closest_section = base_tool_panel.closest_section(section_def.id, section_def.name)
if closest_section is None:
log.warning(f"Failed to find matching section for (id, name) = ({section_def.id}, {section_def.name})")
continue
section = closest_section.copy()
if section_def.id is not None:
section.id = section_def.id
if section_def.name is not None:
section.name = section_def.name
apply_filter(section_def, section.elems)
new_panel.append_section(section.id, section)
elif element.content_type == "section_alias":
assert allow_sections
closest_section = base_tool_panel.closest_section(element.section, element.section)
if closest_section is None:
log.warning(f"Failed to find matching section for (id, name) = ({element.section}, {element.section})")
continue
section = closest_section.copy()
apply_filter(element, section.elems)
new_panel.append_section(section.id, section)
elif element.content_type == "label":
as_dict = {
"id": element.id or element.text.lower().replace(" ", "-"),
"text": element.text,
"type": "label",
}
label = ToolSectionLabel(as_dict)
key = f"label_{label.id}"
new_panel[key] = label
elif element.content_type == "tool":
tool_id = element.id
if not toolbox_registry.has_tool(tool_id):
log.warning(f"Failed to find tool_id {tool_id} from parent toolbox, cannot load into panel view")
continue
tool = toolbox_registry.get_tool(tool_id)
toolbox_registry.add_tool_to_tool_panel_view(tool, new_panel)
elif element.content_type == "workflow":
workflow_def: Workflow = element
workflow = toolbox_registry.get_workflow(element.id)
panel_id = f"workflow_{workflow_def.id}"
new_panel[panel_id] = workflow
elif element.content_type == "items_from":
closest_section = base_tool_panel.closest_section(element.items_from, element.items_from)
if closest_section is None:
log.warning(f"Failed to find matching section for (id, name) = ({element.items_from}, None)")
continue
elems = closest_section.elems.copy()
apply_filter(element, elems)
for key, item in elems.items():
new_panel[key] = item
else:
raise AssertionError("Unknown static toolbox configuration element encountered.")
if definition.excludes:
new_panel.apply_filter(build_filter(definition.excludes))
return new_panel
return definition_with_items_to_panel(self._definition)
def to_model(self) -> ToolPanelViewModel:
model_id = self._definition.id
name = self._definition.name
description = self._definition.description
view_type = ToolPanelViewModelType[self._definition.view_type.value]
model_class = self.__class__.__name__
return ToolPanelViewModel(
id=model_id,
name=name,
description=description,
model_class=model_class,
view_type=view_type,
searchable=True,
)
+9 -1
View File
@@ -49,6 +49,7 @@ from galaxy.tool_util.parser import (
from galaxy.tool_util.parser.xml import XmlPageSource
from galaxy.tool_util.provided_metadata import parse_tool_provided_metadata
from galaxy.tool_util.toolbox import BaseGalaxyToolBox
from galaxy.tool_util.toolbox.views.sources import StaticToolBoxViewSources
from galaxy.tools import expressions
from galaxy.tools.actions import DefaultToolAction
from galaxy.tools.actions.data_manager import DataManagerToolAction
@@ -279,10 +280,17 @@ class ToolBox(BaseGalaxyToolBox):
# FIXME: ./
if tool_root_dir == './tools':
tool_root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bundled'))
view_sources = StaticToolBoxViewSources(
view_directories=app.config.panel_views_dir,
view_dicts=app.config.panel_views,
)
default_panel_view = app.config.default_panel_view
super().__init__(
config_filenames=config_filenames,
tool_root_dir=tool_root_dir,
app=app,
view_sources=view_sources,
default_panel_view=default_panel_view,
save_integrated_tool_panel=save_integrated_tool_panel,
)
@@ -771,7 +779,7 @@ class Tool(Dictifiable):
return self.app.job_config.get_destination(self.__get_job_tool_configuration(job_params=job_params).destination)
def get_panel_section(self):
return self.app.toolbox.get_integrated_section_for_tool(self)
return self.app.toolbox.get_section_for_tool(self)
def allow_user_access(self, user, attempting_access=True):
"""
+63 -22
View File
@@ -7,6 +7,7 @@ through the library docs at https://whoosh.readthedocs.io.
import logging
import os
import re
from typing import Dict, List, Tuple, Union
from whoosh import (
analysis,
@@ -34,8 +35,11 @@ from galaxy.web.framework.helpers import to_unicode
log = logging.getLogger(__name__)
CanConvertToFloat = Union[str, int, float]
CanConvertToInt = Union[str, int, float]
def get_or_create_index(index_dir, schema):
def get_or_create_index(index_dir: str, schema: Schema) -> index.Index:
if not os.path.exists(index_dir):
os.makedirs(index_dir)
if index.exists_in(index_dir):
@@ -49,12 +53,44 @@ def get_or_create_index(index_dir, schema):
class ToolBoxSearch:
"""Support searching across all fixed panel views in a toolbox.
Search is delegated off to ToolPanelViewSearch for each panel object.
"""
def __init__(self, toolbox, index_dir: str, index_help: bool = True):
panel_searches = {}
for panel_view in toolbox.panel_views():
panel_view_id = panel_view.id
panel_index_dir = os.path.join(index_dir, panel_view_id)
panel_searches[panel_view_id] = ToolPanelViewSearch(toolbox, panel_view_id, panel_index_dir, index_help=index_help)
self.panel_searches = panel_searches
# We keep track of how many times the tool index has been rebuilt.
# We start at -1, so that after the first index the count is at 0,
# which is the same as the toolbox reload count. This way we can skip
# reindexing if the index count is equal to the toolbox reload count.
self.index_count = -1
def build_index(self, tool_cache, index_help: bool = True) -> None:
self.index_count += 1
for panel_search in self.panel_searches.values():
panel_search.build_index(tool_cache, index_help=index_help)
def search(self, *args, **kwd) -> List[str]:
panel_view = kwd.pop("panel_view")
if panel_view not in self.panel_searches:
raise KeyError(f"Unknown panel_view specified {panel_view}")
panel_search = self.panel_searches[panel_view]
return panel_search.search(*args, **kwd)
class ToolPanelViewSearch:
"""
Support searching tools in a toolbox. This implementation uses
the Whoosh search library.
"""
def __init__(self, toolbox, index_dir=None, index_help=True):
def __init__(self, toolbox, panel_view_id: str, index_dir: str, index_help: bool = True):
self.schema = Schema(id=ID(stored=True, unique=True),
old_id=ID,
stub=KEYWORD,
@@ -66,23 +102,18 @@ class ToolBoxSearch:
self.rex = analysis.RegexTokenizer()
self.index_dir = index_dir
self.toolbox = toolbox
self.panel_view_id = panel_view_id
self.index = self._index_setup()
# We keep track of how many times the tool index has been rebuilt.
# We start at -1, so that after the first index the count is at 0,
# which is the same as the toolbox reload count. This way we can skip
# reindexing if the index count is equal to the toolbox reload count.
self.index_count = -1
def _index_setup(self):
def _index_setup(self) -> index.Index:
return get_or_create_index(index_dir=self.index_dir, schema=self.schema)
def build_index(self, tool_cache, index_help=True):
def build_index(self, tool_cache, index_help: bool = True) -> None:
"""
Prepare search index for tools loaded in toolbox.
Use `tool_cache` to determine which tools need indexing and which tools should be expired.
"""
log.debug('Starting to build toolbox index.')
self.index_count += 1
log.debug(f"Starting to build toolbox index of panel {self.panel_view_id}.")
execution_timer = ExecutionTimer()
with self.index.reader() as reader:
# Index ocasionally contains empty stored fields
@@ -102,7 +133,7 @@ class ToolBoxSearch:
writer.delete_by_term('id', tool_id)
for tool_id in tool_cache._new_tool_ids - indexed_tool_ids:
tool = self.toolbox.get_tool(tool_id)
if tool and tool.is_latest_version:
if tool and tool.is_latest_version and self.toolbox.panel_has_tool(tool, self.panel_view_id):
if tool.hidden:
# we check if there is an older tool we can return
if tool.lineage:
@@ -117,9 +148,9 @@ class ToolBoxSearch:
continue
add_doc_kwds = self._create_doc(tool_id=tool_id, tool=tool, index_help=index_help)
writer.update_document(**add_doc_kwds)
log.debug("Toolbox index finished %s", execution_timer)
log.debug(f"Toolbox index of panel {self.panel_view_id} finished {execution_timer}")
def _create_doc(self, tool_id, tool, index_help=True):
def _create_doc(self, tool_id: str, tool, index_help: bool = True) -> Dict[str, str]:
# Do not add data managers to the public index
if tool.tool_type == 'manage_data':
return {}
@@ -153,10 +184,20 @@ class ToolBoxSearch:
pass
return add_doc_kwds
def search(self, q, tool_name_boost, tool_id_boost, tool_section_boost,
tool_description_boost, tool_label_boost, tool_stub_boost,
tool_help_boost, tool_search_limit, tool_enable_ngram_search,
tool_ngram_minsize, tool_ngram_maxsize):
def search(
self, q: str,
tool_name_boost: CanConvertToFloat,
tool_id_boost: CanConvertToFloat,
tool_section_boost: CanConvertToFloat,
tool_description_boost: CanConvertToFloat,
tool_label_boost: CanConvertToFloat,
tool_stub_boost: CanConvertToFloat,
tool_help_boost: CanConvertToFloat,
tool_search_limit: CanConvertToFloat,
tool_enable_ngram_search: bool,
tool_ngram_minsize: CanConvertToInt,
tool_ngram_maxsize: CanConvertToInt
) -> List[str]:
"""
Perform search on the in-memory index. Weight in the given boosts.
"""
@@ -194,13 +235,13 @@ class ToolBoxSearch:
hits = self.searcher.search(parsed_query, limit=float(tool_search_limit), sortedby='')
return [hit['id'] for hit in hits]
def _search_ngrams(self, cleaned_query, tool_ngram_minsize, tool_ngram_maxsize, tool_search_limit):
def _search_ngrams(self, cleaned_query: str, tool_ngram_minsize: CanConvertToInt, tool_ngram_maxsize: CanConvertToInt, tool_search_limit: CanConvertToFloat) -> List[str]:
"""
Break tokens into ngrams and search on those instead.
This should make searching more resistant to typos and unfinished words.
See docs at https://whoosh.readthedocs.io/en/latest/ngrams.html
"""
hits_with_score = {}
hits_with_score: Dict[str, float] = {}
token_analyzer = StandardAnalyzer() | analysis.NgramFilter(minsize=int(tool_ngram_minsize), maxsize=int(tool_ngram_maxsize))
ngrams = [token.text for token in token_analyzer(cleaned_query)]
for query in ngrams:
@@ -219,6 +260,6 @@ class ToolBoxSearch:
if not is_present:
hits_with_score[curr_hit['id']] = curr_hits.score(i)
# Sort the results based on aggregated BM25 score in decreasing order of scores
hits_with_score = sorted(hits_with_score.items(), key=lambda x: x[1], reverse=True)
hits_with_score_list: List[Tuple[str, float]] = sorted(hits_with_score.items(), key=lambda x: x[1], reverse=True)
# Return the tool ids
return [item[0] for item in hits_with_score[0:int(tool_search_limit)]]
return [item[0] for item in hits_with_score_list[0:int(tool_search_limit)]]
-1
View File
@@ -307,7 +307,6 @@ class JSAppLauncher(BaseUIController):
config = {
'active_view': 'analysis',
'enable_webhooks': True if trans.app.webhooks_registry.webhooks else False,
'toolbox': trans.app.toolbox.to_dict(trans),
'message_box_visible': trans.app.config.message_box_visible,
'show_inactivity_warning': trans.app.config.user_activation_on and trans.user and not trans.user.active,
'tool_shed_urls': list(trans.app.tool_shed_registry.tool_sheds.values()) if trans.app.tool_shed_registry else [],
+7 -3
View File
@@ -46,6 +46,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
:param in_panel: if true, tools are returned in panel structure,
including sections and labels
:param view: ToolBox view to apply (default is 'default')
:param trackster: if true, only tools that are compatible with
Trackster are returned
:param q: if present search on the given query will be performed
@@ -59,6 +60,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
q = kwds.get('q', '')
tool_id = kwds.get('tool_id', '')
tool_help = util.string_as_bool(kwds.get('tool_help', 'False'))
view = kwds.get("view", None)
# Find whether to search.
if q:
@@ -69,7 +71,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
else:
hits = None
else:
hits = self._search(q)
hits = self._search(q, view)
results = []
if hits:
for hit in hits:
@@ -90,7 +92,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
# Return everything.
try:
return self.app.toolbox.to_dict(trans, in_panel=in_panel, trackster=trackster, tool_help=tool_help)
return self.app.toolbox.to_dict(trans, in_panel=in_panel, trackster=trackster, tool_help=tool_help, view=view)
except Exception:
raise exceptions.InternalServerError("Error: Could not convert toolbox to dictionary")
@@ -377,7 +379,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
detected_versions.append(tool.version)
return detected_versions
def _search(self, q):
def _search(self, q, view):
"""
Perform the search on the given query.
Boosts and numer of results are configurable in galaxy.ini file.
@@ -388,6 +390,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
:return: Dictionary containing the tools' ids of the best hits.
:return type: dict
"""
panel_view = view or self.app.config.default_panel_view
tool_name_boost = self.app.config.get('tool_name_boost', 9)
tool_id_boost = self.app.config.get('tool_id_boost', 9)
tool_section_boost = self.app.config.get('tool_section_boost', 3)
@@ -401,6 +404,7 @@ class ToolsController(BaseGalaxyAPIController, UsesVisualizationMixin):
tool_ngram_maxsize = self.app.config.get('tool_ngram_maxsize', 4)
results = self.app.toolbox_search.search(q=q,
panel_view=panel_view,
tool_name_boost=tool_name_boost,
tool_id_boost=tool_id_boost,
tool_section_boost=tool_section_boost,
+36 -7
View File
@@ -2454,21 +2454,50 @@ mapping:
stable API. (The module state definitions may change and workflows built using
these modules may not function in the future.)
enable_beta_edam_toolbox:
type: bool
default: false
edam_panel_views:
type: str
default: 'operations,topics'
required: false
desc: |
Enable beta EDAM organised toolbox which ignores admin managed
sections in lieu of EDAM topics only.
Comma-separated list of the EDAM panel views to load - choose from merged, operations, topics.
Set to empty string to disable EDAM all together. Set default_panel_view to 'ontology:edam_topics'
to override default tool panel to use an EDAM view.
beta_edam_toolbox_ontology_path:
edam_toolbox_ontology_path:
type: str
default: EDAM.tsv
path_resolves_to: data_dir
required: false
desc: |
Sets the path to EDAM ontology file.
Sets the path to EDAM ontology file - if the path doesn't exist PyPI package data will be loaded.
panel_views_dir:
type: str
default: config/plugins/activities
required: false
desc: |
Directory to check out for toolbox tool panel views. The path is relative to the
Galaxy root dir. To use an absolute path begin the path with '/'. This is a
comma-separated list.
panel_views:
type: seq
sequence:
- type: any
required: false
desc: |
Definitions of static toolbox panel views embedded directly in the config instead of reading
YAML from directory with panel_views_dir.
default_panel_view:
type: str
default: default
required: false
desc: |
Default tool panel view for the current Galaxy configuration. This should refer to an id of
a panel view defined using the panel_views or panel_views_dir configuration options or an
EDAM panel view. The default panel view is simply called `default` and refers to the tool
panel state defined by the integrated tool panel.
default_workflow_export_format:
type: str
@@ -724,7 +724,6 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
'tags': item_tag_names,
'initialVersion': version,
'annotation': self.get_item_annotation_str(trans.sa_session, trans.user, stored),
'toolbox': trans.app.toolbox.to_dict(trans),
'moduleSections': module_sections,
'dataManagers': data_managers,
'workflows': workflows
+11 -3
View File
@@ -15,6 +15,7 @@ import sys
import tempfile
import threading
import time
from typing import Optional
from urllib.parse import urlparse
import nose.config
@@ -155,6 +156,7 @@ def setup_galaxy_config(
conda_auto_init=False,
conda_auto_install=False,
use_shared_connection_for_amqp=False,
allow_tool_conf_override: bool = True,
):
"""Setup environment and build config for test Galaxy instance."""
# For certain docker operations this needs to be evaluated out - e.g. for cwltool.
@@ -165,6 +167,7 @@ def setup_galaxy_config(
new_file_path = tempfile.mkdtemp(prefix='new_files_path_', dir=tmpdir)
job_working_directory = tempfile.mkdtemp(prefix='job_working_directory_', dir=tmpdir)
user_library_import_dir: Optional[str]
if use_test_file_dir:
first_test_file_dir = ensure_test_file_dir_set()
if not os.path.isabs(first_test_file_dir):
@@ -196,7 +199,10 @@ def setup_galaxy_config(
# For storing Data Manager outputs and .loc files so that real ones don't get clobbered
galaxy_data_manager_data_path = tempfile.mkdtemp(prefix='data_manager_tool-data', dir=tmpdir)
tool_conf = os.environ.get('GALAXY_TEST_TOOL_CONF', default_tool_conf)
if allow_tool_conf_override:
tool_conf = os.environ.get('GALAXY_TEST_TOOL_CONF', default_tool_conf)
else:
tool_conf = default_tool_conf
conda_auto_install = os.environ.get('GALAXY_TEST_CONDA_AUTO_INSTALL', conda_auto_install)
conda_auto_init = os.environ.get('GALAXY_TEST_CONDA_AUTO_INIT', conda_auto_init)
conda_prefix = os.environ.get('GALAXY_TEST_CONDA_PREFIX')
@@ -1002,7 +1008,8 @@ class GalaxyTestDriver(TestDriver):
else:
default_tool_conf = getattr(config_object, "default_tool_conf", None)
datatypes_conf_override = getattr(config_object, "datatypes_conf_override", None)
allow_tool_conf_override = getattr(config_object, "allow_tool_conf_override", True)
self.allow_tool_conf_override = allow_tool_conf_override
self.default_tool_conf = default_tool_conf
self.datatypes_conf_override = datatypes_conf_override
@@ -1047,7 +1054,8 @@ class GalaxyTestDriver(TestDriver):
log_format=self.log_format,
conda_auto_init=getattr(config_object, "conda_auto_init", False),
conda_auto_install=getattr(config_object, "conda_auto_install", False),
use_shared_connection_for_amqp=getattr(config_object, "use_shared_connection_for_amqp", False)
use_shared_connection_for_amqp=getattr(config_object, "use_shared_connection_for_amqp", False),
allow_tool_conf_override=self.allow_tool_conf_override,
)
galaxy_config = setup_galaxy_config(
galaxy_db_path,
+1 -1
View File
@@ -50,7 +50,7 @@ for ((i=0; i<${#PACKAGE_DIRS[@]}; i++)); do
pip install -e '.[template,jstree]'
fi
if [ "$package_dir" = "tool_util" ]; then
pip install -e '.[mulled]'
pip install -e '.[mulled,edam]'
fi
if [[ "$run_tests" == "1" ]]; then
+2
View File
@@ -2,3 +2,5 @@ galaxy-util>=20.1.0.dev0
galaxy-containers
lxml
sortedcontainers
pydantic
typing-extensions
+4
View File
@@ -43,6 +43,7 @@ PACKAGES = [
'galaxy.tool_util.toolbox',
'galaxy.tool_util.toolbox.filters',
'galaxy.tool_util.toolbox.lineages',
'galaxy.tool_util.toolbox.views',
'galaxy.tool_util.verify',
'galaxy.tool_util.verify.asserts',
]
@@ -104,6 +105,9 @@ setup(
'jinja2',
'Whoosh',
],
'edam': [
'edam-ontology',
],
},
license="AFL",
zip_safe=False,
+1
View File
@@ -34,6 +34,7 @@ circus = "*"
cwltool = "==3.0.20201109103151"
dictobj = "*"
docutils = "!=0.17, !=0.17.1"
edam-ontology = "*"
Fabric3 = "*"
fastapi = ">=0.65.2"
fastapi-utils = "*"
@@ -1,4 +1,10 @@
<tool id="mapper" name="Mapper" version="0.1.0">
<edam_topics>
<edam_topic>topic_0102</edam_topic><!-- CompBio -> SeqAnalysis -> Mapping -->
</edam_topics>
<edam_operations>
<edam_operation>operation_3198</edam_operation> <!-- Analysis -> SeqAnalysis -> Nucleic acid sequence analysis -> Sequence read processing -> Read Mapping -->
</edam_operations>
<command><![CDATA[
cp '$__tool_directory__/1.bam' '$out_file1'
]]></command>
@@ -0,0 +1,14 @@
id: my-custom
name: Custom Panel Filter
type: generic
items:
- type: label
text: The Start
- type: tool
id: empty_list
- type: label
text: The Middle
- type: tool
id: count_list
- type: label
text: The End
@@ -0,0 +1,4 @@
name: Filtered Test Section
type: generic
items:
- sections: [test,filter]
@@ -0,0 +1,7 @@
name: Filtered Test Section
type: activity
items:
- type: section
id: test
excludes:
- tool_id: multi_data_optional
@@ -0,0 +1,8 @@
name: Filtered Test Section
type: activity
items:
- type: section
id: test
excludes:
- tool_id_regex: 'multi_data_.*'
- tool_id_regex: '.*_text_option'
@@ -0,0 +1,15 @@
name: Custom Panel 4
type: generic
items:
- type: label
text: The Start
- type: tool
id: empty_list
- type: label
text: The Middle
- type: tool
id: count_list
- type: label
text: The End
excludes:
- types: [label]
@@ -0,0 +1,9 @@
name: Merged Section Example
type: generic
items:
- type: section
id: my-new-section
name: My New Section
items:
- items_from: test
- items_from: filter
@@ -0,0 +1,13 @@
name: Custom Panel in a New Section
type: generic
items:
- type: section
name: My Completely New Sectin
items:
- type: label
text: The Start
- type: tool
id: empty_list
- label: The Middle
- type: tool
id: count_list
@@ -0,0 +1,13 @@
name: Merged Section Example
type: generic
items:
- type: section
id: my-new-section
name: My New Section
items:
- items_from: test
excludes:
- tool_id_regex: '.*column.*'
- type: label
text: A cool label is here
- items_from: filter
@@ -0,0 +1,9 @@
name: Merged Section Example
type: generic
items:
- type: section
id: my-new-section
name: My New Section
items:
- items_from: Test Section
- items_from: filter
@@ -0,0 +1,7 @@
name: Filtered Test Section
type: activity
items:
- section: test
excludes:
- tool_id_regex: 'multi_data_.*'
- tool_id_regex: '.*_text_option'
@@ -0,0 +1,5 @@
name: Filtering Tools
type: activity
items:
- type: section
id: filter
+60
View File
@@ -0,0 +1,60 @@
from galaxy_test.driver import integration_util
class EdamToolboxIntegrationTestCase(integration_util.IntegrationTestCase):
framework_tool_and_types = True
@classmethod
def handle_galaxy_config_kwds(cls, config):
config["edam_panel_views"] = "merged"
def test_edam_toolbox(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="ontology:edam_merged"))
index.raise_for_status()
index_as_list = index.json()
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
section_names = [s["name"] for s in sections]
assert "Mapping" in section_names
mapping_section = [s for s in sections if s["name"] == "Mapping"][0]
mapping_section_elems = mapping_section["elems"]
# make sure our mapper tool was mapped using Edam correctly...
assert [x for x in mapping_section_elems if x["id"] == "mapper"]
config_index = self.galaxy_interactor.get("configuration")
config_index.raise_for_status()
panel_views = config_index.json()["panel_views"]
assert len(panel_views) > 1
assert isinstance(panel_views, dict)
edam_panel_view = panel_views["ontology:edam_merged"]
assert edam_panel_view["view_type"] == "ontology"
class EdamToolboxDefaultIntegrationTestCase(integration_util.IntegrationTestCase):
framework_tool_and_types = True
@classmethod
def handle_galaxy_config_kwds(cls, config):
config["edam_panel_views"] = "topics"
config["default_panel_view"] = "ontology:edam_topics"
def test_edam_toolbox(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True))
index.raise_for_status()
index_as_list = index.json()
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
section_names = [s["name"] for s in sections]
assert "Mapping" in section_names
mapping_section = [s for s in sections if s["name"] == "Mapping"][0]
mapping_section_elems = mapping_section["elems"]
# make sure our mapper tool was mapped using Edam correctly...
assert [x for x in mapping_section_elems if x["id"] == "mapper"]
config_index = self.galaxy_interactor.get("configuration")
config_index.raise_for_status()
panel_views = config_index.json()["panel_views"]
assert len(panel_views) > 1
assert isinstance(panel_views, dict)
edam_panel_view = panel_views["ontology:edam_topics"]
assert edam_panel_view["view_type"] == "ontology"
+178
View File
@@ -0,0 +1,178 @@
import os
from galaxy_test.driver import integration_util
THIS_DIR = os.path.dirname(__file__)
PANEL_VIEWS_DIR_1 = os.path.join(THIS_DIR, "panel_views_1")
class PanelViewsFromDirectoryIntegrationTestCase(integration_util.IntegrationTestCase):
framework_tool_and_types = True
allow_tool_conf_override = False
@classmethod
def handle_galaxy_config_kwds(cls, config):
config["panel_views_dir"] = PANEL_VIEWS_DIR_1
def test_section_copy(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="filter"))
index_as_list = index.json()
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
section_names = [s["name"] for s in sections]
assert len(section_names) == 1
assert "For Tours" in section_names
def test_custom_label_order(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="my-custom"))
verify_my_custom(index)
def test_filtering_sections_by_tool_id(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_2"))
index.raise_for_status()
index_as_list = index.json()
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
assert len(sections) == 1
section = sections[0]
tools = section["elems"]
assert len(tools) == 3, len(tools)
def test_filtering_sections_by_tool_id_regex(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_3"))
verify_custom_regex_filtered(index)
def test_filtering_root_by_type(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_4"))
index.raise_for_status()
index_as_list = index.json()
assert len(index_as_list) == 2
# Labels are filtered out...
assert model_classes(index_as_list) == ["Tool", "Tool"]
assert element_ids(index_as_list) == ["empty_list", "count_list"]
def test_custom_section_def(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_6"))
index.raise_for_status()
index_as_list = index.json()
assert len(index_as_list) == 1
assert model_classes(index_as_list) == ["ToolSection"]
section = index_as_list[0]
section_elems = section["elems"]
assert len(section_elems) == 4, model_classes(section_elems)
assert model_classes(section_elems) == ["ToolSectionLabel", "Tool", "ToolSectionLabel", "Tool"]
assert element_ids(section_elems) == ["the-start", "empty_list", "the-middle", "count_list"]
def test_section_embed(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_5"))
verify_custom_embed(index)
def test_section_embed_filtering(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_7"))
index.raise_for_status()
index_as_list = index.json()
assert len(index_as_list) == 1
assert model_classes(index_as_list) == ["ToolSection"]
section = index_as_list[0]
section_elems = section["elems"]
assert len(section_elems) == 5, model_classes(section_elems)
assert model_classes(section_elems) == ["Tool", "Tool", "Tool", "ToolSectionLabel", "Tool"]
elem_ids = element_ids(section_elems)
assert elem_ids[0:3] == ["multi_data_optional", "paths_as_file", "param_text_option"]
assert elem_ids[4] == "Filter1"
def test_section_reference_by_name(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_8"))
verify_custom_embed(index)
def test_section_alias(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_9"))
verify_custom_regex_filtered(index)
def test_expand_section_aliases(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True, view="custom_10"))
index.raise_for_status()
index_as_list = index.json()
assert len(index_as_list) == 2
assert model_classes(index_as_list) == ["ToolSection", "ToolSection"]
class PanelViewsFromConfigIntegrationTestCase(integration_util.IntegrationTestCase):
framework_tool_and_types = True
@classmethod
def handle_galaxy_config_kwds(cls, config):
config["panel_views"] = [
{
"id": "my-custom",
"name": "My Custom",
"type": "generic",
"items": [
{
"type": "label",
"text": "The Start",
}, {
"type": "tool",
"id": "empty_list",
}, {
"type": "label",
"text": "The Middle",
}, {
"type": "tool",
"id": "count_list",
}, {
"type": "label",
"text": "The End",
}
],
}
]
config["default_panel_view"] = "my-custom"
def test_custom_label_order(self):
index = self.galaxy_interactor.get("tools", data=dict(in_panel=True))
verify_my_custom(index)
def verify_my_custom(index):
index.raise_for_status()
index_as_list = index.json()
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
assert len(sections) == 0
assert len(index_as_list) == 5
assert model_classes(index_as_list) == ["ToolSectionLabel", "Tool", "ToolSectionLabel", "Tool", "ToolSectionLabel"]
def verify_custom_embed(index):
# custom_5 / custom_8
index.raise_for_status()
index_as_list = index.json()
assert len(index_as_list) == 1
assert model_classes(index_as_list) == ["ToolSection"]
section = index_as_list[0]
assert section["name"] == "My New Section"
assert section["id"] == "my-new-section"
section_elems = section["elems"]
assert len(section_elems) == 5, model_classes(section_elems)
assert model_classes(section_elems) == ["Tool", "Tool", "Tool", "Tool", "Tool"]
assert element_ids(section_elems) == ["multi_data_optional", "paths_as_file", "param_text_option", "column_param", "Filter1"]
def verify_custom_regex_filtered(index):
# custom_3 / custom_9
index.raise_for_status()
index_as_list = index.json()
sections = [x for x in index_as_list if x["model_class"] == "ToolSection"]
assert len(sections) == 1
section = sections[0]
tools = section["elems"]
assert len(tools) == 2, len(tools)
def element_ids(elements):
return [x["id"] for x in elements]
def model_classes(elements):
return [x["model_class"] for x in elements]
+1 -1
View File
@@ -99,7 +99,6 @@ class ExpectedValues:
self._expected_paths = {
'admin_tool_recommendations_path': self._in_config_dir('tool_recommendations_overwrite.yml'),
'auth_config_file': self._in_config_dir('auth_conf.xml'),
'beta_edam_toolbox_ontology_path': self._in_data_dir('EDAM.tsv'),
'build_sites_config_file': self._in_sample_dir('build_sites.yml.sample'),
'builds_file_path': self._in_root_dir('tool-data/shared/ucsc/builds.txt'),
'cache_dir': self._in_data_dir('cache'),
@@ -113,6 +112,7 @@ class ExpectedValues:
'datatypes_config_file': self._in_sample_dir('datatypes_conf.xml.sample'),
'dependency_resolvers_config_file': self._in_config_dir('dependency_resolvers_conf.xml'),
'dynamic_proxy_session_map': self._in_data_dir('session_map.sqlite'),
'edam_toolbox_ontology_path': self._in_data_dir('EDAM.tsv'),
'error_report_file': self._in_config_dir('error_report.yml'),
'file_path': self._in_data_dir('objects'),
'file_sources_config_file': self._in_config_dir('file_sources_conf.yml'),
+28
View File
@@ -0,0 +1,28 @@
import tempfile
from typing import Any, Dict
from edam_ontology.streams import tabular_stream
from galaxy.tool_util.edam_util import load_edam_tree
def test_load_edam_tree():
tree = load_edam_tree()
_verify_tree(tree)
def test_load_edam_tree_from_path():
tf = tempfile.NamedTemporaryFile("w")
tf.write(tabular_stream().read())
tf.flush()
tree = load_edam_tree(tf.name)
_verify_tree(tree)
def _verify_tree(tree: Dict[str, Any]):
assert tree is not None
assert "operation_0004" in tree
assert "topic_3974" in tree
assert tree["topic_3974"]["label"] == "Epistasis"
assert tree["topic_3974"]["parents"] == ['topic_0622', 'topic_3295']
assert tree["topic_3974"]["path"] == [['topic_3391', 'topic_0003'], ['topic_3070', 'topic_0003']]
@@ -0,0 +1,101 @@
import pydantic
from galaxy.tool_util.toolbox.views.definitions import StaticToolBoxView, StaticToolBoxViewTypeEnum
EXAMPLE_1 = {
"id": "rna",
"name": "RNA Seq",
"type": "activity",
"items": [
{
"type": "workflow",
"id": "12345abcd",
},
{
"type": "section",
"name": "My Cool Section",
},
{
"type": "label",
"text": "My Cool Label",
"id": "my-cool-label-id",
},
{
"type": "tool",
"id": "cat1",
},
],
}
def test_root_parsing():
view = StaticToolBoxView.from_dict(EXAMPLE_1)
assert view.id == "rna"
assert view.name == "RNA Seq"
assert view.view_type == StaticToolBoxViewTypeEnum.activity
assert len(view.items) == 4
workflow_item = view.items[0]
assert workflow_item.content_type == "workflow"
assert workflow_item.id == "12345abcd"
section_item = view.items[1]
assert section_item.content_type == "section"
assert section_item.id is None
assert section_item.name == "My Cool Section"
outer_label_item = view.items[2]
assert outer_label_item.content_type == "label"
assert outer_label_item.id == "my-cool-label-id"
assert outer_label_item.text == "My Cool Label"
output_tool_item = view.items[3]
assert output_tool_item.content_type == "tool"
assert output_tool_item.id == "cat1"
def test_section_parsing():
view = StaticToolBoxView.from_dict({
"id": "rna",
"name": "RNA Seq",
"type": "activity",
"items": [
{
"type": "section",
"name": "My Cool Section",
"items": [
{
"type": "workflow",
"id": "123456",
}
],
},
],
})
section_item = view.items[0]
assert len(section_item.items) == 1
assert section_item.items[0].content_type == "workflow"
def test_no_nested_sections():
exception_raised = False
try:
StaticToolBoxView.from_dict({
"id": "rna",
"name": "RNA Seq",
"type": "activity",
"items": [
{
"type": "section",
"name": "My Cool Section",
"items": [
{
"type": "section",
"name": "My Cool Inner Section",
},
],
},
],
})
except pydantic.ValidationError:
exception_raised = True
assert exception_raised
@@ -0,0 +1,45 @@
import json
import os
import shutil
import tempfile
from galaxy.tool_util.toolbox.views.sources import StaticToolBoxViewSources
from galaxy.util import safe_makedirs
from .test_toolbox_view_parsing import EXAMPLE_1
def test_from_dicts():
sources = StaticToolBoxViewSources(
view_dicts=[EXAMPLE_1]
)
defs = sources.get_definitions()
assert len(defs) == 1
assert defs[0].id == "rna"
def test_from_directory():
target_dir = tempfile.mkdtemp()
dir_1 = os.path.join(target_dir, "dir1")
dir_2 = os.path.join(target_dir, "dir2")
safe_makedirs(dir_1)
safe_makedirs(dir_2)
with open(os.path.join(dir_1, "foo.json"), "w") as f:
json.dump(EXAMPLE_1, f)
with open(os.path.join(dir_2, "seqanalysis.yml"), "w") as f:
f.write("""
name: Sequence Analysis
type: generic
items:
- type: workflow
id: 12345abcd
""")
config_param = f"{os.path.abspath(dir_1)},{os.path.abspath(dir_2)}"
try:
sources = StaticToolBoxViewSources(view_directories=config_param)
defs = sources.get_definitions()
assert len(defs) == 2
assert defs[0].id == "rna"
assert defs[1].id == "seqanalysis"
finally:
shutil.rmtree(target_dir)
+4
View File
@@ -371,6 +371,10 @@ class ToolBoxTestCase(BaseToolBoxTestCase):
assert len(section.elems) == 1
assert next(iter(section.elems.values())).id == "github.com/galaxyproject/example/test_tool/0.2"
test_tool = self.toolbox.get_tool("test_tool", tool_version="0.1")
section_pair = self.toolbox.get_section_for_tool(test_tool)
assert section_pair == ("tid", "TID")
def test_group_tools_out_of_section(self):
self._init_tool()
self._setup_two_versions_in_config(section=False)
+5
View File
@@ -197,6 +197,11 @@ class MockAppConfig(Bunch):
self.delay_tool_initialization = True
self.external_chown_script = None
self.default_panel_view = "default"
self.panel_views_dir = ''
self.panel_views = {}
self.edam_panel_views = ''
self.config_file = None
@property