mirror of
https://github.com/chanzuckerberg/cellxgene.git
synced 2026-10-07 08:58:11 +08:00
add GET routes for expression data (#1387)
* add GET routes for expression data * fix comment typo
This commit is contained in:
+83
-92
@@ -1,4 +1,3 @@
|
|||||||
import _ from "lodash";
|
|
||||||
import * as globals from "../globals";
|
import * as globals from "../globals";
|
||||||
import { Universe, MatrixFBS } from "../util/stateManager";
|
import { Universe, MatrixFBS } from "../util/stateManager";
|
||||||
import * as Dataframe from "../util/dataframe";
|
import * as Dataframe from "../util/dataframe";
|
||||||
@@ -6,7 +5,7 @@ import {
|
|||||||
catchErrorsWrap,
|
catchErrorsWrap,
|
||||||
doJsonRequest,
|
doJsonRequest,
|
||||||
doBinaryRequest,
|
doBinaryRequest,
|
||||||
dispatchNetworkErrorMessageToUser
|
dispatchNetworkErrorMessageToUser,
|
||||||
} from "../util/actionHelpers";
|
} from "../util/actionHelpers";
|
||||||
import { PromiseLimit } from "../util/promiseLimit";
|
import { PromiseLimit } from "../util/promiseLimit";
|
||||||
import { requestReembed, reembedResetWorldToUniverse } from "./reembed";
|
import { requestReembed, reembedResetWorldToUniverse } from "./reembed";
|
||||||
@@ -19,23 +18,23 @@ function obsAnnotationFetchAndLoad(dispatch, schema) {
|
|||||||
const obsAnnotations = schema?.schema?.annotations?.obs ?? {};
|
const obsAnnotations = schema?.schema?.annotations?.obs ?? {};
|
||||||
const index = obsAnnotations.index ?? false;
|
const index = obsAnnotations.index ?? false;
|
||||||
const columns = (obsAnnotations.columns ?? []).filter(
|
const columns = (obsAnnotations.columns ?? []).filter(
|
||||||
col => col.name !== index
|
(col) => col.name !== index
|
||||||
);
|
);
|
||||||
|
|
||||||
const plimit = new PromiseLimit(4);
|
const plimit = new PromiseLimit(5);
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
columns.map(col =>
|
columns.map((col) =>
|
||||||
plimit.add(() => {
|
plimit.add(() => {
|
||||||
const path = `annotations/obs?annotation-name=${encodeURIComponent(
|
const path = `annotations/obs?annotation-name=${encodeURIComponent(
|
||||||
col.name
|
col.name
|
||||||
)}`;
|
)}`;
|
||||||
const url = `${globals.API.prefix}${globals.API.version}${path}`;
|
const url = `${globals.API.prefix}${globals.API.version}${path}`;
|
||||||
return doBinaryRequest(url).then(buffer => {
|
return doBinaryRequest(url).then((buffer) => {
|
||||||
const df = Universe.matrixFBSToDataframe(buffer);
|
const df = Universe.matrixFBSToDataframe(buffer);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "universe: column load success",
|
type: "universe: column load success",
|
||||||
dim: "obsAnnotations",
|
dim: "obsAnnotations",
|
||||||
dataframe: df
|
dataframe: df,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -52,20 +51,22 @@ function varAnnotationFetchAndLoad(dispatch, schema) {
|
|||||||
const names = index ? [index] : [];
|
const names = index ? [index] : [];
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
names
|
names
|
||||||
.map(name => {
|
.map((name) => {
|
||||||
const path = `annotations/var?annotation-name=${encodeURIComponent(
|
const path = `annotations/var?annotation-name=${encodeURIComponent(
|
||||||
name
|
name
|
||||||
)}`;
|
)}`;
|
||||||
const url = `${globals.API.prefix}${globals.API.version}${path}`;
|
const url = `${globals.API.prefix}${globals.API.version}${path}`;
|
||||||
return doBinaryRequest(url);
|
return doBinaryRequest(url);
|
||||||
})
|
})
|
||||||
.map(rqst => rqst.then(buffer => Universe.matrixFBSToDataframe(buffer)))
|
.map((rqst) =>
|
||||||
.map(resp =>
|
rqst.then((buffer) => Universe.matrixFBSToDataframe(buffer))
|
||||||
resp.then(df =>
|
)
|
||||||
|
.map((resp) =>
|
||||||
|
resp.then((df) =>
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "universe: column load success",
|
type: "universe: column load success",
|
||||||
dim: "varAnnotations",
|
dim: "varAnnotations",
|
||||||
dataframe: df
|
dataframe: df,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -77,25 +78,25 @@ return promise fetching layout we need
|
|||||||
*/
|
*/
|
||||||
function layoutFetchAndLoad(dispatch, schema) {
|
function layoutFetchAndLoad(dispatch, schema) {
|
||||||
const embeddings = schema?.schema?.layout?.obs ?? [];
|
const embeddings = schema?.schema?.layout?.obs ?? [];
|
||||||
const embNames = embeddings.map(e => e.name);
|
const embNames = embeddings.map((e) => e.name);
|
||||||
const baseURL = `${globals.API.prefix}${globals.API.version}layout/obs`;
|
const baseURL = `${globals.API.prefix}${globals.API.version}layout/obs`;
|
||||||
|
|
||||||
const plimit = new PromiseLimit(4);
|
const plimit = new PromiseLimit(5);
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
embNames.map(e =>
|
embNames.map((e) =>
|
||||||
plimit.add(() => {
|
plimit.add(() => {
|
||||||
const url = `${baseURL}?layout-name=${encodeURIComponent(e)}`;
|
const url = `${baseURL}?layout-name=${encodeURIComponent(e)}`;
|
||||||
return doBinaryRequest(url).then(buffer =>
|
return doBinaryRequest(url).then((buffer) =>
|
||||||
Universe.matrixFBSToDataframe(buffer)
|
Universe.matrixFBSToDataframe(buffer)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).then(dfs => {
|
).then((dfs) => {
|
||||||
const df = Dataframe.Dataframe.empty().withColsFromAll(dfs);
|
const df = Dataframe.Dataframe.empty().withColsFromAll(dfs);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "universe: column load success",
|
type: "universe: column load success",
|
||||||
dim: "obsLayout",
|
dim: "obsLayout",
|
||||||
dataframe: df
|
dataframe: df,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ Bootstrap application with the initial data loading.
|
|||||||
* /layout - all default layout
|
* /layout - all default layout
|
||||||
*/
|
*/
|
||||||
const doInitialDataLoad = () =>
|
const doInitialDataLoad = () =>
|
||||||
catchErrorsWrap(async dispatch => {
|
catchErrorsWrap(async (dispatch) => {
|
||||||
dispatch({ type: "initial data load start" });
|
dispatch({ type: "initial data load start" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -116,8 +117,8 @@ const doInitialDataLoad = () =>
|
|||||||
Step 1 - config & schema, all JSON
|
Step 1 - config & schema, all JSON
|
||||||
*/
|
*/
|
||||||
const requestJson = ["config", "schema"]
|
const requestJson = ["config", "schema"]
|
||||||
.map(r => `${globals.API.prefix}${globals.API.version}${r}`)
|
.map((r) => `${globals.API.prefix}${globals.API.version}${r}`)
|
||||||
.map(url => doJsonRequest(url));
|
.map((url) => doJsonRequest(url));
|
||||||
const stepOneResults = await Promise.all(requestJson);
|
const stepOneResults = await Promise.all(requestJson);
|
||||||
/* set config defaults */
|
/* set config defaults */
|
||||||
const config = { ...globals.configDefaults, ...stepOneResults[0].config };
|
const config = { ...globals.configDefaults, ...stepOneResults[0].config };
|
||||||
@@ -125,11 +126,11 @@ const doInitialDataLoad = () =>
|
|||||||
const universe = Universe.createUniverseFromResponse(config, schema);
|
const universe = Universe.createUniverseFromResponse(config, schema);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "universe exists, but loading is still in progress",
|
type: "universe exists, but loading is still in progress",
|
||||||
universe
|
universe,
|
||||||
});
|
});
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "configuration load complete",
|
type: "configuration load complete",
|
||||||
config
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -137,7 +138,7 @@ const doInitialDataLoad = () =>
|
|||||||
*/
|
*/
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
layoutFetchAndLoad(dispatch, schema),
|
layoutFetchAndLoad(dispatch, schema),
|
||||||
varAnnotationFetchAndLoad(dispatch, schema)
|
varAnnotationFetchAndLoad(dispatch, schema),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -147,7 +148,7 @@ const doInitialDataLoad = () =>
|
|||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "initial data load complete (universe exists)",
|
type: "initial data load complete (universe exists)",
|
||||||
universe
|
universe,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: "initial data load error", error });
|
dispatch({ type: "initial data load error", error });
|
||||||
@@ -164,7 +165,7 @@ const setWorldToSelection = () => (dispatch, getState) => {
|
|||||||
type: "set World to current selection",
|
type: "set World to current selection",
|
||||||
universe,
|
universe,
|
||||||
world,
|
world,
|
||||||
crossfilter
|
crossfilter,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,6 +176,11 @@ const dispatchExpressionErrors = (dispatch, res) => {
|
|||||||
throw new Error(msg);
|
throw new Error(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* double URI encode - needed for query-param filters */
|
||||||
|
function dubEncURIComponent(s) {
|
||||||
|
return encodeURIComponent(encodeURIComponent(s));
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Fetch expression vectors for each gene in genes. This is NOT an action
|
Fetch expression vectors for each gene in genes. This is NOT an action
|
||||||
function, but rather a helper to be called from an action helper that
|
function, but rather a helper to be called from an action helper that
|
||||||
@@ -188,51 +194,31 @@ async function _doRequestExpressionData(dispatch, getState, genes) {
|
|||||||
const varIndexName = universe.schema.annotations.var.index;
|
const varIndexName = universe.schema.annotations.var.index;
|
||||||
|
|
||||||
/* helper for this function only */
|
/* helper for this function only */
|
||||||
const fetchData = async geneNames => {
|
const fetchData = async (geneNames) => {
|
||||||
const res = await fetch(
|
const query = geneNames
|
||||||
`${globals.API.prefix}${globals.API.version}data/var`,
|
.map(
|
||||||
{
|
(g) =>
|
||||||
method: "PUT",
|
`var:${dubEncURIComponent(varIndexName)}=${dubEncURIComponent(g)}`
|
||||||
body: JSON.stringify({
|
)
|
||||||
filter: {
|
.join("&");
|
||||||
var: {
|
const url = `${globals.API.prefix}${globals.API.version}data/var?${query}`;
|
||||||
annotation_value: [{ name: varIndexName, values: geneNames }]
|
return doBinaryRequest(url).then((buffer) =>
|
||||||
}
|
// TODO: why convert to an Object and not a Dataframe?
|
||||||
}
|
Universe.convertDataFBStoObject(universe, buffer)
|
||||||
}),
|
|
||||||
headers: new Headers({
|
|
||||||
accept: "application/octet-stream",
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}),
|
|
||||||
credentials: "include"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
res.headers.get("Content-Type") !== "application/octet-stream"
|
|
||||||
) {
|
|
||||||
// WILL throw
|
|
||||||
return dispatchExpressionErrors(dispatch, res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.arrayBuffer();
|
|
||||||
return Universe.convertDataFBStoObject(universe, data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* preload data already in cache */
|
/* preload data already in cache */
|
||||||
let expressionData = _.transform(
|
let expressionData = genes.reduce((acc, g) => {
|
||||||
genes,
|
const data = universe.varData.col(g);
|
||||||
(expData, g) => {
|
if (data) {
|
||||||
const data = universe.varData.col(g);
|
acc[g] = data.asArray();
|
||||||
if (data) {
|
}
|
||||||
expData[g] = data.asArray();
|
return acc;
|
||||||
}
|
}, {}); // --> { gene: data }
|
||||||
},
|
|
||||||
{}
|
|
||||||
); // --> { gene: data }
|
|
||||||
/* make a list of genes for which we do not have data */
|
/* make a list of genes for which we do not have data */
|
||||||
const genesToFetch = _.filter(genes, g => expressionData[g] === undefined);
|
const genesToFetch = genes.filter((g) => expressionData[g] === undefined);
|
||||||
|
|
||||||
dispatch({ type: "expression load start" });
|
dispatch({ type: "expression load start" });
|
||||||
|
|
||||||
@@ -242,7 +228,7 @@ async function _doRequestExpressionData(dispatch, getState, genes) {
|
|||||||
const newExpressionData = await fetchData(genesToFetch);
|
const newExpressionData = await fetchData(genesToFetch);
|
||||||
expressionData = {
|
expressionData = {
|
||||||
...expressionData,
|
...expressionData,
|
||||||
...newExpressionData
|
...newExpressionData,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({ type: "expression load error", error });
|
dispatch({ type: "expression load error", error });
|
||||||
@@ -264,19 +250,19 @@ function requestSingleGeneExpressionCountsForColoringPOST(gene) {
|
|||||||
type: "color by expression",
|
type: "color by expression",
|
||||||
gene,
|
gene,
|
||||||
data: {
|
data: {
|
||||||
[gene]: world.varData.col(gene).asArray()
|
[gene]: world.varData.col(gene).asArray(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "get single gene expression for coloring error",
|
type: "get single gene expression for coloring error",
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestUserDefinedGene = gene => async (dispatch, getState) => {
|
const requestUserDefinedGene = (gene) => async (dispatch, getState) => {
|
||||||
dispatch({ type: "request user defined gene started" });
|
dispatch({ type: "request user defined gene started" });
|
||||||
try {
|
try {
|
||||||
await await _doRequestExpressionData(dispatch, getState, [gene]);
|
await await _doRequestExpressionData(dispatch, getState, [gene]);
|
||||||
@@ -287,13 +273,13 @@ const requestUserDefinedGene = gene => async (dispatch, getState) => {
|
|||||||
type: "request user defined gene success",
|
type: "request user defined gene success",
|
||||||
data: {
|
data: {
|
||||||
genes: [gene],
|
genes: [gene],
|
||||||
expression: world.varData.col(gene).asArray()
|
expression: world.varData.col(gene).asArray(),
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: "request user defined gene error",
|
type: "request user defined gene error",
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -315,7 +301,7 @@ const dispatchDiffExpErrors = (dispatch, response) => {
|
|||||||
dispatchNetworkErrorMessageToUser(msg);
|
dispatchNetworkErrorMessageToUser(msg);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "request differential expression error",
|
type: "request differential expression error",
|
||||||
error: new Error(msg)
|
error: new Error(msg),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,15 +339,15 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
}),
|
}),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
mode: "topN",
|
mode: "topN",
|
||||||
count: num_genes,
|
count: num_genes,
|
||||||
set1: { filter: { obs: { index: set1 } } },
|
set1: { filter: { obs: { index: set1 } } },
|
||||||
set2: { filter: { obs: { index: set2 } } }
|
set2: { filter: { obs: { index: set2 } } },
|
||||||
}),
|
}),
|
||||||
credentials: "include"
|
credentials: "include",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -371,7 +357,7 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// result is [ [varIdx, ...], ... ]
|
// result is [ [varIdx, ...], ... ]
|
||||||
const topNGenes = _.map(data, r =>
|
const topNGenes = data.map((r) =>
|
||||||
universe.varAnnotations.at(r[0], varIndexName)
|
universe.varAnnotations.at(r[0], varIndexName)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -379,17 +365,22 @@ const requestDifferentialExpression = (set1, set2, num_genes = 10) => async (
|
|||||||
Kick off secondary action to fetch all of the expression data for the
|
Kick off secondary action to fetch all of the expression data for the
|
||||||
topN expressed genes.
|
topN expressed genes.
|
||||||
*/
|
*/
|
||||||
await _doRequestExpressionData(dispatch, getState, topNGenes);
|
const plimit = new PromiseLimit(5);
|
||||||
|
await Promise.all(
|
||||||
|
topNGenes.map((gene) =>
|
||||||
|
plimit.add(() => _doRequestExpressionData(dispatch, getState, [gene]))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
/* then send the success case action through */
|
/* then send the success case action through */
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: "request differential expression success",
|
type: "request differential expression success",
|
||||||
data
|
data,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: "request differential expression error",
|
type: "request differential expression error",
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -399,7 +390,7 @@ const resetWorldToUniverse = () => (dispatch, getState) => {
|
|||||||
reembedResetWorldToUniverse(dispatch, getState);
|
reembedResetWorldToUniverse(dispatch, getState);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "reset World to eq Universe",
|
type: "reset World to eq Universe",
|
||||||
universe
|
universe,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,12 +400,12 @@ const saveObsAnnotations = () => async (dispatch, getState) => {
|
|||||||
const { dataCollectionNameIsReadOnly, dataCollectionName } = annotations;
|
const { dataCollectionNameIsReadOnly, dataCollectionName } = annotations;
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "writable obs annotations - save started"
|
type: "writable obs annotations - save started",
|
||||||
});
|
});
|
||||||
|
|
||||||
const writableAnnotations = schema.annotations.obs.columns
|
const writableAnnotations = schema.annotations.obs.columns
|
||||||
.filter(s => s.writable)
|
.filter((s) => s.writable)
|
||||||
.map(s => s.name);
|
.map((s) => s.name);
|
||||||
const df = obsAnnotations.subset(null, writableAnnotations);
|
const df = obsAnnotations.subset(null, writableAnnotations);
|
||||||
const matrix = MatrixFBS.encodeMatrixFBS(df);
|
const matrix = MatrixFBS.encodeMatrixFBS(df);
|
||||||
try {
|
try {
|
||||||
@@ -430,28 +421,28 @@ const saveObsAnnotations = () => async (dispatch, getState) => {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: matrix,
|
body: matrix,
|
||||||
headers: new Headers({
|
headers: new Headers({
|
||||||
"Content-Type": "application/octet-stream"
|
"Content-Type": "application/octet-stream",
|
||||||
}),
|
}),
|
||||||
credentials: "include"
|
credentials: "include",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "writable obs annotations - save complete",
|
type: "writable obs annotations - save complete",
|
||||||
obsAnnotations
|
obsAnnotations,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "writable obs annotations - save error",
|
type: "writable obs annotations - save error",
|
||||||
message: `HTTP error ${res.status} - ${res.statusText}`,
|
message: `HTTP error ${res.status} - ${res.statusText}`,
|
||||||
res
|
res,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "writable obs annotations - save error",
|
type: "writable obs annotations - save error",
|
||||||
message: error.toString(),
|
message: error.toString(),
|
||||||
error
|
error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -464,5 +455,5 @@ export default {
|
|||||||
requestReembed,
|
requestReembed,
|
||||||
resetWorldToUniverse,
|
resetWorldToUniverse,
|
||||||
saveObsAnnotations,
|
saveObsAnnotations,
|
||||||
setWorldToSelection
|
setWorldToSelection,
|
||||||
};
|
};
|
||||||
|
|||||||
Vendored
+27
-26
@@ -20,8 +20,6 @@ const Controls = (
|
|||||||
scatterplotXXaccessor: null, // just easier to read
|
scatterplotXXaccessor: null, // just easier to read
|
||||||
scatterplotYYaccessor: null,
|
scatterplotYYaccessor: null,
|
||||||
graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */,
|
graphRenderCounter: 0 /* integer as <Component key={graphRenderCounter} - a change in key forces a remount */,
|
||||||
__storedStateForCelllist1__: null /* will need procedural control of brush ie., brush.extent https://bl.ocks.org/micahstubbs/3cda05ca68cba260cb81 */,
|
|
||||||
__storedStateForCelllist2__: null
|
|
||||||
},
|
},
|
||||||
action,
|
action,
|
||||||
nextSharedState,
|
nextSharedState,
|
||||||
@@ -62,7 +60,7 @@ const Controls = (
|
|||||||
universeExists &&
|
universeExists &&
|
||||||
embeddingsExist &&
|
embeddingsExist &&
|
||||||
varAnnotationsIndexExists
|
varAnnotationsIndexExists
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "initial data load complete (universe exists)": {
|
case "initial data load complete (universe exists)": {
|
||||||
@@ -71,38 +69,42 @@ const Controls = (
|
|||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
resettingInterface: false
|
resettingInterface: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "reset World to eq Universe": {
|
case "reset World to eq Universe": {
|
||||||
const [ newUserDefinedGenes, newDiffExpGenes ] = subsetAndResetGeneLists(state);
|
const [newUserDefinedGenes, newDiffExpGenes] = subsetAndResetGeneLists(
|
||||||
|
state
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
resettingInterface: false,
|
resettingInterface: false,
|
||||||
userDefinedGenes: newUserDefinedGenes,
|
userDefinedGenes: newUserDefinedGenes,
|
||||||
diffexpGenes: newDiffExpGenes
|
diffexpGenes: newDiffExpGenes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "set World to current selection": {
|
case "set World to current selection": {
|
||||||
const [ newUserDefinedGenes, newDiffExpGenes ] = subsetAndResetGeneLists(state);
|
const [newUserDefinedGenes, newDiffExpGenes] = subsetAndResetGeneLists(
|
||||||
|
state
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null,
|
error: null,
|
||||||
userDefinedGenes: newUserDefinedGenes,
|
userDefinedGenes: newUserDefinedGenes,
|
||||||
diffexpGenes: newDiffExpGenes
|
diffexpGenes: newDiffExpGenes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "request user defined gene started": {
|
case "request user defined gene started": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userDefinedGenesLoading: true
|
userDefinedGenesLoading: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "request user defined gene error": {
|
case "request user defined gene error": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userDefinedGenesLoading: false
|
userDefinedGenesLoading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "request user defined gene success": {
|
case "request user defined gene success": {
|
||||||
@@ -113,50 +115,49 @@ const Controls = (
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userDefinedGenes: _userDefinedGenes,
|
userDefinedGenes: _userDefinedGenes,
|
||||||
userDefinedGenesLoading: false
|
userDefinedGenesLoading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "request differential expression success": {
|
case "request differential expression success": {
|
||||||
const { world } = prevSharedState;
|
const { world } = prevSharedState;
|
||||||
const varIndexName = world.schema.annotations.var.index;
|
const varIndexName = world.schema.annotations.var.index;
|
||||||
const _diffexpGenes = [];
|
const _diffexpGenes = [];
|
||||||
action.data.forEach(d => {
|
action.data.forEach((d) => {
|
||||||
_diffexpGenes.push(world.varAnnotations.at(d[0], varIndexName));
|
_diffexpGenes.push(world.varAnnotations.at(d[0], varIndexName));
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
diffexpGenes: _diffexpGenes
|
diffexpGenes: _diffexpGenes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "clear differential expression": {
|
case "clear differential expression": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
diffexpGenes: []
|
diffexpGenes: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "clear user defined gene": {
|
case "clear user defined gene": {
|
||||||
const { userDefinedGenes } = state;
|
const { userDefinedGenes } = state;
|
||||||
const newUserDefinedGenes = _.filter(
|
const newUserDefinedGenes = _.filter(
|
||||||
userDefinedGenes,
|
userDefinedGenes,
|
||||||
d => d !== action.data
|
(d) => d !== action.data
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userDefinedGenes: newUserDefinedGenes
|
userDefinedGenes: newUserDefinedGenes,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "clear all user defined genes": {
|
case "clear all user defined genes": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
userDefinedGenes: []
|
userDefinedGenes: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "expression load error":
|
|
||||||
case "initial data load error": {
|
case "initial data load error": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: action.error
|
error: action.error,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,24 +167,24 @@ const Controls = (
|
|||||||
case "change graph interaction mode":
|
case "change graph interaction mode":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphInteractionMode: action.data
|
graphInteractionMode: action.data,
|
||||||
};
|
};
|
||||||
case "change opacity deselected cells in 2d graph background":
|
case "change opacity deselected cells in 2d graph background":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
opacityForDeselectedCells: action.data
|
opacityForDeselectedCells: action.data,
|
||||||
};
|
};
|
||||||
case "increment graph render counter": {
|
case "increment graph render counter": {
|
||||||
const c = state.graphRenderCounter + 1;
|
const c = state.graphRenderCounter + 1;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
graphRenderCounter: c
|
graphRenderCounter: c,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "interface reset started": {
|
case "interface reset started": {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
resettingInterface: true
|
resettingInterface: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,18 +194,18 @@ const Controls = (
|
|||||||
case "set scatterplot x":
|
case "set scatterplot x":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
scatterplotXXaccessor: action.data
|
scatterplotXXaccessor: action.data,
|
||||||
};
|
};
|
||||||
case "set scatterplot y":
|
case "set scatterplot y":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
scatterplotYYaccessor: action.data
|
scatterplotYYaccessor: action.data,
|
||||||
};
|
};
|
||||||
case "clear scatterplot":
|
case "clear scatterplot":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
scatterplotXXaccessor: null,
|
scatterplotXXaccessor: null,
|
||||||
scatterplotYYaccessor: null
|
scatterplotYYaccessor: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { postNetworkErrorToast } from "../components/framework/toasters";
|
|||||||
dispatch an action error to the user. Currently we use
|
dispatch an action error to the user. Currently we use
|
||||||
async toasts.
|
async toasts.
|
||||||
*/
|
*/
|
||||||
export const dispatchNetworkErrorMessageToUser = message =>
|
export const dispatchNetworkErrorMessageToUser = (message) =>
|
||||||
postNetworkErrorToast(message);
|
postNetworkErrorToast(message);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -14,7 +14,7 @@ Catch unexpected errors and make sure we don't lose them!
|
|||||||
*/
|
*/
|
||||||
export function catchErrorsWrap(fn, dispatchToUser = false) {
|
export function catchErrorsWrap(fn, dispatchToUser = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
fn(dispatch, getState).catch(error => {
|
fn(dispatch, getState).catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (dispatchToUser) {
|
if (dispatchToUser) {
|
||||||
dispatchNetworkErrorMessageToUser(error.message);
|
dispatchNetworkErrorMessageToUser(error.message);
|
||||||
@@ -29,30 +29,33 @@ Wrapper to perform async fetch with some modest error handling
|
|||||||
and decoding.
|
and decoding.
|
||||||
*/
|
*/
|
||||||
const doFetch = async (url, acceptType) => {
|
const doFetch = async (url, acceptType) => {
|
||||||
const res = await fetch(url, {
|
try {
|
||||||
method: "get",
|
const res = await fetch(url, {
|
||||||
headers: new Headers({
|
method: "get",
|
||||||
Accept: acceptType
|
headers: new Headers({
|
||||||
}),
|
Accept: acceptType,
|
||||||
credentials: "include"
|
}),
|
||||||
});
|
credentials: "include",
|
||||||
if (res.ok && res.headers.get("Content-Type").includes(acceptType)) {
|
});
|
||||||
return res;
|
if (res.ok && res.headers.get("Content-Type").includes(acceptType)) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
// else an error
|
||||||
|
const msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`;
|
||||||
|
dispatchNetworkErrorMessageToUser(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
} catch (e) {
|
||||||
|
// network error
|
||||||
|
const msg = "Unexpected HTTP error";
|
||||||
|
dispatchNetworkErrorMessageToUser(msg);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
// else an error
|
|
||||||
let msg = `Unexpected HTTP response ${res.status}, ${res.statusText}`;
|
|
||||||
const body = await res.text();
|
|
||||||
if (body && body.length > 0) {
|
|
||||||
msg = `${msg} -- ${body}`;
|
|
||||||
}
|
|
||||||
dispatchNetworkErrorMessageToUser(msg);
|
|
||||||
throw new Error(msg);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Wrapper to perform an async fetch and JSON decode response.
|
Wrapper to perform an async fetch and JSON decode response.
|
||||||
*/
|
*/
|
||||||
export const doJsonRequest = async url => {
|
export const doJsonRequest = async (url) => {
|
||||||
const res = await doFetch(url, "application/json");
|
const res = await doFetch(url, "application/json");
|
||||||
return res.json();
|
return res.json();
|
||||||
};
|
};
|
||||||
@@ -60,7 +63,7 @@ export const doJsonRequest = async url => {
|
|||||||
/*
|
/*
|
||||||
Wrapper to perform an async fetch for binary data.
|
Wrapper to perform an async fetch for binary data.
|
||||||
*/
|
*/
|
||||||
export const doBinaryRequest = async url => {
|
export const doBinaryRequest = async (url) => {
|
||||||
const res = await doFetch(url, "application/octet-stream");
|
const res = await doFetch(url, "application/octet-stream");
|
||||||
return res.arrayBuffer();
|
return res.arrayBuffer();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -198,6 +198,11 @@ class DataVarAPI(Resource):
|
|||||||
def put(self, data_adaptor):
|
def put(self, data_adaptor):
|
||||||
return common_rest.data_var_put(request, data_adaptor)
|
return common_rest.data_var_put(request, data_adaptor)
|
||||||
|
|
||||||
|
@cache_control(public=True, max_age=ONE_WEEK)
|
||||||
|
@rest_get_data_adaptor
|
||||||
|
def get(self, data_adaptor):
|
||||||
|
return common_rest.data_var_get(request, data_adaptor)
|
||||||
|
|
||||||
|
|
||||||
class DiffExpObsAPI(Resource):
|
class DiffExpObsAPI(Resource):
|
||||||
@cache_control(no_store=True)
|
@cache_control(no_store=True)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from http import HTTPStatus
|
|||||||
import copy
|
import copy
|
||||||
import logging
|
import logging
|
||||||
from flask import make_response, jsonify, current_app, abort
|
from flask import make_response, jsonify, current_app, abort
|
||||||
|
from werkzeug.urls import url_unquote
|
||||||
from server.common.constants import Axis, DiffExpMode, JSON_NaN_to_num_warning_msg
|
from server.common.constants import Axis, DiffExpMode, JSON_NaN_to_num_warning_msg
|
||||||
from server.common.errors import (
|
from server.common.errors import (
|
||||||
FilterError,
|
FilterError,
|
||||||
@@ -31,6 +32,70 @@ def abort_and_log(code, logmsg, loglevel=logging.DEBUG, include_exc_info=False):
|
|||||||
return abort(code)
|
return abort(code)
|
||||||
|
|
||||||
|
|
||||||
|
def _query_parameter_to_filter(args):
|
||||||
|
"""
|
||||||
|
Convert an annotation value filter, if present in the query args,
|
||||||
|
into the standard dict filter format used by internal code.
|
||||||
|
|
||||||
|
Query param filters look like: <axis>:name=value, where value
|
||||||
|
may be one of:
|
||||||
|
- a range, min,max, where either may be an open range by using an asterisc, eg, 10,*
|
||||||
|
- a value
|
||||||
|
Eg,
|
||||||
|
...?tissue=lung&obs:tissue=heart&obs:num_reads=1000,*
|
||||||
|
"""
|
||||||
|
filters = {
|
||||||
|
"obs": {},
|
||||||
|
"var": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# args has already been url-unquoted once. We assume double escaping
|
||||||
|
# on name and value.
|
||||||
|
try:
|
||||||
|
for key, value in args.items(multi=True):
|
||||||
|
axis, name = key.split(':')
|
||||||
|
if axis not in ("obs", "var"):
|
||||||
|
raise FilterError("unknown filter axis")
|
||||||
|
name = url_unquote(name)
|
||||||
|
current = filters[axis].setdefault(name, {"name": name})
|
||||||
|
|
||||||
|
val_split = value.split(',')
|
||||||
|
if len(val_split) == 1:
|
||||||
|
if 'min' in current or 'max' in current:
|
||||||
|
raise FilterError("do not mix range and value filters")
|
||||||
|
value = url_unquote(value)
|
||||||
|
values = current.setdefault("values", [])
|
||||||
|
values.append(value)
|
||||||
|
|
||||||
|
elif len(val_split) == 2:
|
||||||
|
if len(current) > 1:
|
||||||
|
raise FilterError("duplicate range specification")
|
||||||
|
min = url_unquote(val_split[0])
|
||||||
|
max = url_unquote(val_split[1])
|
||||||
|
if min != '*':
|
||||||
|
current["min"] = float(min)
|
||||||
|
if max != "*":
|
||||||
|
current["max"] = float(max)
|
||||||
|
if len(current) < 2:
|
||||||
|
raise FilterError("must specify at least min or max in range filter")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise FilterError("badly formated filter value")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
raise FilterError(str(e))
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for axis in ("obs", "var"):
|
||||||
|
axis_filter = filters[axis]
|
||||||
|
if len(axis_filter) > 0:
|
||||||
|
result[axis] = {
|
||||||
|
"annotation_value": [val for val in axis_filter.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def schema_get_helper(data_adaptor, annotations):
|
def schema_get_helper(data_adaptor, annotations):
|
||||||
"""helper function to gather the schema from the data source and annotations"""
|
"""helper function to gather the schema from the data source and annotations"""
|
||||||
schema = data_adaptor.get_schema()
|
schema = data_adaptor.get_schema()
|
||||||
@@ -143,6 +208,22 @@ def data_var_put(request, data_adaptor):
|
|||||||
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
|
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def data_var_get(request, data_adaptor):
|
||||||
|
preferred_mimetype = request.accept_mimetypes.best_match(["application/octet-stream"])
|
||||||
|
if preferred_mimetype != "application/octet-stream":
|
||||||
|
return abort(HTTPStatus.NOT_ACCEPTABLE)
|
||||||
|
|
||||||
|
try:
|
||||||
|
filter = _query_parameter_to_filter(request.args)
|
||||||
|
return make_response(
|
||||||
|
data_adaptor.data_frame_to_fbs_matrix(filter, axis=Axis.VAR),
|
||||||
|
HTTPStatus.OK,
|
||||||
|
{"Content-Type": "application/octet-stream"},
|
||||||
|
)
|
||||||
|
except (FilterError, ValueError, ExceedsLimitError) as e:
|
||||||
|
return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
def diffexp_obs_post(request, data_adaptor):
|
def diffexp_obs_post(request, data_adaptor):
|
||||||
if not data_adaptor.config.diffexp__enable:
|
if not data_adaptor.config.diffexp__enable:
|
||||||
return abort(HTTPStatus.NOT_IMPLEMENTED)
|
return abort(HTTPStatus.NOT_IMPLEMENTED)
|
||||||
|
|||||||
@@ -206,6 +206,13 @@ class EndPoints(object):
|
|||||||
result = self.session.put(url, headers=header)
|
result = self.session.put(url, headers=header)
|
||||||
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
|
def test_data_get_fbs(self):
|
||||||
|
endpoint = f"data/var"
|
||||||
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
|
header = {"Accept": "application/octet-stream"}
|
||||||
|
result = self.session.get(url, headers=header)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
def test_data_put_filter_fbs(self):
|
def test_data_put_filter_fbs(self):
|
||||||
endpoint = f"data/var"
|
endpoint = f"data/var"
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
@@ -222,6 +229,19 @@ class EndPoints(object):
|
|||||||
self.assertEqual(len(df["columns"]), df["n_cols"])
|
self.assertEqual(len(df["columns"]), df["n_cols"])
|
||||||
self.assertListEqual(df["col_idx"].tolist(), [0, 1, 4])
|
self.assertListEqual(df["col_idx"].tolist(), [0, 1, 4])
|
||||||
|
|
||||||
|
def test_data_get_filter_fbs(self):
|
||||||
|
index_col_name = self.schema["schema"]["annotations"]["var"]["index"]
|
||||||
|
query = f"var:{index_col_name}=SIK1"
|
||||||
|
endpoint = f"data/var"
|
||||||
|
url = f"{self.URL_BASE}{endpoint}?{query}"
|
||||||
|
header = {"Accept": "application/octet-stream"}
|
||||||
|
result = self.session.get(url, headers=header)
|
||||||
|
self.assertEqual(result.status_code, HTTPStatus.OK)
|
||||||
|
self.assertEqual(result.headers["Content-Type"], "application/octet-stream")
|
||||||
|
df = decode_fbs.decode_matrix_FBS(result.content)
|
||||||
|
self.assertEqual(df["n_rows"], 2638)
|
||||||
|
self.assertEqual(df["n_cols"], 1)
|
||||||
|
|
||||||
def test_data_put_single_var(self):
|
def test_data_put_single_var(self):
|
||||||
endpoint = f"data/var"
|
endpoint = f"data/var"
|
||||||
url = f"{self.URL_BASE}{endpoint}"
|
url = f"{self.URL_BASE}{endpoint}"
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import unittest
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
from werkzeug.datastructures import MultiDict
|
||||||
|
from server.common.rest import _query_parameter_to_filter
|
||||||
|
from server.common.errors import FilterError
|
||||||
|
|
||||||
|
|
||||||
|
def _qsparse(qs):
|
||||||
|
""" emulate what Flask/Werkzeug do to our QS """
|
||||||
|
return MultiDict(parse_qs(qs))
|
||||||
|
|
||||||
|
|
||||||
|
class FilterParseTests(unittest.TestCase):
|
||||||
|
""" Test cases for various filter parsing """
|
||||||
|
|
||||||
|
def test_queryparam_to_filter_parse(self):
|
||||||
|
# categories
|
||||||
|
self.assertEqual(_query_parameter_to_filter(_qsparse("obs:foo=bar&var:baz=133&var:baz=A&obs:baz=foo")), {
|
||||||
|
"obs": {
|
||||||
|
"annotation_value": [
|
||||||
|
{"name": "foo", "values": ["bar"]},
|
||||||
|
{"name": "baz", "values": ["foo"]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"var": {
|
||||||
|
"annotation_value": [
|
||||||
|
{"name": "baz", "values": ["133", "A"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# ranges
|
||||||
|
self.assertEqual(_query_parameter_to_filter(_qsparse("obs:A=1,99&obs:B=*,100&obs:C=0,*")), {
|
||||||
|
"obs": {
|
||||||
|
"annotation_value": [
|
||||||
|
{"name": "A", "min": 1, "max": 99.},
|
||||||
|
{"name": "B", "max": 100.},
|
||||||
|
{"name": "C", "min": 0.},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
# combo
|
||||||
|
self.assertEqual(_query_parameter_to_filter(_qsparse("var:B=YES&var:A=1,99&var:B=NO")), {
|
||||||
|
"var": {
|
||||||
|
"annotation_value": [
|
||||||
|
{"name": "B", "values": ["YES", "NO"]},
|
||||||
|
{"name": "A", "min": 1., "max": 99.},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_queryparam_to_filter_escaping(self):
|
||||||
|
self.assertEqual(_query_parameter_to_filter(_qsparse("obs:var=%2521%252C%253AOK%253D&obs:A%2521=YO")), {
|
||||||
|
"obs": {
|
||||||
|
"annotation_value": [
|
||||||
|
{"name": "var", "values": ["!,:OK="]},
|
||||||
|
{"name": "A!", "values": ["YO"]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_queryparam_to_filter_errors(self):
|
||||||
|
|
||||||
|
# should raise FilterError
|
||||||
|
filter_errors = [
|
||||||
|
"foo=bar", # no axis
|
||||||
|
"X=&Y=3", # no value
|
||||||
|
"X&Y=3", # no value
|
||||||
|
"moo:foo=bar", # bad axis
|
||||||
|
"obs:x=1,A", # non-numeric range
|
||||||
|
"var:X=1,2&var:X=3,4", # duplicate ranges
|
||||||
|
"var:Y=,",
|
||||||
|
"var:Y=2,",
|
||||||
|
"var:Y=,5",
|
||||||
|
"var:Y=*,",
|
||||||
|
"var:Y=,*",
|
||||||
|
"var:Y=*,*",
|
||||||
|
]
|
||||||
|
|
||||||
|
for qs in filter_errors:
|
||||||
|
with self.assertRaises(FilterError):
|
||||||
|
_query_parameter_to_filter(_qsparse(qs))
|
||||||
Reference in New Issue
Block a user