Merge pull request #15210 from ahmedhamidawan/filter_inputs_in_history

Filter Inputs and Outputs in HistoryPanel
This commit is contained in:
Dannon
2023-01-17 11:57:32 -05:00
committed by GitHub
12 changed files with 448 additions and 113 deletions
@@ -14,27 +14,34 @@
<icon v-if="selected" fixed-width size="lg" :icon="['far', 'check-square']" />
<icon v-else fixed-width size="lg" :icon="['far', 'square']" />
</b-button>
<span v-if="highlight == 'input'" v-b-tooltip.hover title="Input" @click.stop="toggleHighlights">
<b-button
v-if="highlight == 'input'"
v-b-tooltip.hover
variant="link"
class="p-0"
title="Input"
@click.stop="toggleHighlights">
<font-awesome-icon class="text-info" icon="arrow-circle-up" />
</span>
<span
v-else-if="highlight == 'noInputs'"
</b-button>
<b-button
v-else-if="highlight == 'active'"
v-b-tooltip.hover
title="No Inputs for this item"
tabindex="0"
@click.stop="toggleHighlights"
@keypress="toggleHighlights">
<font-awesome-icon icon="minus-circle" />
</span>
<span
v-else-if="highlight == 'output'"
v-b-tooltip.hover
title="Inputs highlighted for this item"
tabindex="0"
variant="link"
class="p-0"
title="Inputs/Outputs highlighted for this item"
@click.stop="toggleHighlights"
@keypress="toggleHighlights">
<font-awesome-icon icon="check-circle" />
</span>
</b-button>
<b-button
v-else-if="highlight == 'output'"
v-b-tooltip.hover
variant="link"
class="p-0"
title="Output"
@click.stop="toggleHighlights">
<font-awesome-icon class="text-info" icon="arrow-circle-down" />
</b-button>
<span v-if="hasStateIcon" class="state-icon">
<icon fixed-width :icon="contentState.icon" :spin="contentState.spin" />
</span>
@@ -86,7 +93,7 @@
v-if="expandDataset"
:dataset="item"
:writable="writable"
:show-highlight="isHistoryItem"
:show-highlight="isHistoryItem && filterable"
:item-urls="itemUrls"
@edit="onEdit"
@toggleHighlights="toggleHighlights" />
@@ -104,11 +111,10 @@ import { updateContentFields } from "components/History/model/queries";
import { JobStateSummary } from "./Collection/JobStateSummary";
import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { faArrowCircleUp, faMinusCircle, faCheckCircle } from "@fortawesome/free-solid-svg-icons";
import { faArrowCircleUp, faArrowCircleDown, faCheckCircle } from "@fortawesome/free-solid-svg-icons";
import { useEntryPointStore } from "stores/entryPointStore";
library.add(faArrowCircleUp, faMinusCircle, faCheckCircle);
library.add(faArrowCircleUp, faArrowCircleDown, faCheckCircle);
export default {
components: {
CollectionDescription,
@@ -1,67 +0,0 @@
/**
* Specifies highlighted items in the history listing. The `highlight` property is passed to
* the content item component and can be used to modify its appearance.
* TO DO: Consider case where parameter history is different and hence inputs cannot be seen
* in the current panel.
*/
import axios from "axios";
import { prependPath } from "utils/redirect";
import { deepeach } from "utils/utils";
import { LastQueue } from "utils/promise-queue";
// add promise queue
const lastQueue = new LastQueue(300);
/** Local cache for parameter requests */
const paramStash = new Map();
/** Performs request to obtain dataset parameters */
async function getDatasetParameters(datasetId, jobId) {
if (!paramStash.has(datasetId)) {
const url = jobId
? `api/jobs/${jobId}/parameters_display`
: `api/datasets/${datasetId}/parameters_display?hda_ldda=hda`;
const { data } = await lastQueue.enqueue(axios.get, prependPath(url));
paramStash.set(datasetId, data);
}
return paramStash.get(datasetId);
}
/** Returns item key */
function getKey(details) {
if (details.id && details.src) {
const historyContentType = details.src == "hda" ? "dataset" : "dataset_collection";
return `${details.id}-${historyContentType}`;
}
return null;
}
/** Returns highlighting details */
export async function getHighlights(item, itemKey) {
const highlights = {};
const { outputs, parameters } = await getDatasetParameters(item.id, item.job_source_id);
deepeach(parameters, (details) => {
const key = getKey(details);
if (key) {
highlights[key] = "input";
}
});
deepeach(outputs, (details) => {
const key = getKey(details);
if (key) {
// some other item created this item (e.g.: inheritance)
if (key != itemKey) {
highlights[itemKey] = "output";
highlights[key] = "input";
} else {
highlights[key] = "output";
}
}
});
// highlights only has item itself as an output (i.e.: no inputs)
if (highlights[itemKey] === "output" && Object.keys(highlights).length == 1) {
highlights[itemKey] = "noInputs";
}
// TO DO: Consider case where a job created multiple items (all highlights are outputs)
return highlights;
}
@@ -46,6 +46,8 @@
<small class="mt-1">Filter by state:</small>
<b-form-input v-model="filterSettings['state:']" size="sm" placeholder="any state" list="stateSelect" />
<b-form-datalist id="stateSelect" :options="states"></b-form-datalist>
<small class="mt-1">Filter by related to item index:</small>
<b-form-input v-model="filterSettings['related:']" size="sm" placeholder="index equals" />
<small class="mt-1">Filter by item index:</small>
<b-form-group class="m-0">
<b-input-group>
@@ -144,7 +144,6 @@ import { useHistoryItemsStore } from "stores/history/historyItemsStore";
import LoadingSpan from "components/LoadingSpan";
import ContentItem from "components/History/Content/ContentItem";
import { deleteContent, updateContentFields } from "components/History/model/queries";
import { getHighlights } from "components/History/Content/model/highlights";
import ExpandedItems from "components/History/Content/ExpandedItems";
import SelectedItems from "components/History/Content/SelectedItems";
import ListingLayout from "components/History/Layout/ListingLayout";
@@ -193,7 +192,6 @@ export default {
return {
error: null,
filterText: "",
highlights: {},
highlightsKey: null,
invisible: {},
loading: false,
@@ -255,13 +253,11 @@ export default {
queryKey() {
this.invisible = {};
this.offset = 0;
this.resetHighlights();
this.loadHistoryItems();
},
historyId(newVal, oldVal) {
if (newVal !== oldVal) {
this.operationRunning = null;
this.resetHighlights();
}
},
filter(newVal) {
@@ -281,10 +277,19 @@ export default {
...vuexMapActions("history", ["loadHistoryById"]),
...mapActions(useHistoryItemsStore, ["fetchHistoryItems"]),
getHighlight(item) {
return this.highlights[this.getItemKey(item)];
},
getItemKey(item) {
return `${item.id}-${item.history_content_type}`;
if (this.filterText.includes("related:" + item.hid)) {
this.highlightsKey = item.hid;
return "active";
} else if (this.filterText.includes("related:") && this.highlightsKey) {
if (item.hid > this.highlightsKey) {
return "output";
} else {
return "input";
}
} else {
this.highlightsKey = null;
return null;
}
},
hasMatches(items) {
return !!items && items.length > 0;
@@ -344,19 +349,13 @@ export default {
console.debug("HistoryPanel - Operation error.", error);
this.operationError = error;
},
async toggleHighlights(item) {
const key = this.getItemKey(item);
if (this.highlightsKey != key) {
this.highlightsKey = key;
this.highlights = await getHighlights(item, key);
toggleHighlights(item) {
if (this.filterText == "related:" + item.hid) {
this.filterText = "";
} else {
this.resetHighlights();
this.filterText = "related:" + item.hid;
}
},
resetHighlights() {
this.highlights = {};
this.highlightsKey = null;
},
onDragEnter(e) {
this.dragTarget = e.target;
this.showDropZone = true;
@@ -9,6 +9,7 @@ export const validFilters = {
hid_gt: compare("hid", "gt"),
hid_le: compare("hid", "le"),
hid_lt: compare("hid", "lt"),
related: equals("related"),
tag: contains("tags", "tag", expandNameTag),
visible: equals("visible", "visible", toBool),
deleted: equals("deleted", "deleted", toBool),
+15 -4
View File
@@ -4,7 +4,7 @@
*/
import { defineStore } from "pinia";
import Vue from "vue";
import { reverse } from "lodash";
import { LastQueue } from "utils/promise-queue";
import { urlData } from "utils/url";
@@ -21,13 +21,15 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
latestCreateTime: new Date(),
totalMatchesCount: undefined,
lastCheckedTime: new Date(),
relatedItems: {},
isWatching: false,
}),
getters: {
getHistoryItems: (state) => {
return (historyId, filterText) => {
const itemArray = state.items[historyId] || [];
const filters = HistoryFilters.getFilters(filterText);
const filters = HistoryFilters.getFilters(filterText).filter((filter) => !filter.includes("related"));
const relatedHid = HistoryFilters.getFilterValue(filterText, "related");
const filtered = itemArray.filter((item) => {
if (!item) {
return false;
@@ -35,6 +37,10 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
if (!HistoryFilters.testFilters(filters, item)) {
return false;
}
const relationKey = `${historyId}-${relatedHid}-${item.hid}`;
if (relatedHid && !state.relatedItems[relationKey]) {
return false;
}
return true;
});
return reverse(filtered);
@@ -63,11 +69,12 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
const stats = data.stats;
this.totalMatchesCount = stats.total_matches;
const payload = data.contents;
this.saveHistoryItems(historyId, payload);
const relatedHid = HistoryFilters.getFilterValue(filterText, "related");
this.saveHistoryItems(historyId, payload, relatedHid);
});
},
// Setters
saveHistoryItems(historyId, payload) {
saveHistoryItems(historyId, payload, relatedHid = null) {
this.$patch((state) => {
// merges incoming payload into existing state
mergeArray(historyId, payload, state.items, state.itemKey);
@@ -79,6 +86,10 @@ export const useHistoryItemsStore = defineStore("historyItemsStore", {
state.latestCreateTime = itemCreateTime;
}
}
if (relatedHid) {
const relationKey = `${historyId}-${relatedHid}-${item.hid}`;
Vue.set(state.relatedItems, relationKey, true);
}
});
});
},
+11
View File
@@ -29,12 +29,23 @@ describe("filtering", () => {
expect(queryDict["name-contains"]).toBe("name of item");
});
test("parse check filter", () => {
expect(HistoryFilters.checkFilter(filterTexts[0], "name", "name of item")).toBe(true);
expect(HistoryFilters.checkFilter(filterTexts[0], "tag", "first")).toBe(true);
expect(HistoryFilters.checkFilter(filterTexts[0], "tag", "second")).toBe(false);
expect(HistoryFilters.checkFilter(filterTexts[0], "deleted", "false")).toBe(true);
expect(HistoryFilters.checkFilter(filterTexts[0], "visible", true)).toBe(true);
expect(HistoryFilters.checkFilter(filterTexts[0], "visible", "false")).toBe(false);
});
test("parse get filter value", () => {
expect(HistoryFilters.getFilterValue(filterTexts[0], "name")).toBe("name of item");
expect(HistoryFilters.getFilterValue(filterTexts[0], "hid", "gt")).toBe("10");
expect(HistoryFilters.getFilterValue(filterTexts[0], "hid", "lt")).toBe("100");
expect(HistoryFilters.getFilterValue(filterTexts[0], "tag")).toBe("first");
expect(HistoryFilters.getFilterValue(filterTexts[0], "deleted")).toBe("False");
expect(HistoryFilters.getFilterValue(filterTexts[0], "visible")).toBe("TRUE");
expect(HistoryFilters.getFilterValue(filterTexts[1], "hid", "gt")).toBe("10");
expect(HistoryFilters.getFilterValue(filterTexts[1], "create_time", "gt")).toBe("2021-01-01");
});
test("parse filter text as entries", () => {
filterTexts.forEach((filterText) => {
const filters = HistoryFilters.getFilters(filterText);
+35 -3
View File
@@ -18,6 +18,14 @@ const defaultValidAliases = [
["<", "_lt"],
];
const operatorForAlias: Record<string, string> = {
lt: "<",
le: "<=",
ge: ">=",
gt: ">",
eq: ":",
};
/** Converts user input to backend compatible date
* @param {string} value
* @returns {Number} seconds since epoch
@@ -61,6 +69,14 @@ export function expandNameTag(value: string | object): string {
return toLower(value);
}
/** Converts string alias to string operator, e.g.: 'gt' to '>'
* @param alias
* @returns Arithmetic operator, e.g.: '>'
* */
export function getOperatorForAlias(alias: string): string {
return operatorForAlias[alias];
}
type HandlerReturn<T> = {
attribute: string;
converter?: Converter<T>;
@@ -323,12 +339,28 @@ export default class Filtering<T> {
* @returns {Boolean} True if the filter is set to the given value
* */
checkFilter<T>(filterText: string, filterName: string, filterValue: T): boolean {
const re = new RegExp(`${filterName}:(\\S+)`);
const reMatch = re.exec(filterText);
const testValue = reMatch ? reMatch[1] : this.defaultFilters[filterName];
const testValue = this.getFilterValue(filterText, filterName);
return toLowerNoQuotes(testValue) === toLowerNoQuotes(filterValue);
}
/** Get the value of a particular filter from filterText.
* @param filterText Raw filter text string
* @param filterName Filter key to check
* @param [alias="eq"] String alias for filter operator, e.g.:"lt"
* @returns The filterValue for the filter
* */
getFilterValue(filterText: string, filterName: string, alias = "eq"): string | boolean {
const op = getOperatorForAlias(alias);
const reString = `${filterName}(?:${op}|[-|_]${alias}:)(?:'([^']*[^\\s']*)'|(\\S+))`;
const re = new RegExp(reString);
const reMatch = re.exec(filterText);
let filterVal = null;
if (reMatch) {
filterVal = reMatch[1] || reMatch[2];
}
return filterVal || this.defaultFilters[filterName];
}
/** Test if an item passes all filters.
* @param {Object} filters Parsed in key-value pairs from getFilters()
* @param {Object} item Item to test against the filters
+31
View File
@@ -2,6 +2,7 @@
Heterogenous lists/contents are difficult to query properly since unions are
not easily made.
"""
import json
import logging
from typing import (
Any,
@@ -41,6 +42,8 @@ from galaxy.managers import (
taggable,
tools,
)
from galaxy.managers.job_connections import JobConnectionsManager
from galaxy.schema import ValueFilterQueryParams
from galaxy.structured_app import MinimalManagerApp
from .base import (
parse_bool,
@@ -525,6 +528,30 @@ class HistoryContentsFilters(
# surprisingly (but ominously), this works for both content classes in the union that's filtered
model_class = model.HistoryDatasetAssociation
def parse_query_filters_with_relations(self, query_filters: ValueFilterQueryParams, history_id):
"""Parse query filters but consider case where related filter is included."""
if query_filters.q and query_filters.qv and "related-eq" in query_filters.q:
qv_index = query_filters.q.index("related-eq")
qv_hid = query_filters.qv[qv_index]
# Make new q and qv excluding related filter
new_q = [x for i, x in enumerate(query_filters.q) if i != qv_index]
new_qv = [x for i, x in enumerate(query_filters.qv) if i != qv_index]
# Get list of related item hids from job_connections manager
job_connections_manager = JobConnectionsManager(self.app.model.session)
related = job_connections_manager.get_related_hids(history_id, qv_hid)
# Make new query_filters with updated list of related hids for given hid
new_q.append("related-eq")
new_qv.append(json.dumps(related))
query_filters_with_relations = ValueFilterQueryParams(
q=new_q,
qv=new_qv,
)
return super().parse_query_filters(query_filters_with_relations)
return super().parse_query_filters(query_filters)
def _parse_orm_filter(self, attr, op, val):
# we need to use some manual/text/column fu here since some where clauses on the union don't work
@@ -538,6 +565,9 @@ class HistoryContentsFilters(
return sql.column("history_content_type") == val
raise_filter_err(attr, op, val, "bad op in filter")
if attr == "related" and op == "eq":
return sql.column("hid").in_(json.loads(val))
if attr == "type_id":
if op == "eq":
return sql.column("type_id") == val
@@ -595,6 +625,7 @@ class HistoryContentsFilters(
self.orm_filter_parsers.update(
{
"history_content_type": {"op": ("eq")},
"related": {"op": ("eq")},
"type_id": {"op": ("eq", "in"), "val": self.parse_type_id_list},
"hid": {"op": ("eq", "ge", "le", "gt", "lt"), "val": int},
# TODO: needs a different val parser - but no way to add to the above
+182
View File
@@ -0,0 +1,182 @@
from sqlalchemy import (
literal,
union,
)
from sqlalchemy.sql import (
expression,
select,
)
from galaxy import model
from galaxy.managers.base import get_class
from galaxy.model.scoped_session import galaxy_scoped_session
class JobConnectionsManager:
"""Get connections graph of inputs and outputs for given item"""
def __init__(self, sa_session: galaxy_scoped_session):
self.sa_session = sa_session
def get_connections_graph(self, id, src):
"""Get connections graph of inputs and outputs for given item id"""
if src == "HistoryDatasetAssociation":
output_selects = self.outputs_derived_from_input_hda(id)
input_selects = self.inputs_for_hda(id)
elif src == "HistoryDatasetCollectionAssociation":
output_selects = self.outputs_derived_from_input_hdca(id)
input_selects = self.inputs_for_hdca(id)
else:
raise Exception(f"Invalid item type {src}")
# Execute selects and return graph of inputs and outputs
result = {}
result["outputs"] = self._get_union_results(*output_selects)
result["inputs"] = self._get_union_results(*input_selects)
return result
def get_related_hids(self, history_id, hid):
"""Get connections graph of inputs and outputs for given item hid from the given history_id"""
# Get id(s) and src(s) for the given hid
items_by_hid = self.sa_session.execute(
select(model.HistoryDatasetAssociation.id, expression.literal("HistoryDatasetAssociation"))
.filter_by(history_id=history_id, hid=hid)
.union(
select(
model.HistoryDatasetCollectionAssociation.id,
expression.literal("HistoryDatasetCollectionAssociation"),
).filter_by(history_id=history_id, hid=hid)
)
).all()
result = [hid]
for item_data in items_by_hid:
graph = self.get_connections_graph(id=item_data[0], src=item_data[1])
# Add found related items' hids to result list
for val in graph["outputs"] + graph["inputs"]:
item_class = get_class(val["src"])
item = self.sa_session.query(item_class).get(val["id"])
result.append(item.hid)
return result
def _get_union_results(self, *selects):
result = []
for row in self.sa_session.execute(union(*selects)).all():
result.append({"src": row.src, "id": row.id})
return result
def outputs_derived_from_input_hda(self, input_hda_id):
hda_select = (
select(
[
literal("HistoryDatasetAssociation").label("src"),
model.JobToOutputDatasetAssociation.dataset_id.label("id"),
]
)
.join(
model.JobToInputDatasetAssociation,
model.JobToInputDatasetAssociation.job_id == model.JobToOutputDatasetAssociation.job_id,
)
.where(model.JobToInputDatasetAssociation.dataset_id == input_hda_id)
)
hdca_select = (
select(
[
literal("HistoryDatasetCollectionAssociation").label("src"),
model.JobToOutputDatasetCollectionAssociation.dataset_collection_id.label("id"),
]
)
.join(
model.JobToInputDatasetAssociation,
model.JobToInputDatasetAssociation.job_id == model.JobToOutputDatasetCollectionAssociation.job_id,
)
.where(model.JobToInputDatasetAssociation.dataset_id == input_hda_id)
)
return hda_select, hdca_select
def outputs_derived_from_input_hdca(self, input_hdca_id):
hda_select = (
select(
[
literal("HistoryDatasetAssociation").label("src"),
model.JobToOutputDatasetAssociation.dataset_id.label("id"),
]
)
.join(
model.JobToInputDatasetCollectionAssociation,
model.JobToInputDatasetCollectionAssociation.job_id == model.JobToOutputDatasetAssociation.job_id,
)
.where(model.JobToInputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
)
hdca_select = (
select(
[
literal("HistoryDatasetCollectionAssociation").label("src"),
model.JobToOutputDatasetCollectionAssociation.dataset_collection_id.label("id"),
]
)
.join(
model.JobToInputDatasetCollectionAssociation,
model.JobToInputDatasetCollectionAssociation.job_id
== model.JobToOutputDatasetCollectionAssociation.job_id,
)
.where(model.JobToInputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
)
return hda_select, hdca_select
def inputs_for_hda(self, input_hda_id):
input_hdas = (
select(
[
literal("HistoryDatasetAssociation").label("src"),
model.JobToInputDatasetAssociation.dataset_id.label("id"),
]
)
.join(
model.JobToOutputDatasetAssociation,
model.JobToOutputDatasetAssociation.job_id == model.JobToInputDatasetAssociation.job_id,
)
.where(model.JobToOutputDatasetAssociation.dataset_id == input_hda_id)
)
input_hdcas = (
select(
[
literal("HistoryDatasetCollectionAssociation").label("src"),
model.JobToInputDatasetCollectionAssociation.dataset_collection_id.label("id"),
]
)
.join(
model.JobToOutputDatasetAssociation,
model.JobToOutputDatasetAssociation.job_id == model.JobToInputDatasetCollectionAssociation.job_id,
)
.where(model.JobToOutputDatasetAssociation.dataset_id == input_hda_id)
)
return input_hdas, input_hdcas
def inputs_for_hdca(self, input_hdca_id):
input_hdas = (
select(
[
literal("HistoryDatasetAssociation").label("src"),
model.JobToInputDatasetAssociation.dataset_id.label("id"),
]
)
.join(
model.JobToOutputDatasetCollectionAssociation,
model.JobToOutputDatasetCollectionAssociation.job_id == model.JobToInputDatasetAssociation.job_id,
)
.where(model.JobToOutputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
)
input_hdcas = (
select(
[
literal("HistoryDatasetCollectionAssociation").label("src"),
model.JobToInputDatasetCollectionAssociation.dataset_collection_id.label("id"),
]
)
.join(
model.JobToOutputDatasetCollectionAssociation,
model.JobToOutputDatasetCollectionAssociation.job_id
== model.JobToInputDatasetCollectionAssociation.job_id,
)
.where(model.JobToOutputDatasetCollectionAssociation.dataset_collection_id == input_hdca_id)
)
return input_hdas, input_hdcas
@@ -939,7 +939,8 @@ class HistoriesContentsService(ServiceBase, ServesExportStores, ConsumesModelSto
Allows additional filtering of contents and custom serialization.
"""
history = self._get_history(trans, history_id)
filters = self.history_contents_filters.parse_query_filters(filter_query_params)
filters = self.history_contents_filters.parse_query_filters_with_relations(filter_query_params, history_id)
stats_requested = accept == HistoryContentsWithStatsResult.__accept_type__
if stats_requested and self.history_contents_filters.contains_non_orm_filter(filters):
@@ -0,0 +1,126 @@
import pytest
from sqlalchemy import union
from galaxy.managers.job_connections import JobConnectionsManager
from galaxy.model import (
HistoryDatasetAssociation,
HistoryDatasetCollectionAssociation,
Job,
)
from galaxy.model.scoped_session import galaxy_scoped_session
from galaxy.model.unittest_utils import GalaxyDataTestApp
@pytest.fixture
def sa_session():
return GalaxyDataTestApp().model.session
@pytest.fixture
def job_connections_manager(sa_session) -> JobConnectionsManager:
gm = JobConnectionsManager(sa_session)
return gm
# =============================================================================
def setup_connected_dataset(sa_session: galaxy_scoped_session):
center_hda = HistoryDatasetAssociation(sa_session=sa_session)
input_hda = HistoryDatasetAssociation(sa_session=sa_session)
input_hdca = HistoryDatasetCollectionAssociation()
output_hda = HistoryDatasetAssociation(sa_session=sa_session)
output_hdca = HistoryDatasetCollectionAssociation()
input_job = Job()
output_job = Job()
input_job.add_output_dataset("output_hda", center_hda)
input_job.add_input_dataset("input_hda", input_hda)
input_job.add_input_dataset_collection("input_hdca", input_hdca)
output_job.add_input_dataset("input_hda", center_hda)
output_job.add_output_dataset("output_hda", output_hda)
output_job.add_output_dataset_collection("output_hdca", output_hdca)
sa_session.add_all([center_hda, input_hda, input_hdca, output_hdca, input_job, output_job])
sa_session.flush()
expected_graph = {
"inputs": [
{"src": "HistoryDatasetAssociation", "id": input_hda.id},
{"src": "HistoryDatasetCollectionAssociation", "id": input_hdca.id},
],
"outputs": [
{"src": "HistoryDatasetAssociation", "id": output_hda.id},
{"src": "HistoryDatasetCollectionAssociation", "id": output_hdca.id},
],
}
return center_hda, expected_graph
def setup_connected_dataset_collection(sa_session: galaxy_scoped_session):
center_hdca = HistoryDatasetCollectionAssociation()
input_hda1 = HistoryDatasetAssociation(sa_session=sa_session)
input_hda2 = HistoryDatasetAssociation(sa_session=sa_session)
input_hdca = HistoryDatasetCollectionAssociation()
output_hda = HistoryDatasetAssociation(sa_session=sa_session)
output_hdca = HistoryDatasetCollectionAssociation()
input_job = Job()
output_job = Job()
input_job.add_output_dataset_collection("output_hdca", center_hdca)
input_job.add_input_dataset("input_hda", input_hda1)
input_job.add_input_dataset("input_hda", input_hda2)
input_job.add_input_dataset_collection("input_hdca", input_hdca)
output_job.add_input_dataset_collection("input_hdca", center_hdca)
output_job.add_output_dataset("output_hda", output_hda)
output_job.add_output_dataset_collection("output_hdca", output_hdca)
sa_session.add_all([center_hdca, input_hda1, input_hda2, input_hdca, output_hdca, input_job, output_job])
sa_session.flush()
expected_graph = {
"inputs": [
{"src": "HistoryDatasetAssociation", "id": input_hda1.id},
{"src": "HistoryDatasetAssociation", "id": input_hda2.id},
{"src": "HistoryDatasetCollectionAssociation", "id": input_hdca.id},
],
"outputs": [
{"src": "HistoryDatasetAssociation", "id": output_hda.id},
{"src": "HistoryDatasetCollectionAssociation", "id": output_hdca.id},
],
}
return center_hdca, expected_graph
# =============================================================================
def test_graph_manager_inputs_for_hda(job_connections_manager: JobConnectionsManager):
sa_session = job_connections_manager.sa_session
center_hda, expected_graph = setup_connected_dataset(sa_session)
s = job_connections_manager.inputs_for_hda(center_hda.id)
assert len(sa_session.execute(union(*s)).all()) == 2
def test_graph_manager_outputs_for_hda(job_connections_manager: JobConnectionsManager):
sa_session = job_connections_manager.sa_session
center_hda, expected_graph = setup_connected_dataset(sa_session)
s = job_connections_manager.outputs_derived_from_input_hda(center_hda.id)
assert len(sa_session.execute(union(*s)).all()) == 2
def test_graph_manager_inputs_for_hdca(job_connections_manager: JobConnectionsManager):
sa_session = job_connections_manager.sa_session
center_hdca, expected_graph = setup_connected_dataset_collection(sa_session)
s = job_connections_manager.inputs_for_hdca(center_hdca.id)
assert len(sa_session.execute(union(*s)).all()) == 3
def test_graph_manager_outputs_for_hdca(job_connections_manager: JobConnectionsManager):
sa_session = job_connections_manager.sa_session
center_hdca, expected_graph = setup_connected_dataset_collection(sa_session)
s = job_connections_manager.outputs_derived_from_input_hdca(center_hdca.id)
assert len(sa_session.execute(union(*s)).all()) == 2
def test_graph_manager_hda(job_connections_manager: JobConnectionsManager):
center_hda, expected_graph = setup_connected_dataset(job_connections_manager.sa_session)
assert job_connections_manager.get_connections_graph(center_hda.id, "HistoryDatasetAssociation") == expected_graph
def test_graph_manager_hdca(job_connections_manager: JobConnectionsManager):
center_hdca, expected_graph = setup_connected_dataset_collection(job_connections_manager.sa_session)
assert (
job_connections_manager.get_connections_graph(center_hdca.id, "HistoryDatasetCollectionAssociation")
== expected_graph
)