@@ -12,7 +223,7 @@
@@ -50,267 +261,3 @@
-
-
diff --git a/client/src/composables/shortTermStorage.js b/client/src/composables/shortTermStorage.js
index 3e34d7226816..8b78cc013737 100644
--- a/client/src/composables/shortTermStorage.js
+++ b/client/src/composables/shortTermStorage.js
@@ -30,8 +30,13 @@ export function useShortTermStorage() {
return prepareObjectDownload(invocationId, "invocations", options);
}
- function downloadObjectByRequestId(storageRequestId) {
+ function getDownloadObjectUrl(storageRequestId) {
const url = withPrefix(`/api/short_term_storage/${storageRequestId}`);
+ return url;
+ }
+
+ function downloadObjectByRequestId(storageRequestId) {
+ const url = getDownloadObjectUrl(storageRequestId);
window.location.assign(url);
}
@@ -119,5 +124,10 @@ export function useShortTermStorage() {
* Whether the download is still being prepared.
*/
isPreparing: readonly(isPreparing),
+ /**
+ * Given a storageRequestId it returns the download URL for that object.
+ * @param {String} storageRequestId The storage request ID associated to the object to be downloaded
+ */
+ getDownloadObjectUrl,
};
}
diff --git a/client/src/stores/workflowConnectionStore.ts b/client/src/stores/workflowConnectionStore.ts
index 46504ada1e10..170ddf76d80f 100644
--- a/client/src/stores/workflowConnectionStore.ts
+++ b/client/src/stores/workflowConnectionStore.ts
@@ -1,5 +1,6 @@
import { defineStore } from "pinia";
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
+import { pushOrSet } from "@/utils/pushOrSet";
import Vue from "vue";
interface InvalidConnections {
@@ -9,6 +10,9 @@ interface InvalidConnections {
export interface State {
connections: Connection[];
invalidConnections: InvalidConnections;
+ inputTerminalToOutputTerminals: TerminalToOutputTerminals;
+ terminalToConnection: { [index: string]: Connection[] };
+ stepToConnections: { [index: number]: Connection[] };
}
export class Connection {
@@ -44,80 +48,38 @@ interface TerminalToOutputTerminals {
[index: string]: OutputTerminal[];
}
-interface TerminalToInputTerminals {
- [index: string]: InputTerminal[];
-}
-
-/**
- * Pushes a value to an array in an object, if the array exists. Else creates a new array containing value.
- * @param object Object which contains array
- * @param key Key which array is in
- * @param value Value to push
- */
-function pushOrSet
(object: { [key: string | number]: Array }, key: string | number, value: T) {
- if (key in object) {
- object[key]!.push(value);
- } else {
- object[key] = [value];
- }
-}
-
export const useConnectionStore = defineStore("workflowConnectionStore", {
state: (): State => ({
connections: [] as Connection[],
invalidConnections: {} as InvalidConnections,
+ inputTerminalToOutputTerminals: {} as TerminalToOutputTerminals,
+ terminalToConnection: {} as { [index: string]: Connection[] },
+ stepToConnections: {} as { [index: number]: Connection[] },
}),
getters: {
getOutputTerminalsForInputTerminal(state: State) {
- const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {};
- state.connections.map((connection) => {
- const terminals = getTerminals(connection);
- const inputTerminalId = getTerminalId(terminals.input);
- pushOrSet(inputTerminalToOutputTerminals, inputTerminalId, terminals.output);
- });
return (terminalId: string): OutputTerminal[] => {
- return inputTerminalToOutputTerminals[terminalId] || [];
- };
- },
- getInputTerminalsForOutputTerminal(state: State) {
- const outputTerminalToInputTerminals: TerminalToInputTerminals = {};
- state.connections.map((connection) => {
- const terminals = getTerminals(connection);
- const outputTerminalId = getTerminalId(terminals.output);
- pushOrSet(outputTerminalToInputTerminals, outputTerminalId, terminals.input);
- });
- return (terminalId: string): BaseTerminal[] => {
- return outputTerminalToInputTerminals[terminalId] || [];
+ return state.inputTerminalToOutputTerminals[terminalId] || [];
};
},
getConnectionsForTerminal(state: State) {
- const terminalToConnection: { [index: string]: Connection[] } = {};
- state.connections.map((connection) => {
- const terminals = getTerminals(connection);
- const outputTerminalId = getTerminalId(terminals.output);
- pushOrSet(terminalToConnection, outputTerminalId, connection);
-
- const inputTerminalId = getTerminalId(terminals.input);
- pushOrSet(terminalToConnection, inputTerminalId, connection);
- });
return (terminalId: string): Connection[] => {
- return terminalToConnection[terminalId] || [];
+ return state.terminalToConnection[terminalId] || [];
};
},
getConnectionsForStep(state: State) {
- const stepToConnections: { [index: number]: Connection[] } = {};
- state.connections.map((connection) => {
- pushOrSet(stepToConnections, connection.input.stepId, connection);
- pushOrSet(stepToConnections, connection.output.stepId, connection);
- });
- return (stepId: number): Connection[] => stepToConnections[stepId] || [];
+ return (stepId: number): Connection[] => state.stepToConnections[stepId] || [];
},
},
actions: {
- addConnection(this: State, connection: Connection) {
+ addConnection(this, _connection: Connection) {
+ const connection = Object.freeze(_connection);
this.connections.push(connection);
const stepStore = useWorkflowStepStore();
stepStore.addConnection(connection);
+ this.terminalToConnection = updateTerminalToConnection(this.connections);
+ this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections);
+ this.stepToConnections = updateStepToConnections(this.connections);
},
markInvalidConnection(this: State, connectionId: string, reason: string) {
Vue.set(this.invalidConnections, connectionId, reason);
@@ -125,7 +87,7 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
dropFromInvalidConnections(this: State, connectionId: string) {
Vue.delete(this.invalidConnections, connectionId);
},
- removeConnection(this: State, terminal: InputTerminal | OutputTerminal | Connection["id"]) {
+ removeConnection(this, terminal: InputTerminal | OutputTerminal | Connection["id"]) {
const stepStore = useWorkflowStepStore();
this.connections = this.connections.filter((connection) => {
if (typeof terminal === "string") {
@@ -154,10 +116,44 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
}
}
});
+ this.terminalToConnection = updateTerminalToConnection(this.connections);
+ this.inputTerminalToOutputTerminals = updateTerminalToTerminal(this.connections);
+ this.stepToConnections = updateStepToConnections(this.connections);
},
},
});
+function updateTerminalToTerminal(connections: Connection[]) {
+ const inputTerminalToOutputTerminals: TerminalToOutputTerminals = {};
+ connections.map((connection) => {
+ const terminals = getTerminals(connection);
+ const inputTerminalId = getTerminalId(terminals.input);
+ pushOrSet(inputTerminalToOutputTerminals, inputTerminalId, terminals.output);
+ });
+ return inputTerminalToOutputTerminals;
+}
+
+function updateTerminalToConnection(connections: Connection[]) {
+ const terminalToConnection: { [index: string]: Connection[] } = {};
+ connections.map((connection) => {
+ const terminals = getTerminals(connection);
+ const outputTerminalId = getTerminalId(terminals.output);
+ pushOrSet(terminalToConnection, outputTerminalId, connection);
+ const inputTerminalId = getTerminalId(terminals.input);
+ pushOrSet(terminalToConnection, inputTerminalId, connection);
+ });
+ return terminalToConnection;
+}
+
+function updateStepToConnections(connections: Connection[]) {
+ const stepToConnections: { [index: number]: Connection[] } = {};
+ connections.map((connection) => {
+ pushOrSet(stepToConnections, connection.input.stepId, connection);
+ pushOrSet(stepToConnections, connection.output.stepId, connection);
+ });
+ return stepToConnections;
+}
+
export function getTerminalId(item: BaseTerminal): string {
return `node-${item.stepId}-${item.connectorType}-${item.name}`;
}
diff --git a/client/src/stores/workflowEditorStateStore.ts b/client/src/stores/workflowEditorStateStore.ts
index 77f7dfcc33dd..0cfd7d7040c1 100644
--- a/client/src/stores/workflowEditorStateStore.ts
+++ b/client/src/stores/workflowEditorStateStore.ts
@@ -4,6 +4,16 @@ import { defineStore } from "pinia";
import type { OutputTerminals } from "@/components/Workflow/Editor/modules/terminals";
import type { UseElementBoundingReturn } from "@vueuse/core";
+export interface InputTerminalPosition {
+ endX: number;
+ endY: number;
+}
+
+export interface OutputTerminalPosition {
+ startX: number;
+ startY: number;
+}
+
export interface TerminalPosition {
startX: number;
endX: number;
@@ -17,8 +27,8 @@ export interface XYPosition {
}
interface State {
- inputTerminals: { [index: number]: { [index: string]: TerminalPosition } };
- outputTerminals: { [index: number]: { [index: string]: TerminalPosition } };
+ inputTerminals: { [index: number]: { [index: string]: InputTerminalPosition } };
+ outputTerminals: { [index: number]: { [index: string]: OutputTerminalPosition } };
draggingPosition: TerminalPosition | null;
draggingTerminal: OutputTerminals | null;
activeNodeId: number | null;
@@ -50,14 +60,14 @@ export const useWorkflowStateStore = defineStore("workflowStateStore", {
},
},
actions: {
- setInputTerminalPosition(stepId: number, inputName: string, position: TerminalPosition) {
+ setInputTerminalPosition(stepId: number, inputName: string, position: InputTerminalPosition) {
if (this.inputTerminals[stepId]) {
Vue.set(this.inputTerminals[stepId]!, inputName, position);
} else {
Vue.set(this.inputTerminals, stepId, { [inputName]: position });
}
},
- setOutputTerminalPosition(stepId: number, outputName: string, position: TerminalPosition) {
+ setOutputTerminalPosition(stepId: number, outputName: string, position: OutputTerminalPosition) {
if (this.outputTerminals[stepId]) {
Vue.set(this.outputTerminals[stepId]!, outputName, position);
} else {
diff --git a/client/src/stores/workflowStepStore.test.ts b/client/src/stores/workflowStepStore.test.ts
index 01a8810b5c94..9cf7b3376229 100644
--- a/client/src/stores/workflowStepStore.test.ts
+++ b/client/src/stores/workflowStepStore.test.ts
@@ -12,6 +12,7 @@ const stepInputConnection: StepInputConnection = {
};
const workflowStepZero: NewStep = {
+ id: 0,
input_connections: {},
inputs: [],
name: "a step",
@@ -33,7 +34,7 @@ describe("Connection Store", () => {
const stepStore = useWorkflowStepStore();
expect(stepStore.steps).toStrictEqual({});
stepStore.addStep(workflowStepZero);
- expect(stepStore.getStep(0)).toBe(workflowStepZero);
+ expect(stepStore.getStep(0)).toStrictEqual(workflowStepZero);
expect(workflowStepZero.id).toBe(0);
});
it("removes step", () => {
diff --git a/client/src/stores/workflowStepStore.ts b/client/src/stores/workflowStepStore.ts
index 79b3180c67c1..31e95c9ee0c3 100644
--- a/client/src/stores/workflowStepStore.ts
+++ b/client/src/stores/workflowStepStore.ts
@@ -10,6 +10,7 @@ interface State {
stepIndex: number;
stepMapOver: { [index: number]: CollectionTypeDescriptor };
stepInputMapOver: StepInputMapOver;
+ stepExtraInputs: { [index: number]: InputTerminalSource[] };
}
interface StepPosition {
@@ -130,7 +131,7 @@ export interface ConnectionOutputLink {
input_subworkflow_step_id?: number;
}
-interface WorkflowOutputs {
+export interface WorkflowOutputs {
[index: string]: {
stepId: number;
outputName: string;
@@ -147,6 +148,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
stepMapOver: {} as { [index: number]: CollectionTypeDescriptor },
stepInputMapOver: {} as StepInputMapOver,
stepIndex: -1,
+ stepExtraInputs: {} as { [index: number]: InputTerminalSource[] },
}),
getters: {
getStep(state: State) {
@@ -155,30 +157,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
};
},
getStepExtraInputs(state: State) {
- const extraInputs: { [index: number]: InputTerminalSource[] } = {};
- Object.values(state.steps).forEach((step) => {
- if (step?.when !== undefined) {
- Object.keys(step.input_connections).forEach((inputName) => {
- if (!step.inputs.find((input) => input.name === inputName) && step.when?.includes(inputName)) {
- const terminalSource = {
- name: inputName,
- optional: false,
- input_type: "parameter" as const,
- type: "boolean" as const,
- multiple: false,
- label: inputName,
- extensions: [],
- };
- if (extraInputs[step.id]) {
- extraInputs[step.id]!.push(terminalSource);
- } else {
- extraInputs[step.id] = [terminalSource];
- }
- }
- });
- }
- });
- return (stepId: number) => extraInputs[stepId] || [];
+ return (stepId: number) => this.stepExtraInputs[stepId] || [];
},
getStepIndex(state: State) {
return Math.max(...Object.values(state.steps).map((step) => step.id), state.stepIndex);
@@ -206,18 +185,19 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
actions: {
addStep(newStep: NewStep): Step {
const stepId = newStep.id ? newStep.id : this.getStepIndex + 1;
- newStep.id = stepId;
- const step = newStep as Step;
+ const step = Object.freeze({ ...newStep, id: stepId } as Step);
Vue.set(this.steps, stepId.toString(), step);
const connectionStore = useConnectionStore();
stepToConnections(step).map((connection) => connectionStore.addConnection(connection));
+ this.stepExtraInputs[step.id] = getStepExtraInputs(step);
return step;
},
updateStep(this: State, step: Step) {
- step.workflow_outputs = step.workflow_outputs?.filter((workflowOutput) =>
+ const workflow_outputs = step.workflow_outputs?.filter((workflowOutput) =>
step.outputs.find((output) => workflowOutput.output_name == output.name)
);
- this.steps[step.id.toString()] = step;
+ this.steps[step.id.toString()] = Object.freeze({ ...step, workflow_outputs });
+ this.stepExtraInputs[step.id] = getStepExtraInputs(step);
},
changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) {
Vue.set(this.stepMapOver, stepId, mapOver);
@@ -298,6 +278,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
.getConnectionsForStep(stepId)
.forEach((connection) => connectionStore.removeConnection(connection.id));
Vue.delete(this.steps, stepId.toString());
+ Vue.delete(this.stepExtraInputs, stepId);
},
},
});
@@ -335,3 +316,24 @@ export function stepToConnections(step: Step): Connection[] {
}
return connections;
}
+
+function getStepExtraInputs(step: Step) {
+ const extraInputs: InputTerminalSource[] = [];
+ if (step.when !== undefined) {
+ Object.keys(step.input_connections).forEach((inputName) => {
+ if (!step.inputs.find((input) => input.name === inputName) && step.when?.includes(inputName)) {
+ const terminalSource = {
+ name: inputName,
+ optional: false,
+ input_type: "parameter" as const,
+ type: "boolean" as const,
+ multiple: false,
+ label: inputName,
+ extensions: [],
+ };
+ extraInputs.push(terminalSource);
+ }
+ });
+ }
+ return extraInputs;
+}
diff --git a/client/src/utils/navigation/navigation.yml b/client/src/utils/navigation/navigation.yml
index 30b5e96f5155..0d953060a9a4 100644
--- a/client/src/utils/navigation/navigation.yml
+++ b/client/src/utils/navigation/navigation.yml
@@ -612,8 +612,8 @@ workflow_editor:
output_terminal: "${_} [output-name='${name}']"
input_terminal: "${_} [input-name='${name}']"
input_mapping_icon: "${_} [input-name='${name}'].multiple"
- workflow_output_toggle: "${_} .callout-terminal.${name}"
- workflow_output_toggle_active: "${_} .callout-terminal.${name} .mark-terminal-active"
+ workflow_output_toggle: "${_} [data-output-name='${name}'] .callout-terminal "
+ workflow_output_toggle_active: "${_} [data-output-name='${name}'] .mark-terminal-active"
selectors:
canvas_body: '#workflow-canvas'
edit_annotation: '#workflow-annotation'
diff --git a/client/src/utils/pushOrSet.ts b/client/src/utils/pushOrSet.ts
new file mode 100644
index 000000000000..b38886bd86b6
--- /dev/null
+++ b/client/src/utils/pushOrSet.ts
@@ -0,0 +1,13 @@
+/**
+ * Pushes a value to an array in an object, if the array exists. Else creates a new array containing value.
+ * @param object Object which contains array
+ * @param key Key which array is in
+ * @param value Value to push
+ */
+export function pushOrSet(object: { [key in K]: Array }, key: K, value: T) {
+ if (key in object) {
+ object[key]!.push(value);
+ } else {
+ object[key] = [value];
+ }
+}
diff --git a/client/yarn.lock b/client/yarn.lock
index ceb98b1a2f48..3eebe8e00fb8 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -7582,6 +7582,14 @@ last-run@^1.1.0:
default-resolution "^2.0.0"
es6-weak-map "^2.0.1"
+launch-editor@^2.6.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.0.tgz#4c0c1a6ac126c572bd9ff9a30da1d2cae66defd7"
+ integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==
+ dependencies:
+ picocolors "^1.0.0"
+ shell-quote "^1.7.3"
+
lazystream@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz"
@@ -9936,6 +9944,11 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+shell-quote@^1.7.3:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba"
+ integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==
+
side-channel@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz"
@@ -11258,10 +11271,10 @@ webpack-dev-middleware@^5.3.1:
range-parser "^1.2.1"
schema-utils "^4.0.0"
-webpack-dev-server@^4.11.1:
- version "4.11.1"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz#ae07f0d71ca0438cf88446f09029b92ce81380b5"
- integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==
+webpack-dev-server@^4.12.0:
+ version "4.12.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.12.0.tgz#e2dcad4d43e486c3bac48ddbf346e77ef03c7428"
+ integrity sha512-XRN9YRnvOj3TQQ5w/0pR1y1xDcVnbWtNkTri46kuEbaWUPTHsWUvOyAAI7PZHLY+hsFki2kRltJjKMw7e+IiqA==
dependencies:
"@types/bonjour" "^3.5.9"
"@types/connect-history-api-fallback" "^1.3.5"
@@ -11282,6 +11295,7 @@ webpack-dev-server@^4.11.1:
html-entities "^2.3.2"
http-proxy-middleware "^2.0.3"
ipaddr.js "^2.0.1"
+ launch-editor "^2.6.0"
open "^8.0.9"
p-retry "^4.5.0"
rimraf "^3.0.2"
@@ -11291,7 +11305,7 @@ webpack-dev-server@^4.11.1:
sockjs "^0.3.24"
spdy "^4.0.2"
webpack-dev-middleware "^5.3.1"
- ws "^8.4.2"
+ ws "^8.13.0"
webpack-merge@^5.7.3, webpack-merge@^5.8.0:
version "5.8.0"
@@ -11534,10 +11548,10 @@ ws@^8.11.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
-ws@^8.4.2:
- version "8.8.1"
- resolved "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz"
- integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
+ws@^8.13.0:
+ version "8.13.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
+ integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
xml-beautifier@^0.5.0:
version "0.5.0"
diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst
index 7359d396e855..3d54bea52166 100644
--- a/doc/source/admin/galaxy_options.rst
+++ b/doc/source/admin/galaxy_options.rst
@@ -4881,6 +4881,9 @@
The `broker_url` option, if unset, defaults to the value of
`amqp_internal_connection`. The `result_backend` option must be
set if the `enable_celery_tasks` option is set.
+ The galaxy.fetch_data task can be disabled by setting its route to
+ "disabled": `galaxy.fetch_data: disabled`. (Other tasks cannot be
+ disabled on a per-task basis at this time.)
For details, see Celery documentation at
https://docs.celeryq.dev/en/stable/userguide/configuration.html.
:Default: ``{'task_routes': {'galaxy.fetch_data': 'galaxy.external', 'galaxy.set_job_metadata': 'galaxy.external'}}``
diff --git a/lib/galaxy/config/__init__.py b/lib/galaxy/config/__init__.py
index 673e5f26bc90..c79f3166aad5 100644
--- a/lib/galaxy/config/__init__.py
+++ b/lib/galaxy/config/__init__.py
@@ -137,6 +137,7 @@
VERSION_JSON_FILE = "version.json"
DEFAULT_EMAIL_FROM_LOCAL_PART = "galaxy-no-reply"
+DISABLED_FLAG = "disabled" # Used to mark a config option as disabled
def configure_logging(config, facts=None):
@@ -1289,6 +1290,17 @@ def check(self):
f"Config option '{key}' is deprecated and will be removed in a future release. Please consult the latest version of the sample configuration file."
)
+ def is_fetch_with_celery_enabled(self):
+ """
+ True iff celery is enabled and celery_conf["task_routes"]["galaxy.fetch_data"] != DISABLED_FLAG.
+ """
+ celery_enabled = self.enable_celery_tasks
+ try:
+ fetch_disabled = self.celery_conf["task_routes"]["galaxy.fetch_data"] == DISABLED_FLAG
+ except (TypeError, KeyError): # celery_conf is None or sub-dictionary is none or either key is not present
+ fetch_disabled = False
+ return celery_enabled and not fetch_disabled
+
@staticmethod
def _parse_allowed_origin_hostnames(allowed_origin_hostnames):
"""
diff --git a/lib/galaxy/config/sample/galaxy.yml.sample b/lib/galaxy/config/sample/galaxy.yml.sample
index 060eb945b674..fdb8f94fbc7e 100644
--- a/lib/galaxy/config/sample/galaxy.yml.sample
+++ b/lib/galaxy/config/sample/galaxy.yml.sample
@@ -2613,6 +2613,9 @@ galaxy:
# The `broker_url` option, if unset, defaults to the value of
# `amqp_internal_connection`. The `result_backend` option must be set
# if the `enable_celery_tasks` option is set.
+ # The galaxy.fetch_data task can be disabled by setting its route to
+ # "disabled": `galaxy.fetch_data: disabled`. (Other tasks cannot be
+ # disabled on a per-task basis at this time.)
# For details, see Celery documentation at
# https://docs.celeryq.dev/en/stable/userguide/configuration.html.
#celery_conf:
diff --git a/lib/galaxy/config/schemas/config_schema.yml b/lib/galaxy/config/schemas/config_schema.yml
index 285ed882e7ab..3e6b1881c627 100644
--- a/lib/galaxy/config/schemas/config_schema.yml
+++ b/lib/galaxy/config/schemas/config_schema.yml
@@ -3564,6 +3564,9 @@ mapping:
The `broker_url` option, if unset, defaults to the value of `amqp_internal_connection`.
The `result_backend` option must be set if the `enable_celery_tasks` option is set.
+ The galaxy.fetch_data task can be disabled by setting its route to "disabled": `galaxy.fetch_data: disabled`.
+ (Other tasks cannot be disabled on a per-task basis at this time.)
+
For details, see Celery documentation at https://docs.celeryq.dev/en/stable/userguide/configuration.html.
enable_celery_tasks:
diff --git a/lib/galaxy/dependencies/pinned-requirements.txt b/lib/galaxy/dependencies/pinned-requirements.txt
index b548d5f38f79..45450221087f 100644
--- a/lib/galaxy/dependencies/pinned-requirements.txt
+++ b/lib/galaxy/dependencies/pinned-requirements.txt
@@ -187,7 +187,7 @@ tinydb==4.7.1 ; python_version >= "3.7" and python_version < "3.12"
tornado==6.2 ; python_version >= "3.7" and python_version < "3.12"
tqdm==4.64.1 ; python_version >= "3.7" and python_version < "3.12"
tuspy==1.0.0 ; python_version >= "3.7" and python_version < "3.12"
-tuswsgi==0.5.4 ; python_version >= "3.7" and python_version < "3.12"
+tuswsgi==0.5.5 ; python_version >= "3.7" and python_version < "3.12"
typing-extensions==4.4.0 ; python_version >= "3.7" and python_version < "3.12"
tzlocal==2.1 ; python_version >= "3.7" and python_version < "3.12"
ubiquerg==0.6.2 ; python_version >= "3.7" and python_version < "3.12"
diff --git a/lib/galaxy/tools/execute.py b/lib/galaxy/tools/execute.py
index 9cfb9a3831a0..4df7e29e3d74 100644
--- a/lib/galaxy/tools/execute.py
+++ b/lib/galaxy/tools/execute.py
@@ -182,7 +182,7 @@ def execute_single_job(execution_slice, completed_job, skip=False):
tool_id = tool.id
for job2 in execution_tracker.successful_jobs:
# Put the job in the queue if tracking in memory
- if tool_id == "__DATA_FETCH__" and tool.app.config.enable_celery_tasks:
+ if tool_id == "__DATA_FETCH__" and tool.app.config.is_fetch_with_celery_enabled():
job_id = job2.id
from galaxy.celery.tasks import (
fetch_data,
diff --git a/lib/galaxy/tools/imp_exp/unpack_tar_gz_archive.py b/lib/galaxy/tools/imp_exp/unpack_tar_gz_archive.py
index 8fbd0a3aa751..0b630e34de40 100644
--- a/lib/galaxy/tools/imp_exp/unpack_tar_gz_archive.py
+++ b/lib/galaxy/tools/imp_exp/unpack_tar_gz_archive.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
"""
-Unpack a tar or tar.gz archive into a directory.
+Unpack a tar, tar.gz or zip archive into a directory.
usage: %prog archive_source dest_dir
--[url|file] source type, either a URL or a file.
@@ -11,6 +11,7 @@
import optparse
import os
import tarfile
+import zipfile
from base64 import b64decode
from galaxy.files import ConfiguredFileSources
@@ -35,11 +36,19 @@ def check_archive(archive_file, dest_dir):
Ensure that a tar archive has no absolute paths or relative paths outside
the archive.
"""
- with tarfile.open(archive_file, mode="r") as archive_fp:
- for arc_path in archive_fp.getnames():
- assert os.path.normpath(os.path.join(dest_dir, arc_path)).startswith(
- dest_dir.rstrip(os.sep) + os.sep
- ), f"Archive member would extract outside target directory: {arc_path}"
+ if zipfile.is_zipfile(archive_file):
+ with zipfile.ZipFile(archive_file, "r") as archive_fp:
+ for arc_path in archive_fp.namelist():
+ assert not os.path.isabs(arc_path), f"Archive member has absolute path: {arc_path}"
+ assert not os.path.relpath(arc_path).startswith(
+ ".."
+ ), f"Archive member would extract outside target directory: {arc_path}"
+ else:
+ with tarfile.open(archive_file, mode="r") as archive_fp:
+ for arc_path in archive_fp.getnames():
+ assert os.path.normpath(os.path.join(dest_dir, arc_path)).startswith(
+ dest_dir.rstrip(os.sep) + os.sep
+ ), f"Archive member would extract outside target directory: {arc_path}"
return True
@@ -47,9 +56,13 @@ def unpack_archive(archive_file, dest_dir):
"""
Unpack a tar and/or gzipped archive into a destination directory.
"""
- archive_fp = tarfile.open(archive_file, mode="r")
- archive_fp.extractall(path=dest_dir)
- archive_fp.close()
+ if zipfile.is_zipfile(archive_file):
+ with zipfile.ZipFile(archive_file, "r") as zip_archive:
+ zip_archive.extractall(path=dest_dir)
+ else:
+ archive_fp = tarfile.open(archive_file, mode="r")
+ archive_fp.extractall(path=dest_dir)
+ archive_fp.close()
def main(options, args):
diff --git a/test/unit/config/test_config_values.py b/test/unit/config/test_config_values.py
index b085b05b35bb..42b7e80bbb1d 100644
--- a/test/unit/config/test_config_values.py
+++ b/test/unit/config/test_config_values.py
@@ -7,7 +7,7 @@
from galaxy.util.properties import running_from_source
-@pytest.fixture(scope="module")
+@pytest.fixture()
def appconfig():
return config.GalaxyAppConfiguration(override_tempdir=False)
@@ -50,3 +50,44 @@ def test_assign_email_from(monkeypatch):
override_tempdir=False, galaxy_infrastructure_url="http://myhost:8080/galaxy/"
)
assert appconfig.email_from == f"{DEFAULT_EMAIL_FROM_LOCAL_PART}@myhost"
+
+
+class TestIsFetchWithCeleryEnabled:
+ def test_disabled_if_celery_disabled(self, appconfig):
+ appconfig.enable_celery_tasks = False
+ assert not appconfig.is_fetch_with_celery_enabled()
+
+ def test_enabled_if_no_celeryconf(self, appconfig):
+ appconfig.enable_celery_tasks = True
+ appconfig.celery_conf = None
+ assert appconfig.is_fetch_with_celery_enabled()
+
+ def test_enabled_if_no_task_routes_key(self, appconfig):
+ appconfig.enable_celery_tasks = True
+ appconfig.celery_conf = {"some-other-key": 1}
+ assert appconfig.is_fetch_with_celery_enabled()
+
+ def test_enabled_if_task_routes_empty(self, appconfig):
+ appconfig.enable_celery_tasks = True
+ appconfig.celery_conf["task_routes"] = None
+ assert appconfig.is_fetch_with_celery_enabled()
+
+ def test_enabled_if_no_route_key(self, appconfig):
+ appconfig.enable_celery_tasks = True
+ appconfig.celery_conf["task_routes"] = {"some-other-route": 1}
+ assert appconfig.is_fetch_with_celery_enabled()
+
+ def test_enabled_if_no_route(self, appconfig):
+ appconfig.enable_celery_tasks = True
+ appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = None
+ assert appconfig.is_fetch_with_celery_enabled()
+
+ def test_enabled_if_has_route(self, appconfig):
+ appconfig.enable_celery_tasks = True
+ appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = "my_route"
+ assert appconfig.is_fetch_with_celery_enabled()
+
+ def test_disabled_if_disabled_flag(self, appconfig):
+ appconfig.enable_celery_tasks = True
+ appconfig.celery_conf["task_routes"]["galaxy.fetch_data"] = config.DISABLED_FLAG
+ assert not appconfig.is_fetch_with_celery_enabled()