diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e970a2d9b66..6599981196f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -6,7 +6,9 @@ repository](https://github.com/galaxyproject/galaxy).
For information on contributing more broadly to the Galaxy ecosystem and a
deeper discussion of some of these points - please see the
[Develop](https://galaxyproject.org/develop) section of the [Galaxy Community
-Hub](https://galaxyproject.org).
+Hub](https://galaxyproject.org) and the [Galaxy Code
+Architecture](https://training.galaxyproject.org/training-material/topics/dev/tutorials/architecture/slides.html#1) slides that are part of the [Galaxy Training
+Materials](https://training.galaxyproject.org/).
## Before you Begin
diff --git a/client/galaxy/scripts/components/Dataset/DatasetAsImage/DatasetAsImage.vue b/client/galaxy/scripts/components/Dataset/DatasetAsImage/DatasetAsImage.vue
new file mode 100644
index 00000000000..3847b70fa31
--- /dev/null
+++ b/client/galaxy/scripts/components/Dataset/DatasetAsImage/DatasetAsImage.vue
@@ -0,0 +1,41 @@
+
+
+
![]()
+
+ Image is not found
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/Dataset/DatasetIndex/DatasetIndex.vue b/client/galaxy/scripts/components/Dataset/DatasetIndex/DatasetIndex.vue
new file mode 100644
index 00000000000..762e014b574
--- /dev/null
+++ b/client/galaxy/scripts/components/Dataset/DatasetIndex/DatasetIndex.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+ {{ path }} {{ errorMessage }}
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/Dataset/DatasetLink/DatasetLink.vue b/client/galaxy/scripts/components/Dataset/DatasetLink/DatasetLink.vue
new file mode 100644
index 00000000000..c3a4411734d
--- /dev/null
+++ b/client/galaxy/scripts/components/Dataset/DatasetLink/DatasetLink.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/Dataset/compositeDatasetUtils.js b/client/galaxy/scripts/components/Dataset/compositeDatasetUtils.js
new file mode 100644
index 00000000000..67a0bdf5f27
--- /dev/null
+++ b/client/galaxy/scripts/components/Dataset/compositeDatasetUtils.js
@@ -0,0 +1,42 @@
+import { getAppRoot } from "onload/loadConfig";
+import { Services } from "components/Dataset/services";
+import store from "../../store/index";
+
+export const getPathDestination = async (history_dataset_id, path) => {
+ const services = new Services({ root: getAppRoot() });
+
+ const computePathDestination = (pathDestination) => {
+ if (pathDestination.datasetContent[0].class === "Directory")
+ pathDestination.datasetRootDir = datasetContent[0].path;
+ else return;
+
+ if (path === undefined || path === "undefined") {
+ return pathDestination;
+ }
+
+ const filepath = `${pathDestination.datasetRootDir}/${path}`;
+
+ const datasetEntry = datasetContent.find((datasetEntry) => {
+ return filepath === datasetEntry.path;
+ });
+
+ if (datasetEntry) {
+ if (datasetEntry.class === "Directory") {
+ pathDestination.isDirectory = true;
+ pathDestination.filepath = filepath;
+ return pathDestination;
+ }
+ pathDestination.fileLink = services.getCompositeDatasetLink(history_dataset_id, datasetEntry.path);
+ }
+ return pathDestination;
+ };
+
+ let datasetContent = store.getters.getDatasetExtFiles(history_dataset_id);
+
+ if (datasetContent == null) {
+ await store.dispatch("fetchDatasetExtFiles", history_dataset_id);
+ datasetContent = store.getters.getDatasetExtFiles(history_dataset_id);
+ }
+
+ return computePathDestination({ datasetContent: datasetContent });
+};
diff --git a/client/galaxy/scripts/components/Dataset/mount.js b/client/galaxy/scripts/components/Dataset/mount.js
new file mode 100644
index 00000000000..a577a4374e4
--- /dev/null
+++ b/client/galaxy/scripts/components/Dataset/mount.js
@@ -0,0 +1,32 @@
+/**
+ * Endpoint for mounting dataset components from non-Vue environment.
+ */
+import $ from "jquery";
+import DatasetLink from "./DatasetLink/DatasetLink";
+import { mountVueComponent } from "utils/mountVueComponent";
+import DatasetIndex from "./DatasetIndex/DatasetIndex";
+import DatasetAsImage from "./DatasetAsImage/DatasetAsImage";
+
+export const mountDatasetAsImage = () => {
+ return mountDatasetComponent(".dataset-as-image", ["history_dataset_id", "path"], DatasetAsImage);
+};
+export const mountDatasetIndex = () => {
+ return mountDatasetComponent(".dataset-index", ["history_dataset_id", "path"], DatasetIndex);
+};
+export const mountDatasetLink = () => {
+ return mountDatasetComponent(".dataset-link", ["history_dataset_id", "path", "label"], DatasetLink);
+};
+
+export const mountDatasetComponents = () => {
+ const all = [mountDatasetAsImage, mountDatasetIndex, mountDatasetLink];
+ all.forEach((func) => func());
+};
+
+const mountDatasetComponent = (selector, props, component, propsData = {}) => {
+ $(selector).each((index, el) => {
+ props.forEach((prop) => {
+ propsData[prop] = $(el).attr(prop);
+ });
+ mountVueComponent(component)(propsData, el);
+ });
+};
diff --git a/client/galaxy/scripts/components/Dataset/services.js b/client/galaxy/scripts/components/Dataset/services.js
index 4d836372d5f..f276215d467 100644
--- a/client/galaxy/scripts/components/Dataset/services.js
+++ b/client/galaxy/scripts/components/Dataset/services.js
@@ -1,4 +1,5 @@
import axios from "axios";
+import { rethrowSimple } from "utils/simple-error";
/** Datasets request helper **/
export class Services {
@@ -68,6 +69,30 @@ export class Services {
}
}
+ async getCompositeDatasetContentFiles(id) {
+ const url = `${this.root}api/histories/${id}/contents/${id}/extra_files`;
+ try {
+ const response = await axios.get(url);
+ return response.data;
+ } catch (e) {
+ rethrowSimple(e);
+ }
+ }
+
+ getCompositeDatasetLink(history_dataset_id, path) {
+ return `${this.root}api/histories/${history_dataset_id}/contents/${history_dataset_id}/display?filename=${path}`;
+ }
+
+ async getCompositeDatasetInfo(id) {
+ const url = `${this.root}api/histories/${id}/contents/${id}`;
+ try {
+ const response = await axios.get(url);
+ return response.data;
+ } catch (e) {
+ rethrowSimple(e);
+ }
+ }
+
_errorMessage(e) {
let message = "Request failed.";
if (e.response) {
diff --git a/client/galaxy/scripts/components/Datatypes/factory.js b/client/galaxy/scripts/components/Datatypes/factory.js
new file mode 100644
index 00000000000..19637145afb
--- /dev/null
+++ b/client/galaxy/scripts/components/Datatypes/factory.js
@@ -0,0 +1,7 @@
+import { DatatypesMapperModel } from "./model.js";
+import { getDatatypes } from "./services";
+
+export async function getDatatypesMapper() {
+ const typesAndMapping = await getDatatypes();
+ return new DatatypesMapperModel(typesAndMapping);
+}
diff --git a/client/galaxy/scripts/components/Datatypes/index.js b/client/galaxy/scripts/components/Datatypes/index.js
new file mode 100644
index 00000000000..3350500fd3b
--- /dev/null
+++ b/client/galaxy/scripts/components/Datatypes/index.js
@@ -0,0 +1 @@
+export { getDatatypesMapper } from "./factory.js";
diff --git a/client/galaxy/scripts/components/Datatypes/index.test.js b/client/galaxy/scripts/components/Datatypes/index.test.js
new file mode 100644
index 00000000000..bf895148209
--- /dev/null
+++ b/client/galaxy/scripts/components/Datatypes/index.test.js
@@ -0,0 +1,27 @@
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+
+import { getDatatypesMapper } from "./index";
+import { typesAndMappingResponse } from "./test_fixtures";
+
+describe("Datatypes/index.js", () => {
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ describe("getDatatypesMapper", () => {
+ it("should fetch logic from API for comparing datatypes in a hierarchy", async () => {
+ axiosMock.onGet(`/api/datatypes/types_and_mapping`).reply(200, typesAndMappingResponse);
+ await getDatatypesMapper().then((mapper) => {
+ expect(mapper.isSubType("txt", "data")).to.equals(true);
+ expect(mapper.isSubType("data", "txt")).to.equals(false);
+ });
+ });
+ });
+});
diff --git a/client/galaxy/scripts/components/Datatypes/model.js b/client/galaxy/scripts/components/Datatypes/model.js
new file mode 100644
index 00000000000..1039dd96d20
--- /dev/null
+++ b/client/galaxy/scripts/components/Datatypes/model.js
@@ -0,0 +1,13 @@
+export class DatatypesMapperModel {
+ constructor(typesAndMapping) {
+ this.datatypes = typesAndMapping.datatypes;
+ this.datatypesMapping = typesAndMapping.datatypes_mapping;
+ }
+
+ isSubType(child, parent) {
+ const mapping = this.datatypesMapping;
+ child = mapping.ext_to_class_name[child];
+ parent = mapping.ext_to_class_name[parent];
+ return mapping.class_to_classes[child] && parent in mapping.class_to_classes[child];
+ }
+}
diff --git a/client/galaxy/scripts/components/Datatypes/services.js b/client/galaxy/scripts/components/Datatypes/services.js
new file mode 100644
index 00000000000..f6ba04aa71f
--- /dev/null
+++ b/client/galaxy/scripts/components/Datatypes/services.js
@@ -0,0 +1,12 @@
+import axios from "axios";
+import { rethrowSimple } from "utils/simple-error";
+import { getAppRoot } from "onload/loadConfig";
+
+export async function getDatatypes() {
+ try {
+ const request = await axios.get(`${getAppRoot()}api/datatypes/types_and_mapping`);
+ return request.data;
+ } catch (e) {
+ rethrowSimple(e);
+ }
+}
diff --git a/client/galaxy/scripts/components/Datatypes/test_fixtures.js b/client/galaxy/scripts/components/Datatypes/test_fixtures.js
new file mode 100644
index 00000000000..39897c7480a
--- /dev/null
+++ b/client/galaxy/scripts/components/Datatypes/test_fixtures.js
@@ -0,0 +1,10 @@
+import DatatypesMappingJson from "qunit/test-data/json/datatypes.mapping.json";
+import DatatypesJson from "qunit/test-data/json/datatypes.json";
+import { DatatypesMapperModel } from "./model.js";
+
+export const typesAndMappingResponse = {
+ datatypes: DatatypesJson,
+ datatypes_mapping: DatatypesMappingJson,
+};
+
+export const testDatatypesMapper = new DatatypesMapperModel(typesAndMappingResponse);
diff --git a/client/galaxy/scripts/components/JobStates/mixin.js b/client/galaxy/scripts/components/JobStates/mixin.js
index 9d9652bd372..320d08e08ca 100644
--- a/client/galaxy/scripts/components/JobStates/mixin.js
+++ b/client/galaxy/scripts/components/JobStates/mixin.js
@@ -26,17 +26,8 @@ export default {
errorCount() {
return this.countStates(["error"]);
},
- runningPercent() {
- return this.runningCount / (this.jobCount * 1.0);
- },
- okPercent() {
- return this.okCount / (this.jobCount * 1.0);
- },
- errorPercent() {
- return this.errorCount / (this.jobCount * 1.0);
- },
- otherPercent() {
- return 1.0 - this.okPercent - this.runningPercent - this.errorPercent;
+ newCount() {
+ return this.jobCount - this.okCount - this.runningCount - this.errorCount;
},
},
methods: {
diff --git a/client/galaxy/scripts/components/Markdown/Markdown.vue b/client/galaxy/scripts/components/Markdown/Markdown.vue
index 9ac141bcaea..70b8538491f 100644
--- a/client/galaxy/scripts/components/Markdown/Markdown.vue
+++ b/client/galaxy/scripts/components/Markdown/Markdown.vue
@@ -14,6 +14,7 @@ import { getGalaxyInstance } from "app";
import { render_embedded_items } from "mvc/embedded-objects";
import { mountJobMetrics } from "components/JobMetrics";
import { mountJobParameters } from "components/JobParameters";
+import { mountDatasetComponents } from "components/Dataset/mount";
import MarkdownIt from "markdown-it";
import JOB_STATES_MODEL from "mvc/history/job-states-model";
@@ -22,7 +23,7 @@ import HDCAListItemEdit from "mvc/history/hdca-li-edit";
import HDCAListItem from "mvc/history/hdca-li";
const FUNCTION_VALUE_REGEX = `\\s*(?:[\\w_\\-]+|\\"[^\\"]+\\"|\\'[^\\']+\\')\\s*`;
const FUNCTION_CALL = `\\s*\\w+\\s*=` + FUNCTION_VALUE_REGEX;
-const FUNCTION_CALL_LINE = `\\s*(\\w+)\\s*\\((` + FUNCTION_CALL + `)(,` + FUNCTION_CALL + `)*\\)\\s*`;
+const FUNCTION_CALL_LINE = `\\s*(\\w+)\\s*\\(\\s*(?:(${FUNCTION_CALL})(,${FUNCTION_CALL})*)?\\s*\\)\\s*`;
const FUNCTION_CALL_LINE_TEMPLATE = new RegExp(FUNCTION_CALL_LINE, "m");
const md = MarkdownIt();
@@ -61,6 +62,17 @@ const RENDER_FUNCTIONS = {
`;
},
+ history_dataset_link: (action, args, content) => {
+ const history_dataset_id = args.history_dataset_id;
+ const path = args.path;
+ const label = args.label;
+ return `
`;
+ },
+ history_dataset_index: (action, args, content) => {
+ const history_dataset_id = args.history_dataset_id;
+ const path = args.path;
+ return ``;
+ },
history_dataset_collection_display: (action, args, content) => {
const history_dataset_collection_id = args.history_dataset_collection_id;
return ``;
@@ -90,7 +102,11 @@ const RENDER_FUNCTIONS = {
},
history_dataset_as_image: (action, args, content) => {
const history_dataset_id = args.history_dataset_id;
- return `
`;
+ const path = args.path;
+ if (path) {
+ `${getAppRoot}api/histories/${history_dataset_id}/contents/${history_dataset_id}/display?filename=${path}`;
+ }
+ return ``;
},
history_dataset_peek: (action, args, content) => {
const history_dataset_id = args.history_dataset_id;
@@ -100,6 +116,14 @@ const RENDER_FUNCTIONS = {
const history_dataset_id = args.history_dataset_id;
return ``;
},
+ history_dataset_name: (action, args, contents) => {
+ const history_dataset_id = args.history_dataset_id;
+ return ``;
+ },
+ history_dataset_type: (actions, args, contents) => {
+ const history_dataset_id = args.history_dataset_id;
+ return ``;
+ },
tool_stdout: (action, args, content) => {
const jobId = args.job_id;
return ``;
@@ -117,27 +141,42 @@ const RENDER_FUNCTIONS = {
const param = args.param;
return ``;
},
+ generate_galaxy_version: (actions, args, content) => {
+ return ``;
+ },
+ generate_time: (actions, args, content) => {
+ return ``;
+ },
+ invocation_time: (actions, args, content) => {
+ const invocationId = args.invocation_id;
+ return ``;
+ },
};
md.renderer.rules.fence = function (tokens, idx, options, env, slf) {
const token = tokens[idx];
const info = token.info ? token.info.trim() : "";
const content = token.content;
+
if (info == "galaxy") {
- const arr = FUNCTION_CALL_LINE_TEMPLATE.exec(content);
+ const galaxy_function = FUNCTION_CALL_LINE_TEMPLATE.exec(content);
const args = {};
- const action = arr[1];
- for (let i = 2; i < arr.length; i++) {
- if (arr[i] === undefined) continue;
- const arguments_str = arr[i].replace(/,/g, "").trim();
+ const function_name = galaxy_function[1];
+ // we need [... ] to return empty string, if regex doesn't match
+ const function_arguments = [...content.matchAll(new RegExp(FUNCTION_CALL, "g"))];
+
+ for (let i = 0; i < function_arguments.length; i++) {
+ if (function_arguments[i] === undefined) continue;
+ const arguments_str = function_arguments[i].toString().replace(/,/g, "").trim();
if (arguments_str) {
const [key, val] = arguments_str.split("=");
args[key.trim()] = val.replace(/['"]+/g, "").trim();
}
}
- return RENDER_FUNCTIONS[action](action, args, content);
+
+ return RENDER_FUNCTIONS[function_name](function_name, args, content);
} else {
return default_fence(tokens, idx, options, env, slf);
}
@@ -172,6 +211,9 @@ export default {
historyDatasetCollections: {},
workflows: {},
jobs: {},
+ invocations: {},
+ generateTime: null,
+ generateGalaxyVersion: null,
};
},
computed: {
@@ -188,11 +230,16 @@ export default {
this.historyDatasetCollections = mConfig.history_dataset_collections || {};
this.workflows = mConfig.workflows || {};
this.jobs = mConfig.jobs || {};
+ this.invocations = mConfig.invocations || {};
+ this.generateGalaxyVersion = mConfig.generate_version || "Unknown Galaxy Version";
+ this.generateTime = mConfig.generate_time;
this.$nextTick(() => {
render_embedded_items();
mountJobMetrics({ includeTitle: false });
mountJobParameters({ includeTitle: false });
+ mountDatasetComponents();
+
$("span.render-name").each((i, el) => {
const historyDatasetId = $(el).attr("history_dataset_id");
if (historyDatasetId) {
@@ -208,6 +255,13 @@ export default {
render_fenced_output("tool-stderr", this.jobs, "job_id", "tool_stderr");
render_fenced_output("dataset-peek", this.historyDatasets, "history_dataset_id", "peek");
render_fenced_output("dataset-info", this.historyDatasets, "history_dataset_id", "info");
+ render_fenced_output("dataset-name", this.historyDatasets, "history_dataset_id", "name");
+ render_fenced_output("dataset-type", this.historyDatasets, "history_dataset_id", "ext");
+
+ $(".galaxy-version code").text(this.generateGalaxyVersion);
+ $(".generate-time code").text(this.generateTime);
+
+ render_fenced_output("invocation-time", this.invocations, "invocation_id", "create_time");
$(".dataset-collection").each((i, el) => {
const Galaxy = getGalaxyInstance();
diff --git a/client/galaxy/scripts/components/ProgressBar.vue b/client/galaxy/scripts/components/ProgressBar.vue
index 5a9b7a58c2d..13fd7f5544b 100644
--- a/client/galaxy/scripts/components/ProgressBar.vue
+++ b/client/galaxy/scripts/components/ProgressBar.vue
@@ -1,35 +1,47 @@
-
-
- {{ note }}...
-
-
-
-
-
-
+
+
+ {{ note }}...
+
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/Workflow/Editor/modules/services.js b/client/galaxy/scripts/components/Workflow/Editor/modules/services.js
index 3696e07290c..07c4098e144 100644
--- a/client/galaxy/scripts/components/Workflow/Editor/modules/services.js
+++ b/client/galaxy/scripts/components/Workflow/Editor/modules/services.js
@@ -13,18 +13,6 @@ export async function getVersions(id) {
}
}
-export async function getDatatypes() {
- try {
- const datatypesRequest = await axios.get(`${getAppRoot()}api/datatypes`);
- const datatypes = datatypesRequest.data;
- const mappingRequest = await axios.get(`${getAppRoot()}api/datatypes/mapping`);
- const datatypes_mapping = mappingRequest.data;
- return { datatypes, datatypes_mapping };
- } catch (e) {
- rethrowSimple(e);
- }
-}
-
export async function getModule(request_data) {
try {
const { data } = await axios.post(`${getAppRoot()}api/workflows/build_module`, request_data);
diff --git a/client/galaxy/scripts/components/Workflow/Editor/modules/terminals.js b/client/galaxy/scripts/components/Workflow/Editor/modules/terminals.js
index eeb5444b96f..df022d214e0 100644
--- a/client/galaxy/scripts/components/Workflow/Editor/modules/terminals.js
+++ b/client/galaxy/scripts/components/Workflow/Editor/modules/terminals.js
@@ -219,7 +219,7 @@ class Terminal extends EventEmitter {
class BaseInputTerminal extends Terminal {
constructor(attr) {
super(attr);
- this.datatypesMapping = attr.datatypesMapping;
+ this.datatypesMapper = attr.datatypesMapper;
this.update(attr.input); // subclasses should implement this...
}
canAccept(other) {
@@ -348,10 +348,7 @@ class BaseInputTerminal extends Terminal {
);
}
_isSubType(child, parent) {
- const mapping = this.datatypesMapping;
- child = mapping.ext_to_class_name[child];
- parent = mapping.ext_to_class_name[parent];
- return mapping.class_to_classes[child] && parent in mapping.class_to_classes[child];
+ return this.datatypesMapper.isSubType(child, parent);
}
_producesAcceptableDatatypeAndOptionalness(other) {
if (!this.optional && !this.multiple && other.optional) {
diff --git a/client/galaxy/scripts/components/Workflow/Run/WorkflowRun.test.js b/client/galaxy/scripts/components/Workflow/Run/WorkflowRun.test.js
index 218650ac4f9..9bb7018ffb2 100644
--- a/client/galaxy/scripts/components/Workflow/Run/WorkflowRun.test.js
+++ b/client/galaxy/scripts/components/Workflow/Run/WorkflowRun.test.js
@@ -3,6 +3,7 @@ import axios from "axios";
import { __RewireAPI__ as rewire } from "./WorkflowRun";
import WorkflowRun from "./WorkflowRun.vue";
import { mount, createLocalVue } from "@vue/test-utils";
+import flushPromises from "flush-promises";
import sampleRunData1 from "./testdata/run1.json";
@@ -34,10 +35,7 @@ describe("WorkflowRun.vue", () => {
expect(wrapper.vm.loading).to.equal(true);
expect(wrapper.vm.error).to.equal(null);
expect(wrapper.vm.model).to.equal(null);
- await localVue.nextTick();
- await localVue.nextTick();
- await localVue.nextTick();
- await localVue.nextTick();
+ await flushPromises();
expect(wrapper.vm.error).to.equal(null);
expect(wrapper.vm.loading).to.equal(false);
const model = wrapper.vm.model;
diff --git a/client/galaxy/scripts/components/Workflow/Run/WorkflowRunSuccess.vue b/client/galaxy/scripts/components/Workflow/Run/WorkflowRunSuccess.vue
index 68a810febcf..8b11155a155 100644
--- a/client/galaxy/scripts/components/Workflow/Run/WorkflowRunSuccess.vue
+++ b/client/galaxy/scripts/components/Workflow/Run/WorkflowRunSuccess.vue
@@ -19,11 +19,11 @@
-
+ />
diff --git a/client/galaxy/scripts/components/WorkflowInvocationState/WorkflowInvocationState.vue b/client/galaxy/scripts/components/WorkflowInvocationState/WorkflowInvocationState.vue
index 17542e426e0..6474a022013 100644
--- a/client/galaxy/scripts/components/WorkflowInvocationState/WorkflowInvocationState.vue
+++ b/client/galaxy/scripts/components/WorkflowInvocationState/WorkflowInvocationState.vue
@@ -1,65 +1,46 @@
-
-
- Workflow Invocation State
- (invoked at {{ createdTime }})
-
-
-
- Step Scheduling
-
-
-
-
-
-
-
-
- View Invocation Report
-
+
+
+
+
+ Invocation {{ index + 1 }}...
+
+
+
+
+
+
+
+
-
diff --git a/client/galaxy/scripts/layout/page.js b/client/galaxy/scripts/layout/page.js
index 5c29c54022e..634a48f67a7 100644
--- a/client/galaxy/scripts/layout/page.js
+++ b/client/galaxy/scripts/layout/page.js
@@ -114,10 +114,13 @@ const View = Backbone.View.extend({
if (this.config.message_box_visible) {
const content = this.config.message_box_content || "";
const level = this.config.message_box_class || "info";
- this.$el.addClass("has-message-box");
- this.$messagebox.attr("class", `panel-${level}-message`).html(content).toggle(!!content).show();
+ this.$messagebox
+ .attr("class", `alert alert-${level} rounded-0 m-0 p-2`)
+ .append($("").attr("class", "fa fa-fw mr-1 fa-exclamation"))
+ .append(content)
+ .toggle(!!content)
+ .show();
} else {
- this.$el.removeClass("has-message-box");
this.$messagebox.hide();
}
return this;
@@ -128,12 +131,16 @@ const View = Backbone.View.extend({
if (this.config.show_inactivity_warning) {
const content = this.config.inactivity_box_content || "";
const verificationLink = $("")
+ .attr("class", "ml-1")
.attr("href", `${getAppRoot()}user/resend_verification`)
.text("Resend verification");
- this.$el.addClass("has-inactivity-box");
- this.$inactivebox.html(`${content} `).append(verificationLink).toggle(!!content).show();
+ this.$inactivebox
+ .append($("").attr("class", "fa fa-fw mr-1 fa-exclamation-triangle"))
+ .append(content)
+ .append(verificationLink)
+ .toggle(!!content)
+ .show();
} else {
- this.$el.removeClass("has-inactivity-box");
this.$inactivebox.hide();
}
return this;
@@ -159,8 +166,8 @@ const View = Backbone.View.extend({
`
-
-
+
+
diff --git a/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js b/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js
index 19c0d7a0c8e..040e2982de2 100644
--- a/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js
+++ b/client/galaxy/scripts/qunit/tests/workflow_editor_tests.js
@@ -1,6 +1,6 @@
/* global QUnit */
import $ from "jquery";
-import DatatypesMapping from "qunit/test-data/json/datatypes.mapping.json";
+import { testDatatypesMapper } from "components/Datatypes/test_fixtures";
import sinon from "sinon";
import WorkflowNode from "components/Workflow/Editor/Node";
import Terminals from "components/Workflow/Editor/modules/terminals";
@@ -49,7 +49,7 @@ function buildNode(propsData) {
propsData.getCanvasManager = () => {
return {};
};
- propsData.datatypesMapping = DatatypesMapping;
+ propsData.datatypesMapper = testDatatypesMapper;
return new component({
propsData: propsData,
el: "#canvas-container",
@@ -63,7 +63,7 @@ QUnit.module("Input terminal model test", {
this.input = { extensions: ["txt"], multiple: false, optional: false };
const inputEl = $("
")[0];
this.input_terminal = new Terminals.InputTerminal({
- datatypesMapping: DatatypesMapping,
+ datatypesMapper: testDatatypesMapper,
element: inputEl,
input: this.input,
node: this.node,
@@ -309,7 +309,7 @@ QUnit.module("Input collection terminal model test", {
this.input = { extensions: ["txt"], collection_types: ["list"] };
const inputEl = $("
")[0];
this.input_terminal = new Terminals.InputCollectionTerminal({
- datatypesMapping: DatatypesMapping,
+ datatypesMapper: testDatatypesMapper,
element: inputEl,
input: this.input,
node: this.node,
@@ -869,7 +869,7 @@ QUnit.module("terminal mapping logic", {
}
const inputEl = $("
")[0];
const inputTerminal = new Terminals.InputTerminal({
- datatypesMapping: DatatypesMapping,
+ datatypesMapper: testDatatypesMapper,
element: inputEl,
input: input,
});
@@ -887,7 +887,7 @@ QUnit.module("terminal mapping logic", {
}
const inputEl = $("
")[0];
const inputTerminal = new Terminals.InputCollectionTerminal({
- datatypesMapping: DatatypesMapping,
+ datatypesMapper: testDatatypesMapper,
element: inputEl,
input: input,
node: node,
diff --git a/client/galaxy/scripts/store/datasetExtFilesStore.js b/client/galaxy/scripts/store/datasetExtFilesStore.js
new file mode 100644
index 00000000000..c07cecb1d85
--- /dev/null
+++ b/client/galaxy/scripts/store/datasetExtFilesStore.js
@@ -0,0 +1,35 @@
+export const state = {
+ datasetExtFilesById: {},
+};
+
+import Vue from "vue";
+import { getAppRoot } from "onload/loadConfig";
+import axios from "axios";
+
+const getters = {
+ getDatasetExtFiles: (state) => (history_dataset_id) => {
+ return state.datasetExtFilesById[history_dataset_id] || null;
+ },
+};
+
+const actions = {
+ fetchDatasetExtFiles: async ({ commit }, history_dataset_id) => {
+ const { data } = await axios.get(
+ `${getAppRoot()}api/histories/${history_dataset_id}/contents/${history_dataset_id}/extra_files`
+ );
+ commit("saveDatasetExtFiles", { history_dataset_id, datasetExtFiles: data });
+ },
+};
+
+const mutations = {
+ saveDatasetExtFiles: (state, { history_dataset_id, datasetExtFiles }) => {
+ Vue.set(state.datasetExtFilesById, history_dataset_id, datasetExtFiles);
+ },
+};
+
+export const datasetExtFilesStore = {
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/client/galaxy/scripts/store/datasetPathDestinationStore.js b/client/galaxy/scripts/store/datasetPathDestinationStore.js
new file mode 100644
index 00000000000..b687a3aa1ce
--- /dev/null
+++ b/client/galaxy/scripts/store/datasetPathDestinationStore.js
@@ -0,0 +1,35 @@
+export const state = {
+ datasetPathDestination: {},
+};
+
+import { getPathDestination } from "components/Dataset/compositeDatasetUtils";
+
+const getters = {
+ pathDestination: (state) => (history_dataset_id, path) => {
+ if (state.datasetPathDestination[history_dataset_id]) {
+ return state.datasetPathDestination[history_dataset_id][path];
+ } else {
+ return;
+ }
+ },
+};
+
+const actions = {
+ fetchPathDestination: async ({ commit }, { history_dataset_id, path }) => {
+ const data = await getPathDestination(history_dataset_id, path);
+ commit("savePathDestination", { history_dataset_id, path, pathDestination: data });
+ },
+};
+
+const mutations = {
+ savePathDestination: (state, { history_dataset_id, path, pathDestination }) => {
+ state.datasetPathDestination[history_dataset_id] = { [path]: pathDestination };
+ },
+};
+
+export const datasetPathDestinationStore = {
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/client/galaxy/scripts/store/index.js b/client/galaxy/scripts/store/index.js
index 0f89419a996..9554dd86af8 100644
--- a/client/galaxy/scripts/store/index.js
+++ b/client/galaxy/scripts/store/index.js
@@ -15,6 +15,8 @@ import { historyStore } from "./historyStore";
import { userStore } from "./userStore";
import { configStore } from "./configStore";
import { workflowStore } from "./workflowStore";
+import { datasetPathDestinationStore } from "./datasetPathDestinationStore";
+import { datasetExtFilesStore } from "./datasetExtFilesStore";
Vue.use(Vuex);
@@ -33,6 +35,8 @@ export function createStore() {
tags: tagStore,
jobMetrics: jobMetricsStore,
destinationParameters: jobDestinationParametersStore,
+ datasetPathDestination: datasetPathDestinationStore,
+ datasetExtFiles: datasetExtFilesStore,
invocations: invocationStore,
user: userStore,
config: configStore,
diff --git a/client/galaxy/style/scss/base.scss b/client/galaxy/style/scss/base.scss
index 44471795d57..a14fbf59619 100644
--- a/client/galaxy/style/scss/base.scss
+++ b/client/galaxy/style/scss/base.scss
@@ -316,43 +316,7 @@ body {
}
}
-// Messages for message box, slightly different style
-
-.panel-error-message,
-.panel-warning-message,
-.panel-done-message,
-.panel-info-message {
- height: $panel-message-height;
- line-height: $panel-message-height;
- padding: 0px;
- padding-left: 2.5rem;
- background-color: $state-danger-bg;
- background-image: url(../../images/error_small.png);
- background-repeat: no-repeat;
- background-position: 0.5rem 50%;
-}
-
-.panel-warning-message {
- background-image: url(../../images/warn_small.png);
- background-color: $state-warning-bg;
-}
-
-.panel-done-message {
- background-image: url(../../images/ok_small.png);
- background-color: $state-success-bg;
-}
-
-.panel-info-message {
- background-image: url(../../images/info_icon.svg);
- background-size: 1.5rem;
- background-color: $state-info-bg;
- svg:path {
- fill: black;
- }
-}
-
// ==== Masthead ====
-
#masthead {
@extend .p-0;
@extend .mb-0;
@@ -961,34 +925,7 @@ ul.manage-table-actions li {
margin-left: 0.5em;
}
-.state-progress {
- @extend .position-relative;
- @extend .m-1;
- border: $border-default;
- .info {
- color: $text-color;
- background: $body-bg;
- }
- .new {
- background: $state-default-bg;
- }
- .running {
- background: $state-running-bg;
- }
- .ok {
- background: $state-success-bg;
- }
- .error {
- background: $state-danger-bg;
- }
- .note {
- @extend .position-absolute;
- @extend .ml-2;
- }
-}
-
// State colors
-
.state-color-new {
border-color: $state-default-border;
background: $state-default-bg;
diff --git a/client/galaxy/style/scss/overrides.scss b/client/galaxy/style/scss/overrides.scss
index ca716be3741..69b0aa6727c 100644
--- a/client/galaxy/style/scss/overrides.scss
+++ b/client/galaxy/style/scss/overrides.scss
@@ -147,3 +147,14 @@ pre.code {
padding: 0.5rem 1.5rem 0.5rem 1rem;
font-weight: bold;
}
+
+// progress bar
+.progress-bar.bg-success {
+ background: $state-success-bg !important;
+}
+.progress-bar.bg-danger {
+ background: $state-danger-bg !important;
+}
+.progress-bar.bg-warning {
+ background: $state-warning-bg !important;
+}
diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst
index 1a3199e173f..c30c71da4e5 100644
--- a/doc/source/admin/galaxy_options.rst
+++ b/doc/source/admin/galaxy_options.rst
@@ -1261,8 +1261,9 @@
:Description:
E-mail domains blocklist is used for filtering out users that are
using disposable email addresses at registration. If their
- address domain matches any domain on the list, they are refused
- registration.
+ address's base domain matches any domain on the list, they are
+ refused registration. Address subdomains are ignored (both
+ 'name@spam.com' and 'name@foo.spam.com' will match 'spam.com').
Example value 'email_blocklist.conf'
The value of this option will be resolved with respect to
.
@@ -1278,8 +1279,11 @@
E-mail domains allowlist is used to specify allowed email address
domains. If the list is non-empty and a user attempts registration
using an email address belonging to a domain that is not on the
- list, registration will be denied. This is a more restrictive
- option than , and therefore, in case
+ list, registration will be denied. Unlike
+ which matches the address's base
+ domain, here email addresses are matched against the full domain
+ (base + subdomain). This is a more restrictive option than
+ , and therefore, in case
is set and is not empty,
will be ignored.
Example value 'email_allowlist.conf'
@@ -3569,6 +3573,22 @@
:Type: bool
+~~~~~~~~~~~~~~~~~~~~~
+``metadata_strategy``
+~~~~~~~~~~~~~~~~~~~~~
+
+:Description:
+ Determines how metadata will be set. Valid values are `directory`,
+ `extended` and `legacy`. In extended mode jobs will decide if a
+ tool run failed, the object stores configuration is serialized and
+ made available to the job and is used for writing output datasets
+ to the object store as part of the job and dynamic output
+ discovery (e.g. discovered datasets ,
+ unpopulated collections, etc) happens as part of the job.
+:Default: ``directory``
+:Type: str
+
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``retry_metadata_internally``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/doc/source/dev/index.rst b/doc/source/dev/index.rst
index 0a459a371de..dfc9f93430a 100644
--- a/doc/source/dev/index.rst
+++ b/doc/source/dev/index.rst
@@ -13,3 +13,4 @@ Development Documentation
faq
debugging_tests
debugging_galaxy
+ starting_galaxy
diff --git a/doc/source/dev/starting_galaxy.md b/doc/source/dev/starting_galaxy.md
new file mode 100644
index 00000000000..2f8c483f45e
--- /dev/null
+++ b/doc/source/dev/starting_galaxy.md
@@ -0,0 +1,95 @@
+# Starting Galaxy
+
+## What happens when you start Galaxy?
+
+Galaxy is started by running ./run.sh script. The following call graph documents what happens during Galaxy startup process. Please note, Galaxy is a WSGI app, so in principle all Python WSGI servers can run the Galaxy app (Paste, Gunicorn and uWSGI are known to work and have some level of support). This document separates web server specific section (e.g. Paste) from Galaxy specific section (FYI, Galaxy uses uWSGI by default).
+
+## Paste
+
+* ./galaxy/scripts/paster.py
+ * Add ./galaxy/lib to system path
+ * Check Python version
+ * version >= 3.5 -> OK
+ * version = 2.7 -> Deprecated
+ * No other version is supported
+ * serve.run()
+
+* ./galaxy/lib/galaxy/util/pastescript/serve.py (The Python Paste package contains Python modules that help in implementing WSGI middleware)
+ * serve.run()
+ * Parse Python command line arguments
+ * Extract the command name from command line arguments (e.g., 'serve')
+ * Get the Command object for command name, then invoke it (call Command's run() method)
+ * Command.run()
+ * Setup default options, validate command line arguments, then call ServeCommand's command() method
+ * ServeCommand.command()
+ * Conditionally, stop the daemon
+ * If a config file is required, make sure a config file is provided on the command line arguments.
+ * Possible subcommands: start, stop, restart, status (or None)
+ * Call loadserver() method in ./galaxy/lib/galaxy/util/pastescript/loadwsgi.py
+ * Calls loadobj() -> calls LoaderContext's create() -> calls _Server's invoke()
+ * Call loadapp() method in ./galaxy/lib/galaxy/util/pastescript/loadwsgi.py
+ * Calls loadobj() -> calls LoaderContext's create() -> calls _App's invoke() -> calls fix_call() on app_factory (in ./galaxy/lib/galaxy/webapps/galaxy/buildapp.py)
+
+## Galaxy
+
+* app_factory:
+ * Create UniverseApplication (in ./galaxy/lib/galaxy/webapps/galaxy/buildapp.py)
+ * Create GalaxyAppConfiguration instance from config file specified on the command line, then check for errors
+ * Configure logging (in ./galaxy/lib/galaxy/config/__init__.py)
+ * Create ExecutionTimerFactory
+ * Which has a GalaxyStatsdClient field
+ * Configure FluentTraceLogger
+ * Create the application stack (uWSGi, Paste, or Webless)
+ * App registers post fork functions to start application stack threads
+ * Create GalaxyQueueWorker instance (a worker for Galaxy's queues, used for dispatching control tasks. Sqlite instance)
+ * Configure tool shed registry
+ * Configure object store. build_object_store_from_config() (in ./galaxy/lib/galaxy/objectstore/__init__.py) calls objectstore_class.from_xml(), calls clazz.parse_xml()
+ * Configure models in _configure_models()
+ * Calls create_or_verify_database() in ./galaxy/libl/galaxy/model/migrate/check.py
+ * If a new database, calls migrate_from_scratch() in ./galaxy/libl/galaxy/model/migrate/check.py
+ * Calls mapping.init() in ./galaxy/libl/galaxy/model/mapping.py (Details of how data objects are mapped to relational DB tables).
+ * Calls init_models_from_config in in ./galaxy/libl/galaxy/model/mapping.py
+ * Configure security
+ * Set GalaxyTagHandler (in ./galaxy/lib/galaxy/model/tags.py)
+ * Handles CRUD operations for tagging datasets, workflows, visualizations, etc
+ * Set DatasetCollectionManager (in ./galaxy/lib/galaxy/managers/collections.py)
+ * Configure HistoryManager
+ * Configure HistoryDatasetAssociation (HDA) Manager
+ * Create WorkflowsManager
+ * Handles CRUD operations related to workflows
+ * Configure DependencyResolverView (a RESTful interface to galaxy.tool_util.deps.DependencyResolver)
+ * Configure TestDataResolver
+ * Configure LibraryManager (Data libraries under Shared Data)
+ * Configure FolderManager (Folders are folders in a data library)
+ * Configure DynamicToolManager (Tools that are loaded from DB instead of disk files)
+ * Configure DataProviderRegistry
+ * Configure JobMetrics manager
+ * Configure ErrorReport plugins
+ * Configure job management configuration
+ * Setup a tool cache
+ * Watch various config files for immediate reload
+ * Configure Data Manager
+ * Configure UpdateRespositoyManager
+ * Load proprietary datatype convertors and display applications
+ * Configure VisualizationRegistry, ToursRegistry, and WebHookRegistry
+ * Load security policy and quota management
+ * Configure heartbeat for thread profiling
+ * If configured, create Sentry client
+ * App registers post fork functions to start Sentry client thread
+ * Create JobManager
+ * App registers post fork functions to start application job manager thread
+ * Job manager thread starts monitor threads for JobHandlerQueue and JobHandlerStopQueue (2 threads)
+ * JobManager has a JobHandler field, which manages the preparation, running, tracking, and finishing of jobs
+ * JobHandler has a job dispatcher, which launches underlying job runners
+ * DefaultJobDispatcher calls app's JobConfiguration's get_job_runner_plugins() to create LocalJobRunner instances
+ * The default number of LocalJobRuner instances is 4 (4 threads)
+ * JobHandler also has queues for starting and stopping jobs (unless track_jobs_in_database is True)
+ * Create WorkflowSchedulingManager
+ * Starts workflow request monitor thread in __start_request_monitor() method (1 thread)
+ * Configure InteractiveToolManager
+ * Create database heartbeat instance
+ * App registers post fork functions to start application DB heartbeat thread (1 thread)
+ * Create CommunityWebApplication (in ./galaxy/lib/galaxy/webapps/galaxy/buildapp.py)
+ * CommunityWebApplication: A WSGI application that maps requests to objects using routes and to methods on those objects
+ * Map all requests to controllers/actions
+ * Wrap web app in some middleware and return webapp
diff --git a/doc/source/releases/20.05.rst b/doc/source/releases/20.05.rst
index 9a2031412d6..226acc2097e 100644
--- a/doc/source/releases/20.05.rst
+++ b/doc/source/releases/20.05.rst
@@ -62,7 +62,7 @@ Enhancements
(thanks to `@OlegZharkov `__).
`Pull Request 9562`_
* Datasource tool for SRA manifest
- `Pull Request 9613`_
+ `Pull Request 9613`_, `Pull Request 9892`_
.. enhancement_tag_viz
* Skip visualization build, if no changes were registered
@@ -175,7 +175,7 @@ Enhancements
`Pull Request 9186`_
* Full Python dependencies update
(thanks to `@nsoranzo `__).
- `Pull Request 9188`_
+ `Pull Request 9188`_, `Pull Request 9402`_, `Pull Request 9529`_, `Pull Request 9697`_
* Allow mixed store_by objectstore configurations.
`Pull Request 9197`_
* implement workflow bookmark selenium test
@@ -217,9 +217,6 @@ Enhancements
`Pull Request 9390`_
* Replace editor grid image with css, add editor client unit tests
`Pull Request 9392`_
-* Full Python dependencies update
- (thanks to `@nsoranzo `__).
- `Pull Request 9402`_
* Vuefy interactive tools
(thanks to `@OlegZharkov `__).
`Pull Request 9409`_
@@ -254,9 +251,6 @@ Enhancements
`Pull Request 9521`_
* Rename and modify current_history_contents method
`Pull Request 9526`_
-* Full Python dependencies update
- (thanks to `@nsoranzo `__).
- `Pull Request 9529`_
* Improvement to the selenium test to change password from user preferences
(thanks to `@Realjuan `__).
`Pull Request 9545`_
@@ -306,14 +300,11 @@ Enhancements
* Feature/3937 advanced search tooltip
(thanks to `@assuntad23 `__).
`Pull Request 9696`_
-* Full Python dependencies update
- (thanks to `@nsoranzo `__).
- `Pull Request 9697`_
.. small_enhancement
* Rename function, params in ConfigSerializer
`Pull Request 9247`_
-* Make `str.join()`s more efficient
+* Make ``str.join()``s more efficient
(thanks to `@nsoranzo `__).
`Pull Request 9258`_
* Faster and cleaner set creation
@@ -338,7 +329,7 @@ Enhancements
`Pull Request 9443`_
* Move sanitize_whitelist into mutable_config_dir
`Pull Request 9455`_
-* Use auto_create functionality of `get_private_user_role()`
+* Use auto_create functionality of ``get_private_user_role()``
(thanks to `@nsoranzo `__).
`Pull Request 9509`_
* Combine selenium tests for creating and switching histories
@@ -401,6 +392,9 @@ Fixes
* extract_genomic_dna.py: import Comment, Header from bx.tabular.io instead of bx.intervals.io
(thanks to `@cat-bro `__).
`Pull Request 9748`_
+* Move some tools that no longer need galaxy on the PATH
+ (thanks to `@bernt-matthias `__).
+ `Pull Request 9850`_
.. bug_tag_workflows
* Fix workflow list filtering dropdown
@@ -411,6 +405,8 @@ Fixes
`Pull Request 9676`_
* Trigger onchange event, fixes tool version changes being lost
`Pull Request 9695`_
+* Input dataset module format filtering fix;
+ `Pull Request 9884`_
.. bug_tag_ui
* fix for python3 and GEDA, was getting "Connection closed at byte 0"
@@ -451,6 +447,8 @@ Fixes
* Migrate condor runner to jwd for job-files
(thanks to `@bgruening `__).
`Pull Request 9833`_
+* Fix incorrect variable assignment
+ `Pull Request 9907`_
.. bug_tag_admin
* Fix vue helper in admin router, fixes toolshed installation, avoids console errors
@@ -514,7 +512,7 @@ Fixes
* Use a Galaxy root path with a space for converters and mulled tests
(thanks to `@nsoranzo `__).
`Pull Request 9429`_
-* Fix `test_run_simple()` test broken on Python 2 by virtualenv 20.0.5
+* Fix ``test_run_simple()`` test broken on Python 2 by virtualenv 20.0.5
(thanks to `@nsoranzo `__).
`Pull Request 9458`_
* Use browser locale over global default
@@ -575,8 +573,8 @@ Fixes
* Fix indexing script
`Pull Request 9565`_
* Assign random password in user manager if none specified
- (thanks to `@nuwang `__).
- `Pull Request 9587`_
+ (thanks to `@nuwang `__, `@nsoranzo `__).
+ `Pull Request 9587`_, `Pull Request 9851`_
* Fix removal of workflow outputs from workflow nodes in editor
`Pull Request 9606`_
* Fix ELIXIR AAI
@@ -587,7 +585,7 @@ Fixes
`Pull Request 9622`_
* Change session timeout redirect URL to login entrypoint.
`Pull Request 9626`_
-* Changed CSS overflow property from scroll to hidden to make table scr…
+* Changed CSS overflow property from scroll to hidden to make table scrollable
(thanks to `@assuntad23 `__).
`Pull Request 9627`_
* fix links in Galaxy xsd docs
@@ -601,7 +599,7 @@ Fixes
* Hide "python: not found" error when Python 2 is not installed
(thanks to `@nsoranzo `__).
`Pull Request 9657`_
-* Fix for `time.clock()` removal in Python 3.8
+* Fix for ``time.clock()`` removal in Python 3.8
(thanks to `@nsoranzo `__).
`Pull Request 9666`_
* Fixed an issue in object store integration tests
@@ -622,6 +620,8 @@ Fixes
`Pull Request 9724`_
* Fix custos credential fetching in python3
`Pull Request 9726`_
+* oidc-based authnz provider overhaul; breaking changes for keycloak
+ `Pull Request 9730`_
* Sort Workflow list by last updated time instead of created time
`Pull Request 9740`_
* Remove dummy tool sequence
@@ -648,6 +648,31 @@ Fixes
`Pull Request 9821`_
* Tolerate exception in container resolution
`Pull Request 9824`_
+* Invalidate tool cache when tool modified outside of Galaxy
+ `Pull Request 9866`_
+* Custos spec fixes
+ `Pull Request 9889`_
+* Fixes for flake8 3.8 and bugbear
+ (thanks to `@nsoranzo `__).
+ `Pull Request 9896`_
+* Fix webpack import proxy prefix
+ `Pull Request 9898`_
+* Fix extra preference values leaking between requests
+ (thanks to `@nuwang `__).
+ `Pull Request 9913`_
+* Rewrite ``check_html()`` and ``Text.count_data_lines()`` to read a chunk of each line
+ (thanks to `@nsoranzo `__).
+ `Pull Request 9920`_
+* Make cookie handling more robust
+ `Pull Request 9921`_
+* Enable regex validator to validate lists of values.
+ `Pull Request 9925`_
+* oidc: fix dict_keys does not support indexing
+ (thanks to `@almahmoud `__).
+ `Pull Request 9931`_
+* Move the ``--debug`` flag after the conda subcommand
+ (thanks to `@nsoranzo `__).
+ `Pull Request 9932`_
.. include:: 20.05_prs.rst
diff --git a/doc/source/releases/20.05_prs.rst b/doc/source/releases/20.05_prs.rst
index da0105d63ed..eee1092d8cc 100644
--- a/doc/source/releases/20.05_prs.rst
+++ b/doc/source/releases/20.05_prs.rst
@@ -226,6 +226,7 @@
.. _Pull Request 9719: https://github.com/galaxyproject/galaxy/pull/9719
.. _Pull Request 9724: https://github.com/galaxyproject/galaxy/pull/9724
.. _Pull Request 9726: https://github.com/galaxyproject/galaxy/pull/9726
+.. _Pull Request 9730: https://github.com/galaxyproject/galaxy/pull/9730
.. _Pull Request 9731: https://github.com/galaxyproject/galaxy/pull/9731
.. _Pull Request 9733: https://github.com/galaxyproject/galaxy/pull/9733
.. _Pull Request 9734: https://github.com/galaxyproject/galaxy/pull/9734
@@ -250,3 +251,22 @@
.. _Pull Request 9824: https://github.com/galaxyproject/galaxy/pull/9824
.. _Pull Request 9833: https://github.com/galaxyproject/galaxy/pull/9833
.. _Pull Request 9835: https://github.com/galaxyproject/galaxy/pull/9835
+.. _Pull Request 9838: https://github.com/galaxyproject/galaxy/pull/9838
+.. _Pull Request 9850: https://github.com/galaxyproject/galaxy/pull/9850
+.. _Pull Request 9851: https://github.com/galaxyproject/galaxy/pull/9851
+.. _Pull Request 9866: https://github.com/galaxyproject/galaxy/pull/9866
+.. _Pull Request 9870: https://github.com/galaxyproject/galaxy/pull/9870
+.. _Pull Request 9878: https://github.com/galaxyproject/galaxy/pull/9878
+.. _Pull Request 9884: https://github.com/galaxyproject/galaxy/pull/9884
+.. _Pull Request 9889: https://github.com/galaxyproject/galaxy/pull/9889
+.. _Pull Request 9892: https://github.com/galaxyproject/galaxy/pull/9892
+.. _Pull Request 9894: https://github.com/galaxyproject/galaxy/pull/9894
+.. _Pull Request 9896: https://github.com/galaxyproject/galaxy/pull/9896
+.. _Pull Request 9898: https://github.com/galaxyproject/galaxy/pull/9898
+.. _Pull Request 9907: https://github.com/galaxyproject/galaxy/pull/9907
+.. _Pull Request 9913: https://github.com/galaxyproject/galaxy/pull/9913
+.. _Pull Request 9920: https://github.com/galaxyproject/galaxy/pull/9920
+.. _Pull Request 9921: https://github.com/galaxyproject/galaxy/pull/9921
+.. _Pull Request 9925: https://github.com/galaxyproject/galaxy/pull/9925
+.. _Pull Request 9931: https://github.com/galaxyproject/galaxy/pull/9931
+.. _Pull Request 9932: https://github.com/galaxyproject/galaxy/pull/9932
diff --git a/lib/galaxy/authnz/custos_authnz.py b/lib/galaxy/authnz/custos_authnz.py
index 11f6e43a52a..18f907a03f7 100644
--- a/lib/galaxy/authnz/custos_authnz.py
+++ b/lib/galaxy/authnz/custos_authnz.py
@@ -17,8 +17,8 @@ from galaxy.model import CustosAuthnzToken, User
from ..authnz import IdentityProvider
log = logging.getLogger(__name__)
-STATE_COOKIE_NAME = 'custos-state'
-NONCE_COOKIE_NAME = 'custos-nonce'
+STATE_COOKIE_NAME = 'galaxy-oidc-state'
+NONCE_COOKIE_NAME = 'galaxy-oidc-nonce'
KEYCLOAK_BACKENDS = {'custos', 'cilogon', 'keycloak'}
diff --git a/lib/galaxy/authnz/managers.py b/lib/galaxy/authnz/managers.py
index be52449ec4e..e994605f08c 100644
--- a/lib/galaxy/authnz/managers.py
+++ b/lib/galaxy/authnz/managers.py
@@ -240,7 +240,7 @@ class AuthnzManager:
ID does not exist, or the configuration is defined for a another
user than trans.user.
- :type trans: galaxy.web.framework.webapp.GalaxyWebTransaction
+ :type trans: galaxy.webapps.base.webapp.GalaxyWebTransaction
:param trans: Galaxy web transaction
:type authz_id: int
@@ -292,7 +292,7 @@ class AuthnzManager:
raise exceptions.AuthenticationFailed(e.message)
except Exception as e:
msg = 'The following error occurred when handling callback from `{}` identity provider: ' \
- '{}'.format(provider, e.message)
+ '{}'.format(provider, str(e))
log.exception(msg)
return False, msg, (None, None)
@@ -321,7 +321,7 @@ class AuthnzManager:
return True, message, backend.logout(trans, post_logout_redirect_url)
except Exception as e:
msg = 'The following error occurred when logging out from `{}` identity provider: ' \
- '{}'.format(provider, e.message)
+ '{}'.format(provider, str(e))
log.exception(msg)
return False, msg, None
diff --git a/lib/galaxy/config/__init__.py b/lib/galaxy/config/__init__.py
index accfb7ac66c..7182026b332 100644
--- a/lib/galaxy/config/__init__.py
+++ b/lib/galaxy/config/__init__.py
@@ -1029,7 +1029,7 @@ class ConfiguresGalaxyMixin:
self._toolbox_lock = threading.RLock()
self.toolbox = tools.ToolBox(self.config.tool_configs, self.config.tool_path, self)
galaxy_root_dir = os.path.abspath(self.config.root)
- file_path = os.path.abspath(getattr(self.config, "file_path"))
+ file_path = os.path.abspath(self.config.file_path)
app_info = AppInfo(
galaxy_root_dir=galaxy_root_dir,
default_file_path=file_path,
diff --git a/lib/galaxy/config/sample/galaxy.yml.sample b/lib/galaxy/config/sample/galaxy.yml.sample
index d6aa6df4953..5eda281b6aa 100644
--- a/lib/galaxy/config/sample/galaxy.yml.sample
+++ b/lib/galaxy/config/sample/galaxy.yml.sample
@@ -700,9 +700,10 @@ galaxy:
#instance_resource_url: null
# E-mail domains blocklist is used for filtering out users that are
- # using disposable email addresses at registration. If their address
- # domain matches any domain on the list, they are refused
- # registration.
+ # using disposable email addresses at registration. If their
+ # address's base domain matches any domain on the list, they are
+ # refused registration. Address subdomains are ignored (both
+ # 'name@spam.com' and 'name@foo.spam.com' will match 'spam.com').
# Example value 'email_blocklist.conf'
# The value of this option will be resolved with respect to
# .
@@ -711,8 +712,11 @@ galaxy:
# E-mail domains allowlist is used to specify allowed email address
# domains. If the list is non-empty and a user attempts registration
# using an email address belonging to a domain that is not on the
- # list, registration will be denied. This is a more restrictive option
- # than , and therefore, in case
+ # list, registration will be denied. Unlike
+ # which matches the address's base
+ # domain, here email addresses are matched against the full domain
+ # (base + subdomain). This is a more restrictive option than
+ # , and therefore, in case
# is set and is not empty,
# will be ignored.
# Example value 'email_allowlist.conf'
@@ -1747,6 +1751,15 @@ galaxy:
# database.
#enable_job_recovery: true
+ # Determines how metadata will be set. Valid values are `directory`,
+ # `extended` and `legacy`. In extended mode jobs will decide if a tool
+ # run failed, the object stores configuration is serialized and made
+ # available to the job and is used for writing output datasets to the
+ # object store as part of the job and dynamic output discovery (e.g.
+ # discovered datasets , unpopulated collections,
+ # etc) happens as part of the job.
+ #metadata_strategy: directory
+
# Although it is fairly reliable, setting metadata can occasionally
# fail. In these instances, you can choose to retry setting it
# internally or leave it in a failed state (since retrying internally
diff --git a/lib/galaxy/config/sample/job_conf.xml.sample_advanced b/lib/galaxy/config/sample/job_conf.xml.sample_advanced
index 53254618eba..beb5feb3d13 100644
--- a/lib/galaxy/config/sample/job_conf.xml.sample_advanced
+++ b/lib/galaxy/config/sample/job_conf.xml.sample_advanced
@@ -401,6 +401,17 @@
try it early - it will slightly speed up local jobs by
embedding metadata calculation in job script itself.
-->
+ directory
+
@@ -194,7 +214,7 @@ class ModelPersistenceContext(metaclass=abc.ABCMeta):
if name is None:
name = "unnamed output"
- element_datasets = []
+ element_datasets = {'element_identifiers': [], 'datasets': [], 'tag_lists': [], 'paths': []}
for filename, discovered_file in filenames.items():
create_dataset_timer = ExecutionTimer()
fields_match = discovered_file.match
@@ -213,7 +233,6 @@ class ModelPersistenceContext(metaclass=abc.ABCMeta):
dataset_name = fields_match.name or designation
link_data = discovered_file.match.link_data
- tag_list = discovered_file.match.tag_list
sources = discovered_file.match.sources
hashes = discovered_file.match.hashes
@@ -225,10 +244,8 @@ class ModelPersistenceContext(metaclass=abc.ABCMeta):
visible=visible,
dbkey=dbkey,
name=dataset_name,
- filename=filename,
metadata_source_name=metadata_source_name,
link_data=link_data,
- tag_list=tag_list,
sources=sources,
hashes=hashes,
created_from_basename=created_from_basename,
@@ -242,18 +259,13 @@ class ModelPersistenceContext(metaclass=abc.ABCMeta):
name,
create_dataset_timer,
)
- element_datasets.append((element_identifiers, dataset))
+ element_datasets['element_identifiers'].append(element_identifiers)
+ element_datasets['datasets'].append(dataset)
+ element_datasets['tag_lists'].append(discovered_file.match.tag_list)
+ element_datasets['paths'].append(filename)
- add_datasets_timer = ExecutionTimer()
- self.add_datasets_to_history([d for (ei, d) in element_datasets])
- log.debug(
- "(%s) Add dynamic collection datasets to history for output [%s] %s",
- self.job_id(),
- name,
- add_datasets_timer,
- )
-
- for (element_identifiers, dataset) in element_datasets:
+ self.add_tags_to_datasets(datasets=element_datasets['datasets'], tag_lists=element_datasets['tag_lists'])
+ for (element_identifiers, dataset) in zip(element_datasets['element_identifiers'], element_datasets['datasets']):
current_builder = root_collection_builder
for element_identifier in element_identifiers[:-1]:
current_builder = current_builder.get_level(element_identifier)
@@ -265,6 +277,31 @@ class ModelPersistenceContext(metaclass=abc.ABCMeta):
self.add_output_dataset_association(association_name, dataset)
self.flush()
+ self.update_object_store_with_datasets(datasets=element_datasets['datasets'], paths=element_datasets['paths'])
+ add_datasets_timer = ExecutionTimer()
+ self.add_datasets_to_history(element_datasets['datasets'])
+ log.debug(
+ "(%s) Add dynamic collection datasets to history for output [%s] %s",
+ self.job_id(),
+ name,
+ add_datasets_timer,
+ )
+ self.set_datasets_metadata(datasets=element_datasets['datasets'])
+
+ def add_tags_to_datasets(self, datasets, tag_lists):
+ if any(tag_lists):
+ # This works around SessionlessModelPersistenceContext not implementing a tag handler ...
+ # that's not better or worse than what we previously did in create_datasets
+ # TDOD: implement that or figure out why it is not implemented and find a better solution.
+ # Could it be that SessionlessModelPersistenceContext doesn't support tags?
+ tag_session = self.tag_handler.create_tag_handler_session()
+ for dataset, tags in zip(datasets, tag_lists):
+ tag_session.add_tags_from_list(self.job.user, dataset, tags, flush=False)
+
+ def update_object_store_with_datasets(self, datasets, paths):
+ for dataset, path in zip(datasets, paths):
+ self.object_store.update_from_file(dataset.dataset, file_name=path, create=True)
+ dataset.set_size(no_extra_files=True)
@abc.abstractproperty
def tag_handler(self):
diff --git a/lib/galaxy/model/tags.py b/lib/galaxy/model/tags.py
index 1d23d3a6580..627c27c25a2 100644
--- a/lib/galaxy/model/tags.py
+++ b/lib/galaxy/model/tags.py
@@ -41,26 +41,31 @@ class TagHandler:
# Initialize with known classes - add to this in subclasses.
self.item_tag_assoc_info = {}
- def add_tags_from_list(self, user, item, new_tags_list):
+ def create_tag_handler_session(self):
+ # Creates a transient tag handler that avoids repeated flushes
+ return GalaxyTagHandlerSession(self.sa_session)
+
+ def add_tags_from_list(self, user, item, new_tags_list, flush=True):
new_tags_set = set(new_tags_list)
if item.tags:
new_tags_set.update(self.get_tags_str(item.tags).split(','))
- return self.set_tags_from_list(user, item, new_tags_set)
+ return self.set_tags_from_list(user, item, new_tags_set, flush=flush)
- def remove_tags_from_list(self, user, item, tag_to_remove_list):
+ def remove_tags_from_list(self, user, item, tag_to_remove_list, flush=True):
tag_to_remove_set = set(tag_to_remove_list)
tags_set = {_.strip() for _ in self.get_tags_str(item.tags).split(',')}
if item.tags:
tags_set -= tag_to_remove_set
- return self.set_tags_from_list(user, item, tags_set)
+ return self.set_tags_from_list(user, item, tags_set, flush=flush)
- def set_tags_from_list(self, user, item, new_tags_list):
+ def set_tags_from_list(self, user, item, new_tags_list, flush=True):
# precondition: item is already security checked against user
# precondition: incoming tags is a list of sanitized/formatted strings
self.delete_item_tags(user, item)
new_tags_str = ','.join(new_tags_list)
- self.apply_item_tags(user, item, unicodify(new_tags_str, 'utf-8'))
- self.sa_session.flush()
+ self.apply_item_tags(user, item, unicodify(new_tags_str, 'utf-8'), flush=flush)
+ if flush:
+ self.sa_session.flush()
return item.tags
def get_tag_assoc_class(self, item_class):
@@ -126,7 +131,9 @@ class TagHandler:
"""Delete tags from an item."""
# Delete item-tag associations.
for tag in item.tags:
- self.sa_session.delete(tag)
+ if tag.id:
+ # Only can and need to delete tag if tag is persisted
+ self.sa_session.delete(tag)
# Delete tags from item.
del item.tags[:]
@@ -145,7 +152,7 @@ class TagHandler:
return True
return False
- def apply_item_tag(self, user, item, name, value=None):
+ def apply_item_tag(self, user, item, name, value=None, flush=True):
# Use lowercase name for searching/creating tag.
lc_name = name.lower()
# Get or create item-tag association.
@@ -173,17 +180,17 @@ class TagHandler:
item_tag_assoc.user_tname = name
item_tag_assoc.user_value = value
item_tag_assoc.value = lc_value
- # Need to flush to get an ID. We need an ID to apply multiple tags with the same tname to an object.
- self.sa_session.flush()
+ if flush:
+ self.sa_session.flush()
return item_tag_assoc
- def apply_item_tags(self, user, item, tags_str):
+ def apply_item_tags(self, user, item, tags_str, flush=True):
"""Apply tags to an item."""
# Parse tags.
parsed_tags = self.parse_tags(tags_str)
# Apply each tag.
for name, value in parsed_tags:
- self.apply_item_tag(user, item, name, value)
+ self.apply_item_tag(user, item, name, value, flush=flush)
def get_tags_str(self, tags):
"""Build a string from an item's tags."""
@@ -217,9 +224,9 @@ class TagHandler:
for sub_tag in tag_hierarchy:
# Get or create subtag.
tag_name = tag_prefix + self._scrub_tag_name(sub_tag)
- tag = self.sa_session.query(galaxy.model.Tag).filter_by(name=tag_name).first()
+ tag = self._get_tag(tag_name)
if not tag:
- tag = galaxy.model.Tag(type=0, name=tag_name)
+ tag = self._create_tag_instance(tag_name)
# Set tag parent.
tag.parent = parent_tag
# Update parent and tag prefix.
@@ -227,6 +234,12 @@ class TagHandler:
tag_prefix = tag.name + self.hierarchy_separator
return tag
+ def _get_tag(self, tag_name):
+ return self.sa_session.query(galaxy.model.Tag).filter_by(name=tag_name).first()
+
+ def _create_tag_instance(self, tag_name):
+ return galaxy.model.Tag(type=0, name=tag_name)
+
def _get_or_create_tag(self, tag_str):
"""Get or create a Tag object from a tag string."""
# Scrub tag; if tag is None after being scrubbed, return None.
@@ -364,6 +377,24 @@ class GalaxyTagHandler(TagHandler):
model.VisualizationTagAssociation.table.c.visualization_id)
+class GalaxyTagHandlerSession(GalaxyTagHandler):
+ """Like GalaxyTagHandler, but avoids one flush per created tag."""
+ def __init__(self, sa_session):
+ super().__init__(sa_session)
+ self.created_tags = {}
+
+ def _get_tag(self, tag_name):
+ """Get tag from cache or database."""
+ # Avoids creating multiple new tags with the same tag_name, which violates unique key constraint
+ return self.created_tags.get(tag_name) or super(GalaxyTagHandler, self)._get_tag(tag_name)
+
+ def _create_tag_instance(self, tag_name):
+ """Create tag and and store in cache."""
+ tag = super()._create_tag_instance(tag_name)
+ self.created_tags[tag_name] = tag
+ return tag
+
+
class CommunityTagHandler(TagHandler):
def __init__(self, sa_session):
TagHandler.__init__(self, sa_session)
diff --git a/lib/galaxy/objectstore/irods.py b/lib/galaxy/objectstore/irods.py
index 4a1d16cd464..6c34673a580 100644
--- a/lib/galaxy/objectstore/irods.py
+++ b/lib/galaxy/objectstore/irods.py
@@ -4,6 +4,7 @@ Object Store plugin for the Integrated Rule-Oriented Data Store (iRODS)
import logging
import os
import shutil
+from contextlib import contextmanager
from datetime import datetime
from functools import partial
try:
@@ -110,7 +111,32 @@ def parse_config_xml(config_xml):
raise
-class CloudConfigMixin:
+def acquire_session(host='localhost', port='1247', user='rods', password='rods', zone='tempZone', timeout='30'):
+ session = iRODSSession(host=host, port=port, user=user, password=password, zone=zone)
+ # Set connection timeout
+ session.connection_timeout = timeout
+ return session
+
+
+def release_session(session):
+ # This call will cleanup all the connections in the connection pool
+ # OSError sometimes happens on GitHub Actions, after the test has successfully completed. Ignore it if it happens.
+ try:
+ session.cleanup()
+ except OSError:
+ pass
+
+
+@contextmanager
+def managed_session(host='localhost', port='1247', user='rods', password='rods', zone='tempZone', timeout='30'):
+ session = acquire_session(host=host, port=port, user=user, password=password, zone=zone, timeout=timeout)
+ try:
+ yield session
+ finally:
+ release_session(session)
+
+
+class CloudConfigMixin(object):
def _config_to_dict(self):
return {
@@ -205,48 +231,11 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin):
_config_dict_error('extra_dirs')
self.extra_dirs.update(extra_dirs)
- self._initialize()
- log.debug("irods __init__ %s", reload_timer)
-
- def shutdown(self):
- # This call will cleanup all the connections in the connection pool
- log.debug("In __shutdown__ Number of active connections: %s, Number of idle connections: %s", len(self.session.pool.active), len(self.session.pool.idle))
- # OSError sometimes happens on GitHub Actions, after the test has successfully completed. Ignore it if it happens.
- try:
- self.session.cleanup()
- except OSError:
- pass
-
- def _initialize(self):
- reload_timer = ExecutionTimer()
if irods is None:
raise Exception(IRODS_IMPORT_MESSAGE)
self.home = "/" + self.zone + "/home/" + self.username
-
- self.session = self._configure_connection(host=self.host, port=self.port, user=self.username, password=self.password, zone=self.zone)
- log.debug("irods _initialize %s", reload_timer)
-
- def _configure_connection(self, host='localhost', port='1247', user='rods', password='rods', zone='tempZone'):
- self.connections = []
- reload_timer_1 = ExecutionTimer()
- reload_timer_2 = ExecutionTimer()
- session = iRODSSession(host=host, port=port, user=user, password=password, zone=zone)
- log.debug("irods _configure_connection, iRODSSession call %s", reload_timer_2)
- # Set connection timeout
- session.connection_timeout = self.timeout
- # Throws NetworkException if connection fails
- try:
- # We will create as many conections as poolsize.
- for idx in range(self.poolsize):
- reload_timer_2 = ExecutionTimer()
- self.connections.append(session.pool.get_connection())
- log.debug("irods _configure_connection, session.pool.get_connection() %s", reload_timer_2)
- except NetworkException as e:
- log.error('Could not create iRODS session: ' + str(e))
- raise
- log.debug("irods _configure_connection %s", reload_timer_1)
- return session
+ log.debug("irods __init__ %s", reload_timer)
@classmethod
def parse_xml(cls, config_xml):
@@ -306,40 +295,43 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin):
# rel_path is file or folder?
def _get_size_in_irods(self, rel_path):
- p = Path(rel_path)
- data_object_name = p.stem + p.suffix
- subcollection_name = p.parent
+ with managed_session(host=self.host, port=self.port, user=self.username, password=self.password, zone=self.zone, timeout=self.timeout) as session:
+ p = Path(rel_path)
+ data_object_name = p.stem + p.suffix
+ subcollection_name = p.parent
- collection_path = self.home + "/" + str(subcollection_name)
- data_object_path = collection_path + "/" + str(data_object_name)
+ collection_path = self.home + "/" + str(subcollection_name)
+ data_object_path = collection_path + "/" + str(data_object_name)
- log.debug("In _get_size_in_irods(): Number of active connections: %s, Number of idle connections: %s", len(self.session.pool.active), len(self.session.pool.idle))
- try:
- data_obj = self.session.data_objects.get(data_object_path)
- return data_obj.__sizeof__()
- except (DataObjectDoesNotExist, CollectionDoesNotExist):
- log.warn("Collection or data object (%s) does not exist", data_object_path)
- return -1
+ try:
+ data_obj = session.data_objects.get(data_object_path)
+ return data_obj.__sizeof__()
+ except (DataObjectDoesNotExist, CollectionDoesNotExist):
+ log.warn("Collection or data object (%s) does not exist", data_object_path)
+ return -1
+ except NetworkException as e:
+ log.exception(e)
+ return -1
# rel_path is file or folder?
def _data_object_exists(self, rel_path):
- p = Path(rel_path)
- data_object_name = p.stem + p.suffix
- subcollection_name = p.parent
+ with managed_session(host=self.host, port=self.port, user=self.username, password=self.password, zone=self.zone, timeout=self.timeout) as session:
+ p = Path(rel_path)
+ data_object_name = p.stem + p.suffix
+ subcollection_name = p.parent
- collection_path = self.home + "/" + str(subcollection_name)
- data_object_path = collection_path + "/" + str(data_object_name)
+ collection_path = self.home + "/" + str(subcollection_name)
+ data_object_path = collection_path + "/" + str(data_object_name)
- log.debug("In _data_object_exists(): Number of active connections: %s, Number of idle connections: %s", len(self.session.pool.active), len(self.session.pool.idle))
- try:
- self.session.data_objects.get(data_object_path)
- return True
- except (DataObjectDoesNotExist, CollectionDoesNotExist):
- log.debug("Collection or data object (%s) does not exist", data_object_path)
- return False
- except Exception as e:
- log.exception(e)
- return False
+ try:
+ session.data_objects.get(data_object_path)
+ return True
+ except (DataObjectDoesNotExist, CollectionDoesNotExist):
+ log.debug("Collection or data object (%s) does not exist", data_object_path)
+ return False
+ except NetworkException as e:
+ log.exception(e)
+ return False
def _in_cache(self, rel_path):
""" Check if the given dataset is in the local cache and return True if so. """
@@ -357,34 +349,37 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin):
return file_ok
def _download(self, rel_path):
- log.debug("Pulling data object '%s' into cache to %s", rel_path, self._get_cache_path(rel_path))
+ with managed_session(host=self.host, port=self.port, user=self.username, password=self.password, zone=self.zone, timeout=self.timeout) as session:
+ log.debug("Pulling data object '%s' into cache to %s", rel_path, self._get_cache_path(rel_path))
- p = Path(rel_path)
- data_object_name = p.stem + p.suffix
- subcollection_name = p.parent
+ p = Path(rel_path)
+ data_object_name = p.stem + p.suffix
+ subcollection_name = p.parent
- collection_path = self.home + "/" + str(subcollection_name)
- data_object_path = collection_path + "/" + str(data_object_name)
- data_obj = None
+ collection_path = self.home + "/" + str(subcollection_name)
+ data_object_path = collection_path + "/" + str(data_object_name)
+ data_obj = None
- log.debug("In _download(): Number of active connections: %s, Number of idle connections: %s", len(self.session.pool.active), len(self.session.pool.idle))
- try:
- data_obj = self.session.data_objects.get(data_object_path)
- except (DataObjectDoesNotExist, CollectionDoesNotExist):
- log.warn("Collection or data object (%s) does not exist", data_object_path)
- return False
+ try:
+ data_obj = session.data_objects.get(data_object_path)
+ except (DataObjectDoesNotExist, CollectionDoesNotExist):
+ log.warn("Collection or data object (%s) does not exist", data_object_path)
+ return False
+ except NetworkException as e:
+ log.exception(e)
+ return False
- if self.cache_size > 0 and data_obj.__sizeof__() > self.cache_size:
- log.critical("File %s is larger (%s) than the cache size (%s). Cannot download.",
- rel_path, data_obj.__sizeof__(), self.cache_size)
- return False
+ if self.cache_size > 0 and data_obj.__sizeof__() > self.cache_size:
+ log.critical("File %s is larger (%s) than the cache size (%s). Cannot download.",
+ rel_path, data_obj.__sizeof__(), self.cache_size)
+ return False
- log.debug("Pulled data object '%s' into cache to %s", rel_path, self._get_cache_path(rel_path))
+ log.debug("Pulled data object '%s' into cache to %s", rel_path, self._get_cache_path(rel_path))
- with data_obj.open('r') as data_obj_fp, open(self._get_cache_path(rel_path), "wb") as cache_fp:
- for chunk in iter(partial(data_obj_fp.read, CHUNK_SIZE), b''):
- cache_fp.write(chunk)
- return True
+ with data_obj.open('r') as data_obj_fp, open(self._get_cache_path(rel_path), "wb") as cache_fp:
+ for chunk in iter(partial(data_obj_fp.read, CHUNK_SIZE), b''):
+ cache_fp.write(chunk)
+ return True
def _push_to_irods(self, rel_path, source_file=None, from_string=None):
"""
@@ -395,52 +390,54 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin):
still using ``rel_path`` for collection and object store names.
If ``from_string`` is provided, set contents of the file to the value of the string.
"""
- p = Path(rel_path)
- data_object_name = p.stem + p.suffix
- subcollection_name = p.parent
+ with managed_session(host=self.host, port=self.port, user=self.username, password=self.password, zone=self.zone, timeout=self.timeout) as session:
+ p = Path(rel_path)
+ data_object_name = p.stem + p.suffix
+ subcollection_name = p.parent
- source_file = source_file if source_file else self._get_cache_path(rel_path)
- options = {kw.FORCE_FLAG_KW: ''}
+ source_file = source_file if source_file else self._get_cache_path(rel_path)
+ options = {kw.FORCE_FLAG_KW: ''}
- log.debug("In _push_to_irods(): Number of active connections: %s, Number of idle connections: %s", len(self.session.pool.active), len(self.session.pool.idle))
- if os.path.exists(source_file):
- # Check if the data object exists in iRODS
- collection_path = self.home + "/" + str(subcollection_name)
- data_object_path = collection_path + "/" + str(data_object_name)
- exists = False
- try:
- exists = self.session.data_objects.exists(data_object_path)
- except NetworkException as e:
- log.exception(e)
- if os.path.getsize(source_file) == 0 and exists:
- log.debug("Wanted to push file '%s' to iRODS collection '%s' but its size is 0; skipping.", source_file, rel_path)
- return True
- if from_string:
- data_obj = self.session.data_objects.create(data_object_path, self.resource, **options)
- with data_obj.open('w') as data_obj_fp:
- data_obj_fp.write(from_string)
- log.debug("Pushed data from string '%s' to collection '%s'", from_string, data_object_path)
- else:
- start_time = datetime.now()
- log.debug("Pushing cache file '%s' of size %s bytes to collection '%s'", source_file, os.path.getsize(source_file), rel_path)
+ if os.path.exists(source_file):
+ # Check if the data object exists in iRODS
+ collection_path = self.home + "/" + str(subcollection_name)
+ data_object_path = collection_path + "/" + str(data_object_name)
+ exists = False
+ try:
+ exists = session.data_objects.exists(data_object_path)
- # Create sub-collection first
- self.session.collections.create(collection_path, recurse=True)
- data_obj = self.session.data_objects.create(data_object_path, self.resource, **options)
+ if os.path.getsize(source_file) == 0 and exists:
+ log.debug("Wanted to push file '%s' to iRODS collection '%s' but its size is 0; skipping.", source_file, rel_path)
+ return True
- # Write to file in subcollection created above
- with open(source_file, 'rb') as content_file, data_obj.open('w') as data_obj_fp:
- for chunk in iter(partial(content_file.read, CHUNK_SIZE), b''):
- data_obj_fp.write(chunk)
+ if from_string:
+ data_obj = session.data_objects.create(data_object_path, self.resource, **options)
+ with data_obj.open('w') as data_obj_fp:
+ data_obj_fp.write(from_string)
+ log.debug("Pushed data from string '%s' to collection '%s'", from_string, data_object_path)
+ else:
+ start_time = datetime.now()
+ log.debug("Pushing cache file '%s' of size %s bytes to collection '%s'", source_file, os.path.getsize(source_file), rel_path)
- end_time = datetime.now()
- log.debug("Pushed cache file '%s' to collection '%s' (%s bytes transfered in %s sec)",
- source_file, rel_path, os.path.getsize(source_file), end_time - start_time)
- return True
- else:
- log.error("Tried updating key '%s' from source file '%s', but source file does not exist.",
- rel_path, source_file)
- return False
+ # Create sub-collection first
+ session.collections.create(collection_path, recurse=True)
+ data_obj = session.data_objects.create(data_object_path, self.resource, **options)
+
+ # Write to file in subcollection created above
+ with open(source_file, 'rb') as content_file, data_obj.open('w') as data_obj_fp:
+ for chunk in iter(partial(content_file.read, CHUNK_SIZE), b''):
+ data_obj_fp.write(chunk)
+
+ end_time = datetime.now()
+ log.debug("Pushed cache file '%s' to collection '%s' (%s bytes transfered in %s sec)",
+ source_file, rel_path, os.path.getsize(source_file), end_time - start_time)
+ return True
+ except NetworkException as e:
+ log.exception(e)
+ return False
+
+ log.error("Tried updating key '%s' from source file '%s', but source file does not exist.", rel_path, source_file)
+ return False
def file_ready(self, obj, **kwargs):
"""
@@ -457,35 +454,21 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin):
return False
def _exists(self, obj, **kwargs):
- in_cache = in_irods = False
rel_path = self._construct_path(obj, **kwargs)
- # Check cache
- if self._in_cache(rel_path):
- in_cache = True
- # Check iRODS
- in_irods = self._data_object_exists(rel_path)
+ # Check cache and irods
+ if self._in_cache(rel_path) or self._data_object_exists(rel_path):
+ return True
# dir_only does not get synced so shortcut the decision
dir_only = kwargs.get('dir_only', False)
base_dir = kwargs.get('base_dir', None)
- if dir_only:
- if in_cache or in_irods:
- return True
+ if dir_only and base_dir:
# for JOB_WORK directory
- elif base_dir:
- if not os.path.exists(rel_path):
- os.makedirs(rel_path)
- return True
- else:
- return False
-
- if in_cache and not in_irods:
+ if not os.path.exists(rel_path):
+ os.makedirs(rel_path)
return True
- elif in_irods:
- return True
- else:
- return False
+ return False
def _create(self, obj, **kwargs):
if not self._exists(obj, **kwargs):
@@ -535,69 +518,76 @@ class IRODSObjectStore(DiskObjectStore, CloudConfigMixin):
return 0
def _delete(self, obj, entire_dir=False, **kwargs):
- rel_path = self._construct_path(obj, **kwargs)
- extra_dir = kwargs.get('extra_dir', None)
- base_dir = kwargs.get('base_dir', None)
- dir_only = kwargs.get('dir_only', False)
- obj_dir = kwargs.get('obj_dir', False)
+ with managed_session(host=self.host, port=self.port, user=self.username, password=self.password, zone=self.zone, timeout=self.timeout) as session:
+ rel_path = self._construct_path(obj, **kwargs)
+ extra_dir = kwargs.get('extra_dir', None)
+ base_dir = kwargs.get('base_dir', None)
+ dir_only = kwargs.get('dir_only', False)
+ obj_dir = kwargs.get('obj_dir', False)
- log.debug("In _delete(): Number of active connections: %s, Number of idle connections: %s", len(self.session.pool.active), len(self.session.pool.idle))
- try:
- # Remove temparory data in JOB_WORK directory
- if base_dir and dir_only and obj_dir:
- shutil.rmtree(os.path.abspath(rel_path))
- return True
-
- # For the case of extra_files, because we don't have a reference to
- # individual files we need to remove the entire directory structure
- # with all the files in it. This is easy for the local file system,
- # but requires iterating through each individual key in irods and deleing it.
- if entire_dir and extra_dir:
- shutil.rmtree(self._get_cache_path(rel_path))
-
- col_path = self.home + "/" + str(rel_path)
- col = None
- try:
- col = self.session.collections.get(col_path)
- except CollectionDoesNotExist:
- log.warn("Collection (%s) does not exist!", col_path)
- return False
-
- cols = col.walk()
- # Traverse the tree only one level deep
- for _ in range(2):
- # get next result
- _, _, data_objects = next(cols)
-
- # Delete data objects
- for data_object in data_objects:
- data_object.unlink(force=True)
-
- return True
-
- else:
- # Delete from cache first
- os.unlink(self._get_cache_path(rel_path))
- # Delete from irods as well
- p = Path(rel_path)
- data_object_name = p.stem + p.suffix
- subcollection_name = p.parent
-
- collection_path = self.home + "/" + str(subcollection_name)
- data_object_path = collection_path + "/" + str(data_object_name)
-
- try:
- data_obj = self.session.data_objects.get(data_object_path)
- # remove object
- data_obj.unlink(force=True)
- return True
- except (DataObjectDoesNotExist, CollectionDoesNotExist):
- log.info("Collection or data object (%s) does not exist", data_object_path)
+ try:
+ # Remove temparory data in JOB_WORK directory
+ if base_dir and dir_only and obj_dir:
+ shutil.rmtree(os.path.abspath(rel_path))
return True
- except OSError:
- log.exception('%s delete error', self._get_filename(obj, **kwargs))
- return False
+ # For the case of extra_files, because we don't have a reference to
+ # individual files we need to remove the entire directory structure
+ # with all the files in it. This is easy for the local file system,
+ # but requires iterating through each individual key in irods and deleing it.
+ if entire_dir and extra_dir:
+ shutil.rmtree(self._get_cache_path(rel_path))
+
+ col_path = self.home + "/" + str(rel_path)
+ col = None
+ try:
+ col = session.collections.get(col_path)
+ except CollectionDoesNotExist:
+ log.warn("Collection (%s) does not exist!", col_path)
+ return False
+ except NetworkException as e:
+ log.exception(e)
+ return False
+
+ cols = col.walk()
+ # Traverse the tree only one level deep
+ for _ in range(2):
+ # get next result
+ _, _, data_objects = next(cols)
+
+ # Delete data objects
+ for data_object in data_objects:
+ data_object.unlink(force=True)
+
+ return True
+
+ else:
+ # Delete from cache first
+ os.unlink(self._get_cache_path(rel_path))
+ # Delete from irods as well
+ p = Path(rel_path)
+ data_object_name = p.stem + p.suffix
+ subcollection_name = p.parent
+
+ collection_path = self.home + "/" + str(subcollection_name)
+ data_object_path = collection_path + "/" + str(data_object_name)
+
+ try:
+ data_obj = session.data_objects.get(data_object_path)
+ # remove object
+ data_obj.unlink(force=True)
+ return True
+ except (DataObjectDoesNotExist, CollectionDoesNotExist):
+ log.info("Collection or data object (%s) does not exist", data_object_path)
+ return True
+ except NetworkException as e:
+ log.exception(e)
+ return False
+ except OSError:
+ log.exception('%s delete error', self._get_filename(obj, **kwargs))
+ except NetworkException as e:
+ log.exception(e)
+ return False
def _get_data(self, obj, start=0, count=-1, **kwargs):
rel_path = self._construct_path(obj, **kwargs)
diff --git a/lib/galaxy/quota/__init__.py b/lib/galaxy/quota/__init__.py
index 7f907508893..cece83e23d7 100644
--- a/lib/galaxy/quota/__init__.py
+++ b/lib/galaxy/quota/__init__.py
@@ -156,7 +156,13 @@ class QuotaAgent(NoQuotaAgent):
except ZeroDivisionError:
return 100
- def set_entity_quota_associations(self, quotas=[], users=[], groups=[], delete_existing_assocs=True):
+ def set_entity_quota_associations(self, quotas=None, users=None, groups=None, delete_existing_assocs=True):
+ if quotas is None:
+ quotas = []
+ if users is None:
+ users = []
+ if groups is None:
+ groups = []
for quota in quotas:
if delete_existing_assocs:
flush_needed = False
diff --git a/lib/galaxy/security/validate_user_input.py b/lib/galaxy/security/validate_user_input.py
index d55cc58a81d..ef016330032 100644
--- a/lib/galaxy/security/validate_user_input.py
+++ b/lib/galaxy/security/validate_user_input.py
@@ -66,21 +66,25 @@ def validate_email(trans, email, user=None, check_dup=True, allow_empty=False):
message = "User with email '%s' already exists." % email
# If the allowlist is not empty filter out any domain not in the list and ignore blocklist.
elif trans.app.config.email_domain_allowlist_content is not None:
- domain = email.split('@')[1]
- if len(domain.split('.')) > 2:
- domain = ('.').join(domain.split('.')[-2:])
+ domain = extract_domain(email)
if domain not in trans.app.config.email_domain_allowlist_content:
message = "Please enter an allowed domain email address for this server."
# If the blocklist is not empty filter out the disposable domains.
elif trans.app.config.email_domain_blocklist_content is not None:
- domain = email.split('@')[1]
- if len(domain.split('.')) > 2:
- domain = ('.').join(domain.split('.')[-2:])
+ domain = extract_domain(email, base_only=True)
if domain in trans.app.config.email_domain_blocklist_content:
message = "Please enter your permanent email address."
return message
+def extract_domain(email, base_only=False):
+ domain = email.split('@')[1]
+ parts = domain.split('.')
+ if len(parts) > 2 and base_only:
+ return ('.').join(parts[-2:])
+ return domain
+
+
def validate_publicname(trans, publicname, user=None):
"""
Check that publicname respects the minimum and maximum string length, the
diff --git a/lib/galaxy/selenium/navigates_galaxy.py b/lib/galaxy/selenium/navigates_galaxy.py
index 7b1ee99aceb..53c093849b6 100644
--- a/lib/galaxy/selenium/navigates_galaxy.py
+++ b/lib/galaxy/selenium/navigates_galaxy.py
@@ -1413,13 +1413,15 @@ class NavigatesGalaxy(HasDriver):
assert text == expected, "Tooltip text [{}] was not expected text [{}].".format(text, expected)
def assert_error_message(self, contains=None):
- return self._assert_message("error", contains=contains)
+ element = self.components._.messages["error"]
+ return self.assert_message(element, contains=contains)
def assert_warning_message(self, contains=None):
- return self._assert_message("warning", contains=contains)
+ element = self.components._.messages["warning"]
+ return self.assert_message(element, contains=contains)
- def _assert_message(self, message_type, contains=None):
- element = self.components._.messages[message_type].wait_for_visible()
+ def assert_message(self, element, contains=None):
+ element = element.wait_for_visible()
assert element, "No error message found, one expected."
if contains is not None:
text = element.text
diff --git a/lib/galaxy/selenium/navigation.yml b/lib/galaxy/selenium/navigation.yml
index e85d8c5962f..2182533ddc4 100644
--- a/lib/galaxy/selenium/navigation.yml
+++ b/lib/galaxy/selenium/navigation.yml
@@ -326,6 +326,7 @@ workflows:
workflow_run:
selectors:
+ warning: ".ui-form-composite .alert-warning"
input_div: "[step-label='${label}']"
input_data_div: "[step-label='${label}'] .select2-container"
# TODO: put step labels in the DOM ideally
@@ -415,6 +416,7 @@ admin:
allowlist: '#admin-link-allowlist'
selectors:
+ warning: '#center .alert-warning'
# TODO: place betters IDS or something on this in these grids in the DOM
datatypes_grid: '#data-types-grid'
data_tables_grid: '#data-tables-grid'
diff --git a/lib/galaxy/tool_util/deps/__init__.py b/lib/galaxy/tool_util/deps/__init__.py
index ce4836e7319..b043c543bcf 100644
--- a/lib/galaxy/tool_util/deps/__init__.py
+++ b/lib/galaxy/tool_util/deps/__init__.py
@@ -107,11 +107,13 @@ class DependencyManager:
"""
cached = False
- def __init__(self, default_base_path, conf_file=None, app_config={}):
+ def __init__(self, default_base_path, conf_file=None, app_config=None):
"""
Create a new dependency manager looking for packages under the paths listed
in `base_paths`. The default base path is app.config.tool_dependency_dir.
"""
+ if app_config is None:
+ app_config = {}
if not os.path.exists(default_base_path):
log.warning("Path '%s' does not exist, ignoring", default_base_path)
if not os.path.isdir(default_base_path):
@@ -149,9 +151,11 @@ class DependencyManager:
"""Returns the union of enabled container types."""
return self._enabled_container_types
- def get_resolver_option(self, resolver, key, explicit_resolver_options={}):
+ def get_resolver_option(self, resolver, key, explicit_resolver_options=None):
"""Look in resolver-specific settings for option and then fallback to global settings.
"""
+ if explicit_resolver_options is None:
+ explicit_resolver_options = {}
default = resolver.config_options.get(key)
config_prefix = resolver.resolver_type
global_key = "{}_{}".format(config_prefix, key)
@@ -408,7 +412,9 @@ class CachedDependencyManager(DependencyManager):
class NullDependencyManager(DependencyManager):
cached = False
- def __init__(self, default_base_path=None, conf_file=None, app_config={}):
+ def __init__(self, default_base_path=None, conf_file=None, app_config=None):
+ if app_config is None:
+ app_config = {}
self.__app_config = app_config
self.resolver_classes = set()
self.dependency_resolvers = []
diff --git a/lib/galaxy/tool_util/deps/conda_util.py b/lib/galaxy/tool_util/deps/conda_util.py
index 469301d2d83..c538e9f58ee 100644
--- a/lib/galaxy/tool_util/deps/conda_util.py
+++ b/lib/galaxy/tool_util/deps/conda_util.py
@@ -219,9 +219,9 @@ class CondaContext(installable.InstallableContext):
Return the process exit code (i.e. 0 in case of success).
"""
cmd = [self.conda_exec]
+ cmd.extend(operation.split())
if self.debug:
cmd.append("--debug")
- cmd.append(operation)
cmd.extend(args)
env = {}
if self.condarc_override:
@@ -248,15 +248,15 @@ class CondaContext(installable.InstallableContext):
"""
Return the process exit code (i.e. 0 in case of success).
"""
- create_base_args = [
+ create_args = [
"-y",
"--quiet"
]
if allow_local and self.use_local:
- create_base_args.extend(["--use-local"])
- create_base_args.extend(self._override_channels_args)
- create_base_args.extend(args)
- return self.exec_command("create", create_base_args, stdout_path=stdout_path)
+ create_args.extend(["--use-local"])
+ create_args.extend(self._override_channels_args)
+ create_args.extend(args)
+ return self.exec_command("create", create_args, stdout_path=stdout_path)
def exec_remove(self, args):
"""
@@ -264,38 +264,38 @@ class CondaContext(installable.InstallableContext):
Return the process exit code (i.e. 0 in case of success).
"""
- remove_base_args = [
- "remove",
+ remove_args = [
"-y",
"--name"
]
- remove_base_args.extend(args)
- return self.exec_command("env", remove_base_args)
+ remove_args.extend(args)
+ return self.exec_command("env remove", remove_args)
def exec_install(self, args, allow_local=True, stdout_path=None):
"""
Return the process exit code (i.e. 0 in case of success).
"""
- install_base_args = [
+ install_args = [
"-y"
]
if allow_local and self.use_local:
- install_base_args.append("--use-local")
- install_base_args.extend(self._override_channels_args)
- install_base_args.extend(args)
- return self.exec_command("install", install_base_args, stdout_path=stdout_path)
+ install_args.append("--use-local")
+ install_args.extend(self._override_channels_args)
+ install_args.extend(args)
+ return self.exec_command("install", install_args, stdout_path=stdout_path)
- def exec_clean(self, args=[], quiet=False):
+ def exec_clean(self, args=None, quiet=False):
"""
Clean up after conda installation.
Return the process exit code (i.e. 0 in case of success).
"""
- clean_base_args = [
+ clean_args = [
"--tarballs",
"-y"
]
- clean_args = clean_base_args + args
+ if args:
+ clean_args.extend(args)
stdout_path = None
if quiet:
stdout_path = "/dev/null"
diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py
index f9d2fd03f11..899cf01d933 100644
--- a/lib/galaxy/tool_util/parser/xml.py
+++ b/lib/galaxy/tool_util/parser/xml.py
@@ -1,7 +1,5 @@
import logging
import re
-import sys
-import traceback
import uuid
from collections import OrderedDict
from math import isinf
@@ -13,7 +11,11 @@ from galaxy.tool_util.parser.util import (
DEFAULT_DELTA,
DEFAULT_DELTA_FRAC
)
-from galaxy.util import string_as_bool, xml_text, xml_to_string
+from galaxy.util import (
+ string_as_bool,
+ xml_text,
+ xml_to_string
+)
from .interface import (
InputSource,
PageSource,
@@ -488,12 +490,14 @@ class XmlToolSource(ToolSource):
def parse_strict_shell(self):
command_el = self._command_el
- if command_el is not None:
- return string_as_bool(command_el.get("strict", "False"))
- elif self.legacy_defaults:
- return False
+ if packaging.version.parse(self.parse_profile()) < packaging.version.parse('20.09'):
+ default = "False"
else:
- return True
+ default = "True"
+ if command_el is not None:
+ return string_as_bool(command_el.get("strict", default))
+ else:
+ return string_as_bool(default)
def parse_help(self):
help_elem = self.root.find('help')
@@ -829,7 +833,7 @@ class StdioParser:
self.parse_stdio_exit_codes(stdio_elem)
self.parse_stdio_regexes(stdio_elem)
except Exception:
- log.error("Exception in parse_stdio! " + str(sys.exc_info()))
+ log.exception("Exception in parse_stdio!")
def parse_stdio_exit_codes(self, stdio_elem):
"""
@@ -857,8 +861,7 @@ class StdioParser:
if code_range is None:
code_range = exit_code_elem.get("value", "")
if code_range is None:
- log.warning("Tool stdio exit codes must have " +
- "a range or value")
+ log.warning("Tool stdio exit codes must have a range or value")
continue
# Parse the range. We look for:
# :Y
@@ -899,18 +902,11 @@ class StdioParser:
# isn't bogus. If we have two infinite values, then
# the start must be -inf and the end must be +inf.
# So at least warn about this situation:
- if (isinf(exit_code.range_start) and
- isinf(exit_code.range_end)):
- log.warning("Tool exit_code range %s will match on " +
- "all exit codes" % code_range)
+ if isinf(exit_code.range_start) and isinf(exit_code.range_end):
+ log.warning("Tool exit_code range %s will match on all exit codes" % code_range)
self.stdio_exit_codes.append(exit_code)
except Exception:
- log.error("Exception in parse_stdio_exit_codes! " +
- str(sys.exc_info()))
- trace = sys.exc_info()[2]
- if trace is not None:
- trace_msg = repr(traceback.format_tb(trace))
- log.error("Traceback: %s" % trace_msg)
+ log.exception("Exception in parse_stdio_exit_codes!")
def parse_stdio_regexes(self, stdio_elem):
"""
@@ -973,12 +969,7 @@ class StdioParser:
regex.stderr_match = True
self.stdio_regexes.append(regex)
except Exception:
- log.error("Exception in parse_stdio_exit_codes! " +
- str(sys.exc_info()))
- trace = sys.exc_info()[2]
- if trace is not None:
- trace_msg = repr(traceback.format_tb(trace))
- log.error("Traceback: %s" % trace_msg)
+ log.exception("Exception in parse_stdio_exit_codes!")
# TODO: This method doesn't have to be part of the Tool class.
def parse_error_level(self, err_level):
@@ -1003,12 +994,7 @@ class StdioParser:
log.debug("Tool %s: error level %s did not match log/warning/fatal" %
(self.id, err_level))
except Exception:
- log.error("Exception in parse_error_level " +
- str(sys.exc_info()))
- trace = sys.exc_info()[2]
- if trace is not None:
- trace_msg = repr(traceback.format_tb(trace))
- log.error("Traceback: %s" % trace_msg)
+ log.exception("Exception in parse_error_level")
return return_level
@@ -1085,7 +1071,7 @@ class XmlInputSource(InputSource):
def parse_static_options(self):
static_options = list()
elem = self.input_elem
- for index, option in enumerate(elem.findall("option")):
+ for option in elem.findall("option"):
value = option.get("value")
selected = string_as_bool(option.get("selected", False))
static_options.append((option.text or value, value, selected))
diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd
index d0ee3d4682b..6bfd45cf079 100644
--- a/lib/galaxy/tool_util/xsd/galaxy.xsd
+++ b/lib/galaxy/tool_util/xsd/galaxy.xsd
@@ -2889,7 +2889,7 @@ Prior to Galaxy release 19.01 the stdio block has only been used for non-legacy
- This boolean forces the ``#set -e`` directive on in shell scripts - so that in a multi-part command if any part fails the job exits with a non-zero exit code. This is enabled by default for tools with ``profile>=16.04`` and disabled on legacy tools.
+ This boolean forces the ``#set -e`` directive on in shell scripts - so that in a multi-part command if any part fails the job exits with a non-zero exit code. This is enabled by default for tools with ``profile>=20.09`` and disabled on legacy tools.
diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py
index ab10eb5fb1c..4082d499f17 100755
--- a/lib/galaxy/tools/__init__.py
+++ b/lib/galaxy/tools/__init__.py
@@ -1583,13 +1583,13 @@ class Tool(Dictifiable):
output_collections=execution_tracker.output_collections,
implicit_collections=execution_tracker.implicit_collections)
- def handle_single_execution(self, trans, rerun_remap_job_id, execution_slice, history, execution_cache=None, completed_job=None, collection_info=None):
+ def handle_single_execution(self, trans, rerun_remap_job_id, execution_slice, history, execution_cache=None, completed_job=None, collection_info=None, flush_job=True):
"""
Return a pair with whether execution is successful as well as either
resulting output data or an error message indicating the problem.
"""
try:
- job, out_data = self.execute(
+ rval = self.execute(
trans,
incoming=execution_slice.param_combination,
history=history,
@@ -1598,7 +1598,13 @@ class Tool(Dictifiable):
dataset_collection_elements=execution_slice.dataset_collection_elements,
completed_job=completed_job,
collection_info=collection_info,
+ flush_job=flush_job,
)
+ job = rval[0]
+ out_data = rval[1]
+ if len(rval) == 4:
+ execution_slice.datasets_to_persist = rval[2]
+ execution_slice.history = rval[3]
except (webob.exc.HTTPFound, exceptions.MessageException) as e:
# if it's a webob redirect exception, pass it up the stack
raise e
diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py
index 1e13fc4e8fc..027deabd1a9 100644
--- a/lib/galaxy/tools/actions/__init__.py
+++ b/lib/galaxy/tools/actions/__init__.py
@@ -32,6 +32,7 @@ class ToolExecutionCache:
self.trans = trans
self.current_user_roles = trans.get_current_user_roles()
self.chrom_info = {}
+ self.cached_collection_elements = {}
def get_chrom_info(self, tool_id, input_dbkey):
genome_builds = self.trans.app.genome_builds
@@ -275,7 +276,7 @@ class DefaultToolAction:
preserved_tags[tag.value] = tag
return history, inp_data, inp_dataset_collections, preserved_tags, all_permissions
- def execute(self, tool, trans, incoming=None, return_job=False, set_output_hid=True, history=None, job_params=None, rerun_remap_job_id=None, execution_cache=None, dataset_collection_elements=None, completed_job=None, collection_info=None):
+ def execute(self, tool, trans, incoming=None, return_job=False, set_output_hid=True, history=None, job_params=None, rerun_remap_job_id=None, execution_cache=None, dataset_collection_elements=None, completed_job=None, collection_info=None, flush_job=True):
"""
Executes a tool, creating job and tool outputs, associating them, and
submitting the job to the job queue. If history is not specified, use
@@ -380,6 +381,7 @@ class DefaultToolAction:
inp_dataset_collections,
input_ext,
python_template_version=tool.python_template_version,
+ execution_cache=execution_cache,
)
create_datasets = True
dataset = None
@@ -407,7 +409,7 @@ class DefaultToolAction:
dataset_collection_elements[name].hda = data
trans.sa_session.add(data)
if not completed_job:
- trans.app.security_agent.set_all_dataset_permissions(data.dataset, output_permissions, new=True)
+ trans.app.security_agent.set_all_dataset_permissions(data.dataset, output_permissions, new=True, flush=False)
data.copy_tags_to(preserved_tags)
if not completed_job and trans.app.config.legacy_eager_objectstore_initialization:
@@ -453,6 +455,7 @@ class DefaultToolAction:
# Flush all datasets at once.
return data
+ datasets_to_persist = []
for name, output in tool.outputs.items():
if not filter_output(output, incoming):
handle_output_timer = ExecutionTimer()
@@ -460,7 +463,6 @@ class DefaultToolAction:
collections_manager = app.dataset_collections_service
element_identifiers = []
known_outputs = output.known_outputs(input_collections, collections_manager.type_registry)
- created_element_datasets = []
# Just to echo TODO elsewhere - this should be restructured to allow
# nested collections.
for output_part_def in known_outputs:
@@ -487,7 +489,7 @@ class DefaultToolAction:
effective_output_name = output_part_def.effective_output_name
element = handle_output(effective_output_name, output_part_def.output_def, hidden=True)
- created_element_datasets.append(element)
+ datasets_to_persist.append(element)
# TODO: this shouldn't exist in the top-level of the history at all
# but for now we are still working around that by hiding the contents
# there.
@@ -499,18 +501,21 @@ class DefaultToolAction:
"name": output_part_def.element_identifier,
})
- history.add_datasets(trans.sa_session, created_element_datasets, set_hid=set_output_hid, quota=False, flush=True)
if output.dynamic_structure:
assert not element_identifiers # known_outputs must have been empty
element_kwds = dict(elements=collections_manager.ELEMENTS_UNINITIALIZED)
else:
element_kwds = dict(element_identifiers=element_identifiers)
- output_collections.create_collection(
+ hdca = output_collections.create_collection(
output=output,
name=name,
+ set_hid=True if flush_job else False,
+ flush=flush_job,
**element_kwds
)
- log.info("Handled collection output named {} for tool {} {}".format(name, tool.id, handle_output_timer))
+ if hdca:
+ datasets_to_persist.append(hdca)
+ log.info("Handled collection output named %s for tool %s %s" % (name, tool.id, handle_output_timer))
else:
handle_output(name, output)
log.info("Handled output named {} for tool {} {}".format(name, tool.id, handle_output_timer))
@@ -520,13 +525,9 @@ class DefaultToolAction:
'Added output datasets to history',
)
# Add all the top-level (non-child) datasets to the history unless otherwise specified
- datasets_to_persist = []
for name, data in out_data.items():
if name not in child_dataset_names and name not in incoming: # don't add children; or already existing datasets, i.e. async created
datasets_to_persist.append(data)
- # Set HID and add to history.
- # This is brand new and certainly empty so don't worry about quota.
- history.add_datasets(trans.sa_session, datasets_to_persist, set_hid=set_output_hid, quota=False, flush=False)
# Add all the children to their parents
for parent_name, child_name in parent_to_child_pairs:
@@ -549,12 +550,17 @@ class DefaultToolAction:
# Now that we have a job id, we can remap any outputs if this is a rerun and the user chose to continue dependent jobs
# This functionality requires tracking jobs in the database.
if app.config.track_jobs_in_database and rerun_remap_job_id is not None:
+ # We need a flush here and get hids in order to rewrite jobs parameter,
+ # but remapping jobs should only affect single jobs anyway, so this is not too costly.
+ trans.sa_session.flush()
+ history.add_datasets(trans.sa_session, datasets_to_persist, set_hid=set_output_hid, quota=False, flush=False)
self._remap_job_on_rerun(trans=trans,
galaxy_session=galaxy_session,
rerun_remap_job_id=rerun_remap_job_id,
current_job=job,
out_data=out_data)
- log.info("Setup for job {} complete, ready to be enqueued {}".format(job.log_str(), job_setup_timer))
+ datasets_to_persist = []
+ log.info("Setup for job %s complete, ready to be enqueued %s" % (job.log_str(), job_setup_timer))
# Some tools are not really executable, but jobs are still created for them ( for record keeping ).
# Examples include tools that redirect to other applications ( epigraph ). These special tools must
@@ -577,10 +583,18 @@ class DefaultToolAction:
trans.sa_session.flush()
trans.response.send_redirect(url_for(controller='tool_runner', action='redirect', redirect_url=redirect_url))
else:
- # Dispatch to a job handler. enqueue() is responsible for flushing the job
- app.job_manager.enqueue(job, tool=tool)
- trans.log_event("Added job to the job queue, id: %s" % str(job.id), tool_id=job.tool_id)
- return job, out_data
+ if flush_job:
+ # Set HID and add to history.
+ # This is brand new and certainly empty so don't worry about quota.
+ history.add_datasets(trans.sa_session, datasets_to_persist, set_hid=set_output_hid, quota=False, flush=False)
+ job_flush_timer = ExecutionTimer()
+ trans.sa_session.flush()
+ log.info("Flushed transaction for job %s %s" % (job.log_str(), job_flush_timer))
+
+ # Dispatch to a job handler. enqueue() is responsible for flushing the job
+ app.job_manager.enqueue(job, tool=tool)
+ trans.log_event("Added job to the job queue, id: %s" % str(job.id), tool_id=job.tool_id)
+ return job, out_data, datasets_to_persist, history
def _remap_job_on_rerun(self, trans, galaxy_session, rerun_remap_job_id, current_job, out_data):
"""
@@ -803,7 +817,7 @@ class OutputCollections:
self.out_collection_instances = {}
self.tags = tags
- def create_collection(self, output, name, collection_type=None, **element_kwds):
+ def create_collection(self, output, name, collection_type=None, set_hid=True, flush=True, **element_kwds):
input_collections = self.input_collections
collections_manager = self.trans.app.dataset_collections_service
collection_type = collection_type or output.structure.collection_type
@@ -882,11 +896,14 @@ class OutputCollections:
collection_type=collection_type,
trusted_identifiers=True,
tags=self.tags,
+ set_hid=set_hid,
+ flush=flush,
**element_kwds
)
# name here is name of the output element - not name
# of the hdca.
self.out_collection_instances[name] = hdca
+ return hdca
def on_text_for_names(input_names):
@@ -933,7 +950,7 @@ def get_ext_or_implicit_ext(hda):
return hda.ext
-def determine_output_format(output, parameter_context, input_datasets, input_dataset_collections, random_input_ext, python_template_version='3'):
+def determine_output_format(output, parameter_context, input_datasets, input_dataset_collections, random_input_ext, python_template_version='3', execution_cache=None):
""" Determines the output format for a dataset based on an abstract
description of the output (galaxy.tool_util.parser.ToolOutput), the parameter
wrappers, a map of the input datasets (name => HDA), and the last input
@@ -974,7 +991,13 @@ def determine_output_format(output, parameter_context, input_datasets, input_dat
try:
input_element = input_collection_collection[element_index]
except KeyError:
- for element in input_collection_collection.dataset_elements:
+ if execution_cache:
+ dataset_elements = execution_cache.cached_collection_elements.get(input_collection_collection.id)
+ if dataset_elements is None:
+ dataset_elements = execution_cache.cached_collection_elements[input_collection_collection.id] = input_collection_collection.dataset_elements
+ else:
+ dataset_elements = input_collection_collection.dataset_elements
+ for element in dataset_elements:
if element.element_identifier == element_index:
input_element = element
break
diff --git a/lib/galaxy/tools/actions/data_manager.py b/lib/galaxy/tools/actions/data_manager.py
index ac3795fc7d1..6b0a761d212 100644
--- a/lib/galaxy/tools/actions/data_manager.py
+++ b/lib/galaxy/tools/actions/data_manager.py
@@ -9,8 +9,8 @@ class DataManagerToolAction(DefaultToolAction):
"""Tool action used for Data Manager Tools"""
def execute(self, tool, trans, **kwds):
- rval = super().execute(tool, trans, **kwds)
- if isinstance(rval, tuple) and len(rval) == 2 and isinstance(rval[0], trans.app.model.Job):
+ rval = super(DataManagerToolAction, self).execute(tool, trans, **kwds)
+ if isinstance(rval, tuple) and len(rval) >= 2 and isinstance(rval[0], trans.app.model.Job):
assoc = trans.app.model.DataManagerJobAssociation(job=rval[0], data_manager_id=tool.data_manager_id)
trans.sa_session.add(assoc)
trans.sa_session.flush()
diff --git a/lib/galaxy/tools/actions/upload_common.py b/lib/galaxy/tools/actions/upload_common.py
index 058955f347c..b01195e2afe 100644
--- a/lib/galaxy/tools/actions/upload_common.py
+++ b/lib/galaxy/tools/actions/upload_common.py
@@ -6,9 +6,9 @@ import socket
import tempfile
from collections import OrderedDict
from json import dump, dumps
+from urllib.parse import urlparse
from six import StringIO
-from six.moves.urllib.parse import urlparse
from sqlalchemy.orm import eagerload_all
from webob.compat import cgi_FieldStorage
@@ -37,6 +37,8 @@ def validate_url(url, ip_allowlist):
if not (url.lstrip().startswith('http://') or url.lstrip().startswith('https://')):
return url
+ # Strip leading whitespace before passing url to urlparse()
+ url = url.lstrip()
# Extract hostname component
parsed_url = urlparse(url).netloc
# If credentials are in this URL, we need to strip those.
diff --git a/lib/galaxy/tools/execute.py b/lib/galaxy/tools/execute.py
index d60719c7c38..6ffa6002c59 100644
--- a/lib/galaxy/tools/execute.py
+++ b/lib/galaxy/tools/execute.py
@@ -68,7 +68,7 @@ def execute(trans, tool, mapping_params, history, rerun_remap_job_id=None, colle
del params['__workflow_resource_params__']
if validate_outputs:
params['__validate_outputs__'] = True
- job, result = tool.handle_single_execution(trans, rerun_remap_job_id, execution_slice, history, execution_cache, completed_job, collection_info)
+ job, result = tool.handle_single_execution(trans, rerun_remap_job_id, execution_slice, history, execution_cache, completed_job, collection_info, flush_job=False)
if job:
log.debug(job_timer.to_str(tool_id=tool.id, job_id=job.id))
execution_tracker.record_success(execution_slice, job, result)
@@ -93,6 +93,7 @@ def execute(trans, tool, mapping_params, history, rerun_remap_job_id=None, colle
jobs_executed = 0
has_remaining_jobs = False
+ datasets_to_persist = []
for i, execution_slice in enumerate(execution_tracker.new_execution_slices()):
if max_num_jobs and jobs_executed >= max_num_jobs:
@@ -100,6 +101,19 @@ def execute(trans, tool, mapping_params, history, rerun_remap_job_id=None, colle
break
else:
execute_single_job(execution_slice, completed_jobs[i])
+ if execution_slice.datasets_to_persist:
+ datasets_to_persist.extend(execution_slice.datasets_to_persist)
+
+ if datasets_to_persist:
+ execution_slice.history.add_datasets(trans.sa_session, datasets_to_persist, set_hid=True, quota=False, flush=False)
+ # a side effect of history.add_datasets is a commit within db_next_hid (even with flush=False).
+ else:
+ # Make sure collections, implicit jobs etc are flushed even if there are no precreated output datasets
+ trans.sa_session.flush()
+ for job in execution_tracker.successful_jobs:
+ # Put the job in the queue if tracking in memory
+ tool.app.job_manager.enqueue(job, tool=tool)
+ trans.log_event("Added job to the job queue, id: %s" % str(job.id), tool_id=job.tool_id)
if has_remaining_jobs:
raise PartialJobExecution(execution_tracker)
@@ -116,6 +130,8 @@ class ExecutionSlice:
self.job_index = job_index
self.param_combination = param_combination
self.dataset_collection_elements = dataset_collection_elements
+ self.datasets_to_persist = None
+ self.history = None
class ExecutionTracker:
@@ -360,7 +376,7 @@ class ExecutionTracker:
job_assoc = model.ImplicitCollectionJobsJobAssociation()
job_assoc.order_index = execution_slice.job_index
job_assoc.implicit_collection_jobs = implicit_collection_jobs
- job_assoc.job_id = job.id
+ job_assoc.job = job
self.trans.sa_session.add(job_assoc)
diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py
index da9bb2c3901..f6d03c8d08c 100644
--- a/lib/galaxy/tools/parameters/validation.py
+++ b/lib/galaxy/tools/parameters/validation.py
@@ -65,6 +65,11 @@ class RegexValidator(Validator):
Traceback (most recent call last):
...
ValueError: Not gonna happen
+ >>> t = p.validate(["Foo", "foo"])
+ >>> t = p.validate(["Foo", "Fop"])
+ Traceback (most recent call last):
+ ...
+ ValueError: Not gonna happen
"""
@classmethod
@@ -78,8 +83,11 @@ class RegexValidator(Validator):
self.expression = expression
def validate(self, value, trans=None):
- if re.match(self.expression, value or '') is None:
- raise ValueError(self.message)
+ if not isinstance(value, list):
+ value = [value]
+ for val in value:
+ if re.match(self.expression, val or '') is None:
+ raise ValueError(self.message)
class ExpressionValidator(Validator):
diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py
index 972dcf8a214..78800ad1500 100644
--- a/lib/galaxy/util/__init__.py
+++ b/lib/galaxy/util/__init__.py
@@ -17,6 +17,7 @@ import stat
import string
import sys
import tempfile
+import textwrap
import threading
import time
import unicodedata
@@ -815,10 +816,9 @@ class Params:
# name. Anything relying on NEVER_SANITIZE should be
# changed to not require this and NEVER_SANITIZE should be
# removed.
- if (value is not None and
- key not in self.NEVER_SANITIZE and
- True not in [key.endswith("|%s" % nonsanitize_parameter) for
- nonsanitize_parameter in self.NEVER_SANITIZE]):
+ if (value is not None and key not in self.NEVER_SANITIZE
+ and True not in [key.endswith("|%s" % nonsanitize_parameter) for
+ nonsanitize_parameter in self.NEVER_SANITIZE]):
self.__dict__[key] = sanitize_param(value)
else:
self.__dict__[key] = value
@@ -1042,11 +1042,10 @@ def unicodify(value, encoding=DEFAULT_ENCODING, error='replace', strip_null=Fals
value = str(value)
# Now in Python 2, value is an instance of basestring, but may be not unicode
# Now in Python 3, value is an instance of bytes or str
- if not isinstance(value, str):
- value = str(value, encoding, error)
- except Exception:
- msg = "Value '%s' could not be coerced to Unicode" % value
- log.exception(msg)
+ if not isinstance(value, text_type):
+ value = text_type(value, encoding, error)
+ except Exception as e:
+ msg = "Value '%s' could not be coerced to Unicode: %s('%s')" % (value, type(e).__name__, e)
raise Exception(msg)
if strip_null:
return value.replace('\0', '')
@@ -1114,6 +1113,19 @@ def string_to_object(s):
return binascii.unhexlify(s)
+def clean_multiline_string(multiline_string, sep='\n'):
+ """
+ Dedent, split, remove first and last empty lines, rejoin.
+ """
+ multiline_string = textwrap.dedent(multiline_string)
+ string_list = multiline_string.split(sep)
+ if not string_list[0]:
+ string_list = string_list[1:]
+ if not string_list[-1]:
+ string_list = string_list[:-1]
+ return '\n'.join(string_list) + '\n'
+
+
class ParamsWithSpecs(collections.defaultdict):
"""
"""
@@ -1246,7 +1258,7 @@ def read_build_sites(filename, check_builds=True):
def relativize_symlinks(path, start=None, followlinks=False):
- for root, dirs, files in os.walk(path, followlinks=followlinks):
+ for root, _, files in os.walk(path, followlinks=followlinks):
rel_start = None
for file_name in files:
symlink_file_name = os.path.join(root, file_name)
@@ -1280,7 +1292,7 @@ def mkstemp_ln(src, prefix='mkstemp_ln_'):
"""
dir = os.path.dirname(src)
names = tempfile._get_candidate_names()
- for seq in xrange(tempfile.TMP_MAX):
+ for _ in xrange(tempfile.TMP_MAX):
name = next(names)
file = os.path.join(dir, prefix + name)
try:
@@ -1682,6 +1694,15 @@ def download_to_file(url, dest_file_path, timeout=30, chunk_size=2 ** 20):
f.write(chunk)
+class classproperty(object):
+
+ def __init__(self, f):
+ self.f = f
+
+ def __get__(self, obj, owner):
+ return self.f(owner)
+
+
def get_executable():
exe = sys.executable
if exe.endswith('uwsgi'):
diff --git a/lib/galaxy/util/checkers.py b/lib/galaxy/util/checkers.py
index e4822b98c6c..25453a37f75 100644
--- a/lib/galaxy/util/checkers.py
+++ b/lib/galaxy/util/checkers.py
@@ -1,43 +1,49 @@
import bz2
import gzip
+import io
import re
import tarfile
import zipfile
-from six import BytesIO
+from six import (
+ BytesIO,
+ StringIO
+)
+from six.moves import filter
from galaxy import util
from galaxy.util.image_util import image_type
HTML_CHECK_LINES = 100
+CHUNK_SIZE = 2 ** 15 # 32Kb
+HTML_REGEXPS = (
+ re.compile(r"]*HREF[^>]+>", re.I),
+ re.compile(r"