diff --git a/README.md b/README.md index 5edf0356..b4223d2e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ source venv/bin/activate python3 -m pip install -e ".[dev,tests]" # make sure to include the extra dependencies! ``` -Acquire two sets of static assets and place all of them within the `server/curation/data` directory: +Acquire two sets of static assets and place all of them within the `server/src/curfu/data` directory: 1. Gene autocomplete files, providing legal gene search terms to the client autocomplete component. One file each is used for entity types `aliases`, `assoc_with`, `xrefs`, `prev_symbols`, `labels`, and `symbols`. Each should be named according to the pattern `gene__.tsv`. These can be regenerated with the shell command `curfu_devtools genes`. @@ -39,7 +39,7 @@ Acquire two sets of static assets and place all of them within the `server/curat Your data/directory should look something like this: ``` -server/curfu/data +server/src/curfu/data ├── domain_lookup_2022-01-20.tsv ├── gene_aliases_suggest_20211025.tsv ├── gene_assoc_with_suggest_20211025.tsv @@ -76,13 +76,6 @@ You can run: yarn install --ignore-engines ``` -Next, run the following commands: - -``` -yarn build -mv build/ ../server/curfu/build -``` - Then start the development server: ```commandline diff --git a/client/src/components/Pages/Assay/Assay.tsx b/client/src/components/Pages/Assay/Assay.tsx index 86c71da0..29e45592 100644 --- a/client/src/components/Pages/Assay/Assay.tsx +++ b/client/src/components/Pages/Assay/Assay.tsx @@ -61,52 +61,52 @@ export const Assay: React.FC = () => { // initialize field values const [fusionDetection, setFusionDetection] = useState( - fusion?.assay?.fusion_detection !== undefined - ? fusion?.assay?.fusion_detection + fusion?.assay?.fusionDetection !== undefined + ? fusion?.assay?.fusionDetection : null ); const [assayName, setAssayName] = useState( - fusion?.assay?.assay_name !== undefined ? fusion?.assay?.assay_name : "" + fusion?.assay?.assayName !== undefined ? fusion?.assay?.assayName : "" ); const [assayId, setAssayId] = useState( - fusion?.assay?.assay_id !== undefined ? fusion?.assay?.assay_id : "" + fusion?.assay?.assayId !== undefined ? fusion?.assay?.assayId : "" ); const [methodUri, setMethodUri] = useState( - fusion?.assay?.method_uri !== undefined ? fusion?.assay?.method_uri : "" + fusion?.assay?.methodUri !== undefined ? fusion?.assay?.methodUri : "" ); const handleEvidenceChange = (event: FormEvent) => { const evidence_value = event.currentTarget.value; - if (fusion?.assay?.fusion_detection !== evidence_value) { + if (fusion?.assay?.fusionDetection !== evidence_value) { setFusionDetection(evidence_value); const assay = JSON.parse(JSON.stringify(fusion.assay)); - assay["fusion_detection"] = evidence_value; + assay["fusionDetection"] = evidence_value; setFusion({ ...fusion, assay: assay }); } }; const propertySetterMap = { - assayName: [setAssayName, "assay_name"], - assayId: [setAssayId, "assay_id"], - methodUri: [setMethodUri, "method_uri"], + assayName: [setAssayName, "assayName"], + assayId: [setAssayId, "assayId"], + methodUri: [setMethodUri, "methodUri"], }; // live update fields useEffect(() => { - if (fusion?.assay?.fusion_detection !== fusionDetection) { - setFusionDetection(fusion?.assay?.fusion_detection); + if (fusion?.assay?.fusionDetection !== fusionDetection) { + setFusionDetection(fusion?.assay?.fusionDetection); } - if (fusion?.assay?.assay_name !== assayName) { - setAssayName(fusion?.assay?.assay_name); + if (fusion?.assay?.assayName !== assayName) { + setAssayName(fusion?.assay?.assayName); } - if (fusion?.assay?.assay_id !== assayId) { - setAssayId(fusion?.assay?.assay_id); + if (fusion?.assay?.assayId !== assayId) { + setAssayId(fusion?.assay?.assayId); } - if (fusion?.assay?.method_uri !== methodUri) { - setMethodUri(fusion?.assay?.method_uri); + if (fusion?.assay?.methodUri !== methodUri) { + setMethodUri(fusion?.assay?.methodUri); } }, [fusion]); diff --git a/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx b/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx index 1e5c3aa1..0cc5b6ca 100644 --- a/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx +++ b/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx @@ -28,18 +28,18 @@ export const CausativeEvent: React.FC = () => { const { fusion, setFusion } = useContext(FusionContext); const [eventType, setEventType] = useState( - fusion.causative_event?.event_type || "" + fusion.causativeEvent?.eventType || "" ); const [eventDescription, setEventDescription] = useState( - fusion.causative_event?.event_type || "" + fusion.causativeEvent?.eventType || "" ); /** * Ensure that causative event object exists for getter/setter purposes */ const ensureEventInitialized = () => { - if (!fusion.causative_event) { - setFusion({ ...fusion, causative_event: {} }); + if (!fusion.causativeEvent) { + setFusion({ ...fusion, causativeEvent: {} }); } }; @@ -56,8 +56,8 @@ export const CausativeEvent: React.FC = () => { if (eventType !== value) { setEventType(value); } - const newCausativeEvent = { event_type: value, ...fusion.causative_event }; - setFusion({ causative_event: newCausativeEvent, ...fusion }); + const newCausativeEvent = { eventType: value, ...fusion.causativeEvent }; + setFusion({ causativeEvent: newCausativeEvent, ...fusion }); }; /** @@ -72,10 +72,10 @@ export const CausativeEvent: React.FC = () => { setEventDescription(value); } const newCausativeEvent = { - event_description: value, - ...fusion.causative_event, + eventDescription: value, + ...fusion.causativeEvent, }; - setFusion({ causative_event: newCausativeEvent, ...fusion }); + setFusion({ causativeEvent: newCausativeEvent, ...fusion }); }; return ( diff --git a/client/src/components/Pages/Domains/DomainForm/DomainForm.tsx b/client/src/components/Pages/Domains/DomainForm/DomainForm.tsx index 8665cc8a..442e305b 100644 --- a/client/src/components/Pages/Domains/DomainForm/DomainForm.tsx +++ b/client/src/components/Pages/Domains/DomainForm/DomainForm.tsx @@ -35,8 +35,8 @@ const useStyles = makeStyles((theme) => ({ const DomainForm: React.FC = () => { // // TODO: shouldn't be necessary useEffect(() => { - if (fusion.critical_functional_domains === undefined) { - setFusion({ ...fusion, ...{ critical_functional_domains: [] } }); + if (fusion.criticalFunctionalDomains === undefined) { + setFusion({ ...fusion, ...{ criticalFunctionalDomains: [] } }); } }, []); @@ -65,7 +65,7 @@ const DomainForm: React.FC = () => { const handleAdd = () => { const domainParams = domainOptions[gene].find( - (domainOption: DomainParams) => domainOption.interpro_id == domain + (domainOption: DomainParams) => domainOption.interproId == domain ); getFunctionalDomain(domainParams, status as DomainStatus, gene).then( (response) => { @@ -74,11 +74,11 @@ const DomainForm: React.FC = () => { domain_id: uuid(), ...response.domain, }; - const cloneArray = Array.from(fusion["critical_functional_domains"]); + const cloneArray = Array.from(fusion["criticalFunctionalDomains"]); cloneArray.push(newDomain); setFusion({ ...fusion, - ...{ critical_functional_domains: cloneArray }, + ...{ criticalFunctionalDomains: cloneArray }, }); setStatus("default"); @@ -107,11 +107,11 @@ const DomainForm: React.FC = () => { if (domainOptions[gene]) { const uniqueInterproIds: Set = new Set(); domainOptions[gene].forEach((domain: DomainParams, index: number) => { - if (!uniqueInterproIds.has(domain.interpro_id)) { - uniqueInterproIds.add(domain.interpro_id); + if (!uniqueInterproIds.has(domain.interproId)) { + uniqueInterproIds.add(domain.interproId); domainOptionMenuItems.push( - - {domain.domain_name} + + {domain.domainName} ); } diff --git a/client/src/components/Pages/Domains/Main/Domains.tsx b/client/src/components/Pages/Domains/Main/Domains.tsx index f51d63b1..197ad832 100644 --- a/client/src/components/Pages/Domains/Main/Domains.tsx +++ b/client/src/components/Pages/Domains/Main/Domains.tsx @@ -22,7 +22,7 @@ export const Domain: React.FC = () => { const { fusion, setFusion } = useContext(FusionContext); const { globalGenes } = useContext(GeneContext); - const domains = fusion.critical_functional_domains || []; + const domains = fusion.criticalFunctionalDomains || []; const { colorTheme } = useColorTheme(); const useStyles = makeStyles(() => ({ @@ -73,14 +73,14 @@ export const Domain: React.FC = () => { const handleRemove = (domain: ClientFunctionalDomain) => { let cloneArray: ClientFunctionalDomain[] = Array.from( - fusion.critical_functional_domains + fusion.criticalFunctionalDomains ); cloneArray = cloneArray.filter((obj) => { return obj["domain_id"] !== domain["domain_id"]; }); setFusion({ ...fusion, - ...{ critical_functional_domains: cloneArray || [] }, + ...{ criticalFunctionalDomains: cloneArray || [] }, }); }; @@ -108,7 +108,7 @@ export const Domain: React.FC = () => { avatar={{domain.status === "preserved" ? "P" : "L"}} label={ - {domainLabelString} {`(${domain.associated_gene.label})`} + {domainLabelString} {`(${domain.associatedGene.label})`} } onDelete={() => handleRemove(domain)} diff --git a/client/src/components/Pages/Gene/StructureDiagram/StructureDiagram.tsx b/client/src/components/Pages/Gene/StructureDiagram/StructureDiagram.tsx index 31caa2d9..e76d335a 100644 --- a/client/src/components/Pages/Gene/StructureDiagram/StructureDiagram.tsx +++ b/client/src/components/Pages/Gene/StructureDiagram/StructureDiagram.tsx @@ -42,8 +42,8 @@ export const StructureDiagram: React.FC = () => { }); const regEls = []; - suggestion.regulatory_elements.forEach((el) => { - regEls.push(el.gene_descriptor.label); + suggestion.regulatoryElements.forEach((el) => { + regEls.push(el.gene.label); }); return ( diff --git a/client/src/components/Pages/ReadingFrame/ReadingFrame.tsx b/client/src/components/Pages/ReadingFrame/ReadingFrame.tsx index 057fe0bc..9c55e7fa 100644 --- a/client/src/components/Pages/ReadingFrame/ReadingFrame.tsx +++ b/client/src/components/Pages/ReadingFrame/ReadingFrame.tsx @@ -50,19 +50,19 @@ export const ReadingFrame: React.FC = ({ index }) => { }; const [rFramePreserved, setRFramePreserved] = useState( - assignRadioValue(fusion.r_frame_preserved) + assignRadioValue(fusion.readingFramePreserved) ); useEffect(() => { if ( - fusion.r_frame_preserved && - fusion.r_frame_preserved !== rFramePreserved + fusion.readingFramePreserved && + fusion.readingFramePreserved !== rFramePreserved ) { - setRFramePreserved(assignRadioValue(fusion.r_frame_preserved)); + setRFramePreserved(assignRadioValue(fusion.readingFramePreserved)); } - if (fusion.r_frame_preserved === undefined) { - setFusion({ ...fusion, r_frame_preserved: null }); + if (fusion.readingFramePreserved === undefined) { + setFusion({ ...fusion, readingFramePreserved: null }); } }, [fusion]); @@ -71,13 +71,13 @@ export const ReadingFrame: React.FC = ({ index }) => { if (value !== rFramePreserved) { if (value === "yes") { setRFramePreserved("yes"); - setFusion({ ...fusion, r_frame_preserved: true }); + setFusion({ ...fusion, readingFramePreserved: true }); } else if (value === "no") { setRFramePreserved("no"); - setFusion({ ...fusion, r_frame_preserved: false }); + setFusion({ ...fusion, readingFramePreserved: false }); } else { setRFramePreserved("unspecified"); - setFusion({ ...fusion, r_frame_preserved: null }); + setFusion({ ...fusion, readingFramePreserved: null }); } } }; diff --git a/client/src/components/Pages/Structure/Builder/Builder.tsx b/client/src/components/Pages/Structure/Builder/Builder.tsx index cd7e73ed..40e28cc3 100644 --- a/client/src/components/Pages/Structure/Builder/Builder.tsx +++ b/client/src/components/Pages/Structure/Builder/Builder.tsx @@ -53,25 +53,23 @@ const ELEMENT_TEMPLATE = [ { type: ElementType.geneElement, nomenclature: "", - element_id: uuid(), - gene_descriptor: { + elementId: uuid(), + gene: { id: "", type: "", - gene_id: "", label: "", }, }, { type: ElementType.transcriptSegmentElement, nomenclature: "", - element_id: uuid(), - exon_start: null, - exon_start_offset: null, - exon_end: null, - exon_end_offset: null, - gene_descriptor: { + elementId: uuid(), + exonStart: null, + exonStartOffset: null, + exonEnd: null, + exonEndOffset: null, + gene: { id: "", - gene_id: "", type: "", label: "", }, @@ -79,12 +77,12 @@ const ELEMENT_TEMPLATE = [ { nomenclature: "", type: ElementType.linkerSequenceElement, - element_id: uuid(), + elementId: uuid(), }, { nomenclature: "", type: ElementType.templatedSequenceElement, - element_id: uuid(), + elementId: uuid(), id: "", location: { sequence_id: "", @@ -104,18 +102,18 @@ const ELEMENT_TEMPLATE = [ }, { type: ElementType.unknownGeneElement, - element_id: uuid(), + elementId: uuid(), nomenclature: "?", }, { type: ElementType.multiplePossibleGenesElement, - element_id: uuid(), + elementId: uuid(), nomenclature: "v", }, { type: ElementType.regulatoryElement, nomenclature: "", - element_id: uuid(), + elementId: uuid(), }, ]; @@ -136,10 +134,10 @@ const Builder: React.FC = () => { }, []); useEffect(() => { - if (!("structural_elements" in fusion)) { + if (!("structure" in fusion)) { setFusion({ ...fusion, - ...{ structural_elements: [] }, + ...{ structure: [] }, }); } }, [fusion]); @@ -150,14 +148,14 @@ const Builder: React.FC = () => { const sourceClone = Array.from(ELEMENT_TEMPLATE); const item = sourceClone[source.index]; const newItem = Object.assign({}, item); - newItem.element_id = uuid(); + newItem.elementId = uuid(); if (draggableId.includes("RegulatoryElement")) { - setFusion({ ...fusion, ...{ regulatory_element: newItem } }); + setFusion({ ...fusion, ...{ regulatoryElement: newItem } }); } else { - const destClone = Array.from(fusion.structural_elements); + const destClone = Array.from(fusion.structure); destClone.splice(destination.index, 0, newItem); - setFusion({ ...fusion, ...{ structural_elements: destClone } }); + setFusion({ ...fusion, ...{ structure: destClone } }); } // auto-save elements that don't need any additional input @@ -172,30 +170,28 @@ const Builder: React.FC = () => { const reorder = (result: DropResult) => { const { source, destination } = result; - const sourceClone = Array.from(fusion.structural_elements); + const sourceClone = Array.from(fusion.structure); const [movedElement] = sourceClone.splice(source.index, 1); sourceClone.splice(destination.index, 0, movedElement); - setFusion({ ...fusion, ...{ structural_elements: sourceClone } }); + setFusion({ ...fusion, ...{ structure: sourceClone } }); }; // Update global fusion object const handleSave = (index: number, newElement: ClientElementUnion) => { - const items = Array.from(fusion.structural_elements); + const items = Array.from(fusion.structure); const spliceLength = EDITABLE_ELEMENT_TYPES.includes( newElement.type as ElementType ) ? 1 : 0; items.splice(index, spliceLength, newElement); - setFusion({ ...fusion, ...{ structural_elements: items } }); + setFusion({ ...fusion, ...{ structure: items } }); }; const handleDelete = (uuid: string) => { - let items: Array = Array.from( - fusion.structural_elements - ); - items = items.filter((item) => item?.element_id !== uuid); - setFusion({ ...fusion, ...{ structural_elements: items } }); + let items: Array = Array.from(fusion.structure); + items = items.filter((item) => item?.elementId !== uuid); + setFusion({ ...fusion, ...{ structure: items } }); }; const elementNameMap = { @@ -331,14 +327,14 @@ const Builder: React.FC = () => { } }; - const nomenclatureParts = fusion.structural_elements + const nomenclatureParts = fusion.structure .filter( (element: ClientElementUnion) => Boolean(element) && element.nomenclature ) .map((element: ClientElementUnion) => element.nomenclature); - if (fusion.regulatory_element && fusion.regulatory_element.nomenclature) { - nomenclatureParts.unshift(fusion.regulatory_element.nomenclature); + if (fusion.regulatoryElement && fusion.regulatoryElement.nomenclature) { + nomenclatureParts.unshift(fusion.regulatoryElement.nomenclature); } const nomenclature = nomenclatureParts.map( (nom: string, index: number) => `${index ? "::" : ""}${nom}` @@ -418,7 +414,7 @@ const Builder: React.FC = () => { style={{ display: "flex" }} > - {ELEMENT_TEMPLATE.map(({ element_id, type }, index) => { + {ELEMENT_TEMPLATE.map(({ elementId, type }, index) => { if ( (fusion.type === "AssayedFusion" && type !== ElementType.multiplePossibleGenesElement) || @@ -427,12 +423,12 @@ const Builder: React.FC = () => { ) { return ( {(provided, snapshot) => { @@ -447,7 +443,7 @@ const Builder: React.FC = () => { className={ "option-item" + (type === ElementType.regulatoryElement && - fusion.regulatory_element !== undefined + fusion.regulatoryElement !== undefined ? " disabled_reg_element" : "") } @@ -470,7 +466,7 @@ const Builder: React.FC = () => { {snapshot.isDragging && ( {elementNameMap[type].icon}{" "} @@ -504,20 +500,20 @@ const Builder: React.FC = () => { >

Drag elements here

- {fusion.regulatory_element && ( + {fusion.regulatoryElement && ( <> - {renderElement(fusion?.regulatory_element, 0)} + {renderElement(fusion?.regulatoryElement, 0)} { /> )} - {fusion.structural_elements?.map( + {fusion.structure?.map( (element: ClientElementUnion, index: number) => { return ( element && ( {(provided) => ( diff --git a/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx b/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx index 7b34f4d7..43a9ae9f 100644 --- a/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx @@ -10,6 +10,7 @@ import { getGeneNomenclature, } from "../../../../../services/main"; import StructuralElementInputAccordion from "../StructuralElementInputAccordion"; +import React from "react"; interface GeneElementInputProps extends StructuralElementInputProps { element: ClientGeneElement; @@ -22,9 +23,7 @@ const GeneElementInput: React.FC = ({ handleDelete, icon, }) => { - const [gene, setGene] = useState( - element.gene_descriptor?.label || "" - ); + const [gene, setGene] = useState(element.gene?.label || ""); const [geneText, setGeneText] = useState(""); const validated = gene !== "" && geneText == ""; const [expanded, setExpanded] = useState(!validated); @@ -35,7 +34,7 @@ const GeneElementInput: React.FC = ({ }, [gene, geneText]); const buildGeneElement = () => { - setPendingResponse(true) + setPendingResponse(true); getGeneElement(gene).then((geneElementResponse) => { if ( geneElementResponse.warnings && @@ -44,7 +43,7 @@ const GeneElementInput: React.FC = ({ setGeneText("Gene not found"); } else if ( geneElementResponse.element && - geneElementResponse.element.gene_descriptor + geneElementResponse.element.gene ) { getGeneNomenclature(geneElementResponse.element).then( (nomenclatureResponse: NomenclatureResponse) => { @@ -54,11 +53,11 @@ const GeneElementInput: React.FC = ({ ) { const clientGeneElement: ClientGeneElement = { ...geneElementResponse.element, - element_id: element.element_id, + elementId: element.elementId, nomenclature: nomenclatureResponse.nomenclature, }; handleSave(index, clientGeneElement); - setPendingResponse(false) + setPendingResponse(false); } } ); @@ -72,7 +71,6 @@ const GeneElementInput: React.FC = ({ setGene={setGene} geneText={geneText} setGeneText={setGeneText} - style={{ width: 125 }} tooltipDirection="left" /> ); @@ -85,7 +83,7 @@ const GeneElementInput: React.FC = ({ inputElements, validated, icon, - pendingResponse + pendingResponse, }); }; diff --git a/client/src/components/Pages/Structure/Input/LinkerElementInput/LinkerElementInput.tsx b/client/src/components/Pages/Structure/Input/LinkerElementInput/LinkerElementInput.tsx index 90b64319..3b5d3111 100644 --- a/client/src/components/Pages/Structure/Input/LinkerElementInput/LinkerElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/LinkerElementInput/LinkerElementInput.tsx @@ -18,7 +18,7 @@ const LinkerElementInput: React.FC = ({ }) => { // bases const [sequence, setSequence] = useState( - element.linker_sequence?.sequence || "" + element.linkerSequence?.sequence || "" ); const linkerError = Boolean(sequence) && sequence.match(/^([aAgGtTcC]+)?$/) === null; @@ -32,11 +32,10 @@ const LinkerElementInput: React.FC = ({ const buildLinkerElement = () => { const linkerElement: ClientLinkerElement = { ...element, - linker_sequence: { + linkerSequence: { id: `fusor.sequence:${sequence}`, - type: "SequenceDescriptor", + type: "LiteralSequenceExpression", sequence: sequence, - residue_type: "SO:0000348", }, nomenclature: sequence, }; diff --git a/client/src/components/Pages/Structure/Input/RegulatoryElementInput/RegulatoryElementInput.tsx b/client/src/components/Pages/Structure/Input/RegulatoryElementInput/RegulatoryElementInput.tsx index 3bc08c4a..fa52cce7 100644 --- a/client/src/components/Pages/Structure/Input/RegulatoryElementInput/RegulatoryElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/RegulatoryElementInput/RegulatoryElementInput.tsx @@ -52,13 +52,13 @@ const RegulatoryElementInput: React.FC = ({ const { fusion, setFusion } = useContext(FusionContext); const [regElement, setRegElement] = useState< ClientRegulatoryElement | undefined - >(fusion.regulatory_element); + >(fusion.regulatoryElement); const [elementClass, setElementClass] = useState( - regElement?.regulatory_class || "default" + regElement?.regulatoryClass || "default" ); const [gene, setGene] = useState( - regElement?.associated_gene?.label || "" + regElement?.associatedGene?.label || "" ); const [geneText, setGeneText] = useState(""); @@ -75,7 +75,7 @@ const RegulatoryElementInput: React.FC = ({ if (reResponse.warnings && reResponse.warnings.length > 0) { throw new Error(reResponse.warnings[0]); } - getRegElementNomenclature(reResponse.regulatory_element).then( + getRegElementNomenclature(reResponse.regulatoryElement).then( (nomenclatureResponse) => { if ( nomenclatureResponse.warnings && @@ -84,19 +84,20 @@ const RegulatoryElementInput: React.FC = ({ throw new Error(nomenclatureResponse.warnings[0]); } const newRegElement: ClientRegulatoryElement = { - ...reResponse.regulatory_element, - display_class: regulatoryClassItems[elementClass][1], - nomenclature: nomenclatureResponse.nomenclature, + ...reResponse.regulatoryElement, + elementId: element.elementId, + displayClass: regulatoryClassItems[elementClass][1], + nomenclature: nomenclatureResponse.nomenclature || "", }; setRegElement(newRegElement); - setFusion({ ...fusion, ...{ regulatory_element: newRegElement } }); + setFusion({ ...fusion, ...{ regulatoryElement: newRegElement } }); } ); }); }; const handleDeleteElement = () => { - delete fusion.regulatory_element; + delete fusion.regulatoryElement; const cloneFusion = { ...fusion }; setRegElement(undefined); setFusion(cloneFusion); diff --git a/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx b/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx index 05b4fe92..24fcf0b9 100644 --- a/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx +++ b/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx @@ -72,7 +72,7 @@ const StructuralElementInputAccordion: React.FC< inputElements, validated, icon, - pendingResponse + pendingResponse, }) => { const classes = useStyles(); @@ -81,20 +81,24 @@ const StructuralElementInputAccordion: React.FC< : - - { - event.stopPropagation(); - handleDelete(element.element_id); - }} - onFocus={(event) => event.stopPropagation()} - > - - - + pendingResponse ? ( + + ) : ( + + { + event.stopPropagation(); + handleDelete(element.elementId); + }} + onFocus={(event) => event.stopPropagation()} + > + + + + ) } title={element.nomenclature ? element.nomenclature : null} classes={{ diff --git a/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx b/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx index f7c60221..ca0d8855 100644 --- a/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx @@ -18,16 +18,21 @@ interface TemplatedSequenceElementInputProps const TemplatedSequenceElementInput: React.FC< TemplatedSequenceElementInputProps > = ({ element, index, handleSave, handleDelete, icon }) => { - const [chromosome, setChromosome] = useState( - element.input_chromosome || "" + element.inputChromosome || "" + ); + const [strand, setStrand] = useState( + element.strand === 1 ? "+" : "-" ); - const [strand, setStrand] = useState(element.strand || "+"); const [startPosition, setStartPosition] = useState( - element.input_start || "" + element.inputStart !== null && element.inputStart !== undefined + ? `${element.inputStart}` + : "" ); const [endPosition, setEndPosition] = useState( - element.input_end || "" + element.inputEnd !== null && element.inputEnd !== undefined + ? `${element.inputEnd}` + : "" ); const [inputError, setInputError] = useState(""); @@ -67,7 +72,7 @@ const TemplatedSequenceElementInput: React.FC< ) { // TODO visible error handling setInputError("element validation unsuccessful"); - setPendingResponse(false) + setPendingResponse(false); return; } else if (templatedSequenceResponse.element) { setInputError(""); @@ -77,17 +82,21 @@ const TemplatedSequenceElementInput: React.FC< if (nomenclatureResponse.nomenclature) { const templatedSequenceElement: ClientTemplatedSequenceElement = { ...templatedSequenceResponse.element, - element_id: element.element_id, + elementId: element.elementId, nomenclature: nomenclatureResponse.nomenclature, - input_chromosome: chromosome, - input_start: startPosition, - input_end: endPosition, + region: + templatedSequenceResponse?.element?.region || element.region, + strand: + templatedSequenceResponse?.element?.strand || element.strand, + inputChromosome: chromosome, + inputStart: startPosition, + inputEnd: endPosition, }; handleSave(index, templatedSequenceElement); } }); } - setPendingResponse(false) + setPendingResponse(false); }); }; @@ -167,7 +176,7 @@ const TemplatedSequenceElementInput: React.FC< inputElements, validated, icon, - pendingResponse + pendingResponse, }); }; diff --git a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx index 077808d0..cabeb193 100644 --- a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx @@ -47,41 +47,43 @@ const TxSegmentCompInput: React.FC = ({ const { fusion } = useContext(FusionContext); const [txInputType, setTxInputType] = useState( - (element.input_type as InputType) || InputType.default + (element.inputType as InputType) || InputType.default ); // "Text" variables refer to helper or warning text to set under input fields // TODO: this needs refactored so badly - const [txAc, setTxAc] = useState(element.input_tx || ""); + const [txAc, setTxAc] = useState(element.inputTx || ""); const [txAcText, setTxAcText] = useState(""); - const [txGene, setTxGene] = useState(element.input_gene || ""); + const [txGene, setTxGene] = useState(element.inputGene || ""); const [txGeneText, setTxGeneText] = useState(""); - const [txStrand, setTxStrand] = useState(element.input_strand || "+"); + const [txStrand, setTxStrand] = useState( + element.inputStrand === 1 ? "+" : "-" + ); - const [txChrom, setTxChrom] = useState(element.input_chr || ""); + const [txChrom, setTxChrom] = useState(element.inputChr || ""); const [txChromText, setTxChromText] = useState(""); const [txStartingGenomic, setTxStartingGenomic] = useState( - element.input_genomic_start || "" + element.inputGenomicStart || "" ); const [txStartingGenomicText, setTxStartingGenomicText] = useState(""); const [txEndingGenomic, setTxEndingGenomic] = useState( - element.input_genomic_end || "" + element.inputGenomicEnd || "" ); const [txEndingGenomicText, setTxEndingGenomicText] = useState(""); - const [startingExon, setStartingExon] = useState(element.exon_start || ""); + const [startingExon, setStartingExon] = useState(element.exonStart || ""); const [startingExonText, setStartingExonText] = useState(""); - const [endingExon, setEndingExon] = useState(element.exon_end || ""); + const [endingExon, setEndingExon] = useState(element.exonEnd || ""); const [endingExonText, setEndingExonText] = useState(""); const [startingExonOffset, setStartingExonOffset] = useState( - element.exon_start_offset || "" + element.exonStartOffset || "" ); const [startingExonOffsetText, setStartingExonOffsetText] = useState(""); const [endingExonOffset, setEndingExonOffset] = useState( - element.exon_end_offset || "" + element.exonEndOffset || "" ); const [endingExonOffsetText, setEndingExonOffsetText] = useState(""); @@ -248,12 +250,12 @@ const TxSegmentCompInput: React.FC = ({ CheckGenomicCoordWarning(txSegmentResponse.warnings); } else { const inputParams = { - input_type: txInputType, - input_strand: txStrand, - input_gene: txGene, - input_chr: txChrom, - input_genomic_start: txStartingGenomic, - input_genomic_end: txEndingGenomic, + inputType: txInputType, + inputStrand: txStrand, + inputGene: txGene, + inputChr: txChrom, + inputGenomicStart: txStartingGenomic, + inputGenomicEnd: txEndingGenomic, }; handleTxElementResponse(txSegmentResponse, inputParams); } @@ -277,12 +279,12 @@ const TxSegmentCompInput: React.FC = ({ CheckGenomicCoordWarning(txSegmentResponse.warnings); } else { const inputParams = { - input_type: txInputType, - input_tx: txAc, - input_strand: txStrand, - input_chr: txChrom, - input_genomic_start: txStartingGenomic, - input_genomic_end: txEndingGenomic, + inputType: txInputType, + inputTx: txAc, + inputStrand: txStrand, + inputChr: txChrom, + inputGenomicStart: txStartingGenomic, + inputGenomicEnd: txEndingGenomic, }; handleTxElementResponse(txSegmentResponse, inputParams); } @@ -323,8 +325,8 @@ const TxSegmentCompInput: React.FC = ({ setStartingExonText(""); setEndingExonText(""); const inputParams = { - input_type: txInputType, - input_tx: txAc, + inputType: txInputType, + inputTx: txAc, }; handleTxElementResponse(txSegmentResponse, inputParams); } @@ -437,10 +439,7 @@ const TxSegmentCompInput: React.FC = ({ const genomicCoordinateInfo = ( <> - + diff --git a/client/src/components/Pages/Structure/Main/Structure.tsx b/client/src/components/Pages/Structure/Main/Structure.tsx index 0f486842..6840b3db 100644 --- a/client/src/components/Pages/Structure/Main/Structure.tsx +++ b/client/src/components/Pages/Structure/Main/Structure.tsx @@ -41,8 +41,8 @@ export const Structure: React.FC = () => { Drag and rearrange elements. { // TODO -- how to interact w/ reg element count? - fusion.structural_elements?.length + - (fusion.regulatory_element !== undefined) >= + fusion.structure?.length + + (fusion.regulatoryElement !== undefined) >= 2 ? null : ( {" "} diff --git a/client/src/components/Pages/Summary/Invalid/Invalid.tsx b/client/src/components/Pages/Summary/Invalid/Invalid.tsx index 8672978c..757bdf02 100644 --- a/client/src/components/Pages/Summary/Invalid/Invalid.tsx +++ b/client/src/components/Pages/Summary/Invalid/Invalid.tsx @@ -52,7 +52,8 @@ export const Invalid: React.FC = ({ const duplicateGeneError = (duplicateGenes: string[]) => { return ( - Duplicate gene element(s) detected: {duplicateGenes.join(", ")}. Per the{" "} + Duplicate gene element(s) detected: {duplicateGenes.join(", ")}. + Per the{" "} = ({ > Gene Fusion Specification - , Internal Tandem Duplications are not considered gene fusions, as they do not involve an interaction - between two or more genes.{" "} + , Internal Tandem Duplications are not considered gene fusions, as they + do not involve an interaction between two or more genes.{" "} setVisibleTab(0)}> Edit elements to resolve. - ) + ); }; const elementNumberError = ( @@ -107,32 +108,33 @@ export const Invalid: React.FC = ({ ); - const geneElements = fusion.structural_elements.filter(el => el.type === "GeneElement").map(el => { return el.nomenclature }) - const findDuplicates = arr => arr.filter((item, index) => arr.indexOf(item) !== index) - const duplicateGenes = findDuplicates(geneElements) + const geneElements = fusion.structure + .filter((el) => el.type === "GeneElement") + .map((el) => { + return el.nomenclature; + }); + const findDuplicates = (arr) => + arr.filter((item, index) => arr.indexOf(item) !== index); + const duplicateGenes = findDuplicates(geneElements); const checkErrors = () => { const errorElements: React.ReactFragment[] = []; - if ( - Boolean(fusion.regulatory_element) + fusion.structural_elements.length < - 2 - ) { + if (Boolean(fusion.regulatoryElement) + fusion.structure.length < 2) { errorElements.push(elementNumberError); } else { - const containsGene = fusion.structural_elements.some( - (e: ClientElementUnion) => - [ - "GeneElement", - "TranscriptSegmentElement", - "TemplatedSequenceElement", - ].includes(e.type) + const containsGene = fusion.structure.some((e: ClientElementUnion) => + [ + "GeneElement", + "TranscriptSegmentElement", + "TemplatedSequenceElement", + ].includes(e.type) ); - if (!containsGene && !fusion.regulatory_element) { + if (!containsGene && !fusion.regulatoryElement) { errorElements.push(noGeneElementsError); } } if (duplicateGenes.length > 0) { - errorElements.push(duplicateGeneError(duplicateGenes)) + errorElements.push(duplicateGeneError(duplicateGenes)); } if (errorElements.length == 0) { errorElements.push( diff --git a/client/src/components/Pages/Summary/JSON/SummaryJSON.tsx b/client/src/components/Pages/Summary/JSON/SummaryJSON.tsx index 90045b22..21b6f5a3 100644 --- a/client/src/components/Pages/Summary/JSON/SummaryJSON.tsx +++ b/client/src/components/Pages/Summary/JSON/SummaryJSON.tsx @@ -1,118 +1,23 @@ import copy from "clipboard-copy"; import React, { useEffect, useState } from "react"; +import { validateFusion } from "../../../../services/main"; import { - ClientElementUnion, - ElementUnion, - validateFusion, -} from "../../../../services/main"; -import { - AssayedFusion, - CategoricalFusion, - FunctionalDomain, - GeneElement, - LinkerElement, - MultiplePossibleGenesElement, - TemplatedSequenceElement, - TranscriptSegmentElement, - UnknownGeneElement, + FormattedAssayedFusion, + FormattedCategoricalFusion, } from "../../../../services/ResponseModels"; -import { FusionType } from "../Main/Summary"; import "./SummaryJSON.scss"; interface Props { - fusion: FusionType; + formattedFusion: FormattedAssayedFusion | FormattedCategoricalFusion; } -export const SummaryJSON: React.FC = ({ fusion }) => { +export const SummaryJSON: React.FC = ({ formattedFusion }) => { const [isDown, setIsDown] = useState(false); const [isCopied, setIsCopied] = useState(false); const [printedFusion, setPrintedFusion] = useState(""); const [validationErrors, setValidationErrors] = useState([]); - /** - * On component render, restructure fusion to drop properties used for client state purposes, - * transmit to validation endpoint, and update local copy. - */ useEffect(() => { - const structuralElements: ElementUnion[] = fusion.structural_elements?.map( - (element: ClientElementUnion) => { - switch (element.type) { - case "GeneElement": - const geneElement: GeneElement = { - type: element.type, - gene_descriptor: element.gene_descriptor, - }; - return geneElement; - case "LinkerSequenceElement": - const linkerElement: LinkerElement = { - type: element.type, - linker_sequence: element.linker_sequence, - }; - return linkerElement; - case "TemplatedSequenceElement": - const templatedSequenceElement: TemplatedSequenceElement = { - type: element.type, - region: element.region, - strand: element.strand, - }; - return templatedSequenceElement; - case "TranscriptSegmentElement": - const txSegmentElement: TranscriptSegmentElement = { - type: element.type, - transcript: element.transcript, - exon_start: element.exon_start, - exon_start_offset: element.exon_start_offset, - exon_end: element.exon_end, - exon_end_offset: element.exon_end_offset, - gene_descriptor: element.gene_descriptor, - element_genomic_start: element.element_genomic_start, - element_genomic_end: element.element_genomic_end, - }; - return txSegmentElement; - case "MultiplePossibleGenesElement": - case "UnknownGeneElement": - const newElement: - | MultiplePossibleGenesElement - | UnknownGeneElement = { - type: element.type, - }; - return newElement; - default: - throw new Error("Unrecognized element type"); - } - } - ); - const regulatoryElements = fusion.regulatory_elements?.map((re) => ({ - type: re.type, - associated_gene: re.associated_gene, - regulatory_class: re.regulatory_class, - feature_id: re.feature_id, - genomic_location: re.genomic_location, - })); - let formattedFusion: AssayedFusion | CategoricalFusion; - if (fusion.type === "AssayedFusion") { - formattedFusion = { - ...fusion, - structural_elements: structuralElements, - regulatory_elements: regulatoryElements, - }; - } else { - const criticalDomains: FunctionalDomain[] = - fusion.critical_functional_domains?.map((domain) => ({ - _id: domain._id, - label: domain.label, - status: domain.status, - associated_gene: domain.associated_gene, - sequence_location: domain.sequence_location, - })); - formattedFusion = { - ...fusion, - structural_elements: structuralElements, - regulatory_elements: regulatoryElements, - critical_functional_domains: criticalDomains, - }; - } - // make request validateFusion(formattedFusion).then((response) => { if (response.warnings && response.warnings?.length > 0) { @@ -126,7 +31,7 @@ export const SummaryJSON: React.FC = ({ fusion }) => { setPrintedFusion(JSON.stringify(response.fusion, null, 2)); } }); - }, [fusion]); // should be blank? + }, [formattedFusion]); const handleCopy = () => { copy(printedFusion); diff --git a/client/src/components/Pages/Summary/Main/Summary.tsx b/client/src/components/Pages/Summary/Main/Summary.tsx index 6854acd1..f8e2a01e 100644 --- a/client/src/components/Pages/Summary/Main/Summary.tsx +++ b/client/src/components/Pages/Summary/Main/Summary.tsx @@ -3,6 +3,8 @@ import { FusionContext } from "../../../../global/contexts/FusionContext"; import React, { useContext, useEffect, useState } from "react"; import { + AssayedFusionElements, + CategoricalFusionElements, ClientElementUnion, ElementUnion, validateFusion, @@ -10,7 +12,8 @@ import { import { AssayedFusion, CategoricalFusion, - FunctionalDomain, + FormattedAssayedFusion, + FormattedCategoricalFusion, GeneElement, LinkerElement, MultiplePossibleGenesElement, @@ -33,6 +36,9 @@ export const Summary: React.FC = ({ setVisibleTab }) => { const [validatedFusion, setValidatedFusion] = useState< AssayedFusion | CategoricalFusion | null >(null); + const [formattedFusion, setFormattedFusion] = useState< + FormattedAssayedFusion | FormattedCategoricalFusion | null + >(null); const [validationErrors, setValidationErrors] = useState([]); const { fusion } = useContext(FusionContext); @@ -48,13 +54,13 @@ export const Summary: React.FC = ({ setVisibleTab }) => { case "GeneElement": const geneElement: GeneElement = { type: element.type, - gene_descriptor: element.gene_descriptor, + gene: element.gene, }; return geneElement; case "LinkerSequenceElement": const linkerElement: LinkerElement = { type: element.type, - linker_sequence: element.linker_sequence, + linkerSequence: element.linkerSequence, }; return linkerElement; case "TemplatedSequenceElement": @@ -68,13 +74,13 @@ export const Summary: React.FC = ({ setVisibleTab }) => { const txSegmentElement: TranscriptSegmentElement = { type: element.type, transcript: element.transcript, - exon_start: element.exon_start, - exon_start_offset: element.exon_start_offset, - exon_end: element.exon_end, - exon_end_offset: element.exon_end_offset, - gene_descriptor: element.gene_descriptor, - element_genomic_start: element.element_genomic_start, - element_genomic_end: element.element_genomic_end, + exonStart: element.exonStart, + exonStartOffset: element.exonStartOffset, + exonEnd: element.exonEnd, + exonEndOffset: element.exonEndOffset, + gene: element.gene, + elementGenomicStart: element.elementGenomicStart, + elementGenomicEnd: element.elementGenomicEnd, }; return txSegmentElement; case "MultiplePossibleGenesElement": @@ -93,7 +99,7 @@ export const Summary: React.FC = ({ setVisibleTab }) => { * @param formattedFusion fusion with client-oriented properties dropped */ const requestValidatedFusion = ( - formattedFusion: AssayedFusion | CategoricalFusion + formattedFusion: FormattedAssayedFusion | FormattedCategoricalFusion ) => { // make request validateFusion(formattedFusion).then((response) => { @@ -116,53 +122,54 @@ export const Summary: React.FC = ({ setVisibleTab }) => { /** * On component render, restructure fusion to drop properties used for client state purposes, + * fix expected casing for fusor fusion constructors, * transmit to validation endpoint, and update local copy. */ useEffect(() => { - const structuralElements: ElementUnion[] = fusion.structural_elements?.map( + const structure: ElementUnion[] = fusion.structure?.map( (element: ClientElementUnion) => fusorifyStructuralElement(element) ); let regulatoryElement: RegulatoryElement | null = null; - if (fusion.regulatory_element) { + if (fusion.regulatoryElement) { regulatoryElement = { - type: fusion.regulatory_element.type, - associated_gene: fusion.regulatory_element.associated_gene, - regulatory_class: fusion.regulatory_element.regulatory_class, - feature_id: fusion.regulatory_element.feature_id, - feature_location: fusion.regulatory_element.feature_location, + type: fusion.regulatoryElement.type, + associatedGene: fusion.regulatoryElement.associatedGene, + regulatoryClass: fusion.regulatoryElement.regulatoryClass, + featureId: fusion.regulatoryElement.featureId, + featureLocation: fusion.regulatoryElement.featureLocation, }; } - let formattedFusion: AssayedFusion | CategoricalFusion; + let formattedFusion: FormattedAssayedFusion | FormattedCategoricalFusion; if (fusion.type === "AssayedFusion") { formattedFusion = { - ...fusion, - structural_elements: structuralElements, + fusion_type: fusion.type, + structure: structure as AssayedFusionElements[], + causative_event: fusion.causativeEvent, + assay: fusion.assay, regulatory_element: regulatoryElement, + reading_frame_preserved: fusion.readingFramePreserved, }; } else { - const criticalDomains: FunctionalDomain[] = - fusion.critical_functional_domains?.map((domain: FunctionalDomain) => ({ - _id: domain._id, - label: domain.label, - status: domain.status, - associated_gene: domain.associated_gene, - sequence_location: domain.sequence_location, - })); formattedFusion = { - ...fusion, - structural_elements: structuralElements, + fusion_type: fusion.type, + structure: structure as CategoricalFusionElements[], regulatory_element: regulatoryElement, - critical_functional_domains: criticalDomains, + critical_functional_domains: fusion.criticalFunctionalDomains, + reading_frame_preserved: fusion.readingFramePreserved, }; } requestValidatedFusion(formattedFusion); + setFormattedFusion(formattedFusion); }, [fusion]); + console.log(formattedFusion); + return ( <> {(!validationErrors || validationErrors.length === 0) && + formattedFusion && validatedFusion ? ( - + ) : ( <> {validationErrors && validationErrors.length > 0 ? ( diff --git a/client/src/components/Pages/Summary/Readable/Readable.tsx b/client/src/components/Pages/Summary/Readable/Readable.tsx index 291464f2..2bed053a 100644 --- a/client/src/components/Pages/Summary/Readable/Readable.tsx +++ b/client/src/components/Pages/Summary/Readable/Readable.tsx @@ -1,5 +1,9 @@ import "./Readable.scss"; -import { ClientStructuralElement } from "../../../../services/ResponseModels"; +import { + ClientStructuralElement, + FormattedAssayedFusion, + FormattedCategoricalFusion, +} from "../../../../services/ResponseModels"; import React, { useContext, useEffect, useState } from "react"; import Chip from "@material-ui/core/Chip"; import { FusionContext } from "../../../../global/contexts/FusionContext"; @@ -12,27 +16,28 @@ import { Typography, } from "@material-ui/core"; import { eventDisplayMap } from "../../CausativeEvent/CausativeEvent"; -import { FusionType } from "../Main/Summary"; import { getFusionNomenclature } from "../../../../services/main"; type Props = { - validatedFusion: FusionType; + formattedFusion: FormattedAssayedFusion | FormattedCategoricalFusion; }; -export const Readable: React.FC = ({ validatedFusion }) => { +export const Readable: React.FC = ({ + formattedFusion: formattedFusion, +}) => { // the validated fusion object is available as a parameter, but we'll use the // client-ified version to grab things like nomenclature and display values const { fusion } = useContext(FusionContext); const [nomenclature, setNomenclature] = useState(""); useEffect(() => { - getFusionNomenclature(validatedFusion).then((nmResponse) => + getFusionNomenclature(formattedFusion).then((nmResponse) => setNomenclature(nmResponse.nomenclature as string) ); - }, [validatedFusion]); + }, [formattedFusion]); - const assayName = fusion.assay?.assay_name ? fusion.assay.assay_name : "" - const assayId = fusion.assay?.assay_id ? `(${fusion.assay.assay_id})` : "" + const assayName = fusion.assay?.assayName ? fusion.assay.assayName : ""; + const assayId = fusion.assay?.assayId ? `(${fusion.assay.assayId})` : ""; /** * Render rows specific to assayed fusion fields @@ -46,7 +51,7 @@ export const Readable: React.FC = ({ validatedFusion }) => { - {eventDisplayMap[fusion.causative_event?.event_type] || ""} + {eventDisplayMap[fusion.causativeEvent?.eventType] || ""} @@ -55,7 +60,9 @@ export const Readable: React.FC = ({ validatedFusion }) => { Assay - {fusion.assay ? `${assayName} ${assayId}` : ""} + + {fusion.assay ? `${assayName} ${assayId}` : ""} + @@ -72,9 +79,9 @@ export const Readable: React.FC = ({ validatedFusion }) => { Functional domains - {fusion.critical_functional_domains && - fusion.critical_functional_domains.length > 0 && - fusion.critical_functional_domains.map((domain, index) => ( + {fusion.criticalFunctionalDomains && + fusion.criticalFunctionalDomains.length > 0 && + fusion.criticalFunctionalDomains.map((domain, index) => ( {`${domain.status}: ${domain.label}`} @@ -87,9 +94,9 @@ export const Readable: React.FC = ({ validatedFusion }) => { - {fusion.r_frame_preserved === true + {fusion.readingFramePreserved === true ? "Preserved" - : fusion.r_frame_preserved === false + : fusion.readingFramePreserved === false ? "Not preserved" : "Unspecified"} @@ -111,7 +118,7 @@ export const Readable: React.FC = ({ validatedFusion }) => { Structure - {fusion.structural_elements.map( + {fusion.structure.map( (element: ClientStructuralElement, index: number) => ( ) @@ -123,8 +130,8 @@ export const Readable: React.FC = ({ validatedFusion }) => { Regulatory Element - {fusion.regulatory_element ? ( - + {fusion.regulatoryElement ? ( + ) : ( "" )} diff --git a/client/src/components/Pages/Summary/Success/Success.tsx b/client/src/components/Pages/Summary/Success/Success.tsx index 28eaa0a0..04e6b218 100644 --- a/client/src/components/Pages/Summary/Success/Success.tsx +++ b/client/src/components/Pages/Summary/Success/Success.tsx @@ -3,7 +3,10 @@ import { useColorTheme } from "../../../../global/contexts/Theme/ColorThemeConte import { Readable } from "../Readable/Readable"; import { Tabs, Tab } from "@material-ui/core/"; import { SummaryJSON } from "../JSON/SummaryJSON"; -import { FusionType } from "../Main/Summary"; +import { + FormattedAssayedFusion, + FormattedCategoricalFusion, +} from "../../../../services/ResponseModels"; const TabPanel = (props) => { const { children, value, index, ...other } = props; @@ -22,7 +25,7 @@ const TabPanel = (props) => { }; interface Props { - fusion: FusionType; + fusion: FormattedAssayedFusion | FormattedCategoricalFusion; } export const Success: React.FC = ({ fusion }) => { @@ -52,12 +55,12 @@ export const Success: React.FC = ({ fusion }) => {
- {fusion && } + {fusion && }
- {fusion && } + {fusion && }
diff --git a/client/src/components/main/App/App.tsx b/client/src/components/main/App/App.tsx index 483ec9e4..34056944 100644 --- a/client/src/components/main/App/App.tsx +++ b/client/src/components/main/App/App.tsx @@ -37,7 +37,7 @@ import { ClientAssayedFusion, ClientCategoricalFusion, DomainParams, - GeneDescriptor, + Gene, } from "../../../services/ResponseModels"; import LandingPage from "../Landing/LandingPage"; import AppMenu from "./AppMenu"; @@ -51,13 +51,13 @@ import { type ClientFusion = ClientCategoricalFusion | ClientAssayedFusion; -type GenesLookup = Record; +type GenesLookup = Record; type DomainOptionsLookup = Record; const path = window.location.pathname; const defaultFusion: ClientFusion = { - structural_elements: [], + structure: [], type: path.includes("/assayed-fusion") ? "AssayedFusion" : "CategoricalFusion", @@ -96,33 +96,26 @@ const App = (): JSX.Element => { useEffect(() => { const newGenes = {}; const remainingGeneIds: Array = []; - fusion.structural_elements.forEach((comp: ClientElementUnion) => { + fusion.structure.forEach((comp: ClientElementUnion) => { if ( comp && comp.type && (comp.type === "GeneElement" || comp.type === "TranscriptSegmentElement") && - comp.gene_descriptor?.gene_id + comp.gene?.id ) { - remainingGeneIds.push(comp.gene_descriptor.gene_id); - if ( - comp.gene_descriptor.gene_id && - !(comp.gene_descriptor.gene_id in globalGenes) - ) { - newGenes[comp.gene_descriptor.gene_id] = comp.gene_descriptor; + remainingGeneIds.push(comp.gene.id); + if (comp.gene.id && !(comp.gene.id in globalGenes)) { + newGenes[comp.gene.id] = comp.gene; } } }); - if (fusion.regulatory_element) { - if (fusion.regulatory_element.associated_gene?.gene_id) { - remainingGeneIds.push( - fusion.regulatory_element.associated_gene.gene_id - ); - if ( - !(fusion.regulatory_element.associated_gene.gene_id in globalGenes) - ) { - newGenes[fusion.regulatory_element.associated_gene.gene_id] = - fusion.regulatory_element.associated_gene; + if (fusion.regulatoryElement) { + if (fusion.regulatoryElement.associatedGene?.id) { + remainingGeneIds.push(fusion.regulatoryElement.associatedGene.id); + if (!(fusion.regulatoryElement.associatedGene.id in globalGenes)) { + newGenes[fusion.regulatoryElement.associatedGene.id] = + fusion.regulatoryElement.associatedGene; } } } @@ -176,38 +169,38 @@ const App = (): JSX.Element => { */ const fusionIsEmpty = () => { if ( - fusion?.structural_elements.length === 0 && - fusion?.regulatory_element === undefined + fusion?.structure.length === 0 && + fusion?.regulatoryElement === undefined ) { return true; - } else if (fusion.structural_elements.length > 0) { + } else if (fusion.structure.length > 0) { return false; - } else if (fusion.regulatory_element) { + } else if (fusion.regulatoryElement) { return false; } else if (fusion.type == "AssayedFusion") { if ( fusion.assay && - (fusion.assay.assay_name || - fusion.assay.assay_id || - fusion.assay.method_uri || - fusion.assay.fusion_detection) + (fusion.assay.assayName || + fusion.assay.assayId || + fusion.assay.methodUri || + fusion.assay.fusionDetection) ) { return false; } if ( - fusion.causative_event && - (fusion.causative_event.event_type || - fusion.causative_event.event_description) + fusion.causativeEvent && + (fusion.causativeEvent.eventType || + fusion.causativeEvent.eventDescription) ) { return false; } } else if (fusion.type == "CategoricalFusion") { - if (fusion.r_frame_preserved !== undefined) { + if (fusion.readingFramePreserved !== undefined) { return false; } if ( - fusion.critical_functional_domains && - fusion.critical_functional_domains.length > 0 + fusion.criticalFunctionalDomains && + fusion.criticalFunctionalDomains.length > 0 ) { return false; } diff --git a/client/src/services/ResponseModels.ts b/client/src/services/ResponseModels.ts index 2b822703..9aaeb169 100644 --- a/client/src/services/ResponseModels.ts +++ b/client/src/services/ResponseModels.ts @@ -5,6 +5,10 @@ /* Do not modify it by hand - just update the pydantic models and then re-run the script */ +/** + * Form of evidence supporting identification of the fusion. + */ +export type Evidence = "observed" | "inferred"; /** * Define possible classes of Regulatory Elements. Options are the possible values * for /regulatory_class value property in the INSDC controlled vocabulary: @@ -31,39 +35,61 @@ export type RegulatoryClass = | "terminator" | "other"; /** - * A `W3C Compact URI `_ formatted string. A CURIE string has the structure ``prefix``:``reference``, as defined by the W3C syntax. + * Indicates that the value is taken from a set of controlled strings defined elsewhere. Technically, a code is restricted to a string which has at least one character and no leading or trailing whitespace, and where there is no whitespace other than single spaces in the contents. + */ +export type Code = string; +/** + * A mapping relation between concepts as defined by the Simple Knowledge + * Organization System (SKOS). + */ +export type Relation = + | "closeMatch" + | "exactMatch" + | "broadMatch" + | "narrowMatch" + | "relatedMatch"; +/** + * An IRI Reference (either an IRI or a relative-reference), according to `RFC3986 section 4.1 ` and `RFC3987 section 2.1 `. MAY be a JSON Pointer as an IRI fragment, as described by `RFC6901 section 6 `. */ -export type CURIE = string; +export type IRI = string; /** - * A range comparator. + * The interpretation of the character codes referred to by the refget accession, + * where "aa" specifies an amino acid character set, and "na" specifies a nucleic acid + * character set. */ -export type Comparator = "<=" | ">="; +export type ResidueAlphabet = "aa" | "na"; /** - * A character string representing cytobands derived from the *International System for Human Cytogenomic Nomenclature* (ISCN) `guidelines `_. + * An inclusive range of values bounded by one or more integers. */ -export type HumanCytoband = string; +export type Range = [number | null, number | null]; /** - * Define possible values for strand + * A character string of Residues that represents a biological sequence using the conventional sequence order (5'-to-3' for nucleic acid sequences, and amino-to-carboxyl for amino acid sequences). IUPAC ambiguity codes are permitted in Sequence Strings. */ -export type Strand = "+" | "-"; +export type SequenceString = string; /** - * A character string of Residues that represents a biological sequence using the conventional sequence order (5'-to-3' for nucleic acid sequences, and amino-to-carboxyl for amino acid sequences). IUPAC ambiguity codes are permitted in Sequences. + * Create enum for positive and negative strand */ -export type Sequence = string; +export type Strand = 1 | -1; /** * Permissible values for describing the underlying causative event driving an * assayed fusion. */ export type EventType = "rearrangement" | "read-through" | "trans-splicing"; -/** - * Form of evidence supporting identification of the fusion. - */ -export type Evidence = "observed" | "inferred"; /** * Define possible statuses of functional domains. */ export type DomainStatus = "lost" | "preserved"; +/** + * Information pertaining to the assay used in identifying the fusion. + */ +export interface Assay { + type?: "Assay"; + assayName?: string | null; + assayId?: string | null; + methodUri?: string | null; + fusionDetection?: Evidence | null; +} /** * Assayed gene fusions from biological specimens are directly detected using * RNA-based gene fusion assays, or alternatively may be inferred from genomic @@ -72,230 +98,253 @@ export type DomainStatus = "lost" | "preserved"; */ export interface AssayedFusion { type?: "AssayedFusion"; - regulatory_element?: RegulatoryElement; - structural_elements: ( + regulatoryElement?: RegulatoryElement | null; + structure: ( | TranscriptSegmentElement | GeneElement | TemplatedSequenceElement | LinkerElement | UnknownGeneElement )[]; - causative_event: CausativeEvent; - assay: Assay; + readingFramePreserved?: boolean | null; + causativeEvent?: CausativeEvent | null; + assay?: Assay | null; } /** * Define RegulatoryElement class. * - * `feature_id` would ideally be constrained as a CURIE, but Encode, our preferred + * `featureId` would ideally be constrained as a CURIE, but Encode, our preferred * feature ID source, doesn't currently have a registered CURIE structure for EH_ * identifiers. Consequently, we permit any kind of free text. */ export interface RegulatoryElement { type?: "RegulatoryElement"; - regulatory_class: RegulatoryClass; - feature_id?: string; - associated_gene?: GeneDescriptor; - feature_location?: LocationDescriptor; + regulatoryClass: RegulatoryClass; + featureId?: string | null; + associatedGene?: Gene | null; + featureLocation?: SequenceLocation | null; } /** - * This descriptor is intended to reference VRS Gene value objects. + * A basic physical and functional unit of heredity. */ -export interface GeneDescriptor { - id: CURIE; - type?: "GeneDescriptor"; - label?: string; - description?: string; - xrefs?: CURIE[]; - alternate_labels?: string[]; - extensions?: Extension[]; - gene_id?: CURIE; - gene?: Gene; +export interface Gene { + /** + * The 'logical' identifier of the entity in the system of record, e.g. a UUID. This 'id' is unique within a given system. The identified entity may have a different 'id' in a different system, or may refer to an 'id' for the shared concept in another system (e.g. a CURIE). + */ + id?: string | null; + /** + * MUST be "Gene". + */ + type?: "Gene"; + /** + * A primary label for the entity. + */ + label?: string | null; + /** + * A free-text description of the entity. + */ + description?: string | null; + /** + * Alternative name(s) for the Entity. + */ + alternativeLabels?: string[] | null; + /** + * A list of extensions to the entity. Extensions are not expected to be natively understood, but may be used for pre-negotiated exchange of message attributes between systems. + */ + extensions?: Extension[] | null; + /** + * A list of mappings to concepts in terminologies or code systems. Each mapping should include a coding and a relation. + */ + mappings?: ConceptMapping[] | null; + [k: string]: unknown; } /** - * The Extension class provides VODs with a means to extend descriptions - * with other attributes unique to a content provider. These extensions are - * not expected to be natively understood under VRSATILE, but may be used - * for pre-negotiated exchange of message attributes when needed. + * The Extension class provides entities with a means to include additional + * attributes that are outside of the specified standard but needed by a given content + * provider or system implementer. These extensions are not expected to be natively + * understood, but may be used for pre-negotiated exchange of message attributes + * between systems. */ export interface Extension { - type?: "Extension"; + /** + * A name for the Extension. Should be indicative of its meaning and/or the type of information it value represents. + */ name: string; - value?: unknown; + /** + * The value of the Extension - can be any primitive or structured object + */ + value?: + | number + | string + | boolean + | { + [k: string]: unknown; + } + | unknown[] + | null; + /** + * A description of the meaning or utility of the Extension, to explain the type of information it is meant to hold. + */ + description?: string | null; + [k: string]: unknown; } /** - * A reference to a Gene as defined by an authority. For human genes, the use of - * `hgnc `_ as the gene authority is - * RECOMMENDED. + * A mapping to a concept in a terminology or code system. */ -export interface Gene { - type?: "Gene"; +export interface ConceptMapping { /** - * A CURIE reference to a Gene concept + * A structured representation of a code for a defined concept in a terminology or code system. */ - gene_id: CURIE; + coding: Coding; + /** + * A mapping relation between concepts as defined by the Simple Knowledge Organization System (SKOS). + */ + relation: Relation; + [k: string]: unknown; } /** - * This descriptor is intended to reference VRS Location value objects. + * A structured representation of a code for a defined concept in a terminology or + * code system. */ -export interface LocationDescriptor { - id: CURIE; - type?: "LocationDescriptor"; - label?: string; - description?: string; - xrefs?: CURIE[]; - alternate_labels?: string[]; - extensions?: Extension[]; - location_id?: CURIE; - location?: SequenceLocation | ChromosomeLocation; +export interface Coding { + /** + * The human-readable name for the coded concept, as defined by the code system. + */ + label?: string | null; + /** + * The terminology/code system that defined the code. May be reported as a free-text name (e.g. 'Sequence Ontology'), but it is preferable to provide a uri/url for the system. When the 'code' is reported as a CURIE, the 'system' should be reported as the uri that the CURIE's prefix expands to (e.g. 'http://purl.obofoundry.org/so.owl/' for the Sequence Ontology). + */ + system: string; + /** + * Version of the terminology or code system that provided the code. + */ + version?: string | null; + /** + * A symbol uniquely identifying the concept, as in a syntax defined by the code system. CURIE format is preferred where possible (e.g. 'SO:0000704' is the CURIE form of the Sequence Ontology code for 'gene'). + */ + code: Code; + [k: string]: unknown; } /** - * A Location defined by an interval on a referenced Sequence. + * A `Location` defined by an interval on a referenced `Sequence`. */ export interface SequenceLocation { /** - * Variation Id. MUST be unique within document. + * The 'logical' identifier of the entity in the system of record, e.g. a UUID. This 'id' is unique within a given system. The identified entity may have a different 'id' in a different system, or may refer to an 'id' for the shared concept in another system (e.g. a CURIE). + */ + id?: string | null; + /** + * MUST be "SequenceLocation" */ - _id?: CURIE; type?: "SequenceLocation"; /** - * A VRS Computed Identifier for the reference Sequence. + * A primary label for the entity. */ - sequence_id: CURIE; + label?: string | null; /** - * Reference sequence region defined by a SequenceInterval. + * A free-text description of the entity. */ - interval: SequenceInterval | SimpleInterval; -} -/** - * A SequenceInterval represents a span on a Sequence. Positions are always - * represented by contiguous spans using interbase coordinates or coordinate ranges. - */ -export interface SequenceInterval { - type?: "SequenceInterval"; + description?: string | null; /** - * The start coordinate or range of the interval. The minimum value of this coordinate or range is 0. MUST represent a coordinate or range less than the value of `end`. + * Alternative name(s) for the Entity. */ - start: DefiniteRange | IndefiniteRange | Number; + alternativeLabels?: string[] | null; /** - * The end coordinate or range of the interval. The minimum value of this coordinate or range is 0. MUST represent a coordinate or range greater than the value of `start`. + * A list of extensions to the entity. Extensions are not expected to be natively understood, but may be used for pre-negotiated exchange of message attributes between systems. */ - end: DefiniteRange | IndefiniteRange | Number; -} -/** - * A bounded, inclusive range of numbers. - */ -export interface DefiniteRange { - type?: "DefiniteRange"; + extensions?: Extension[] | null; /** - * The minimum value; inclusive + * A list of mappings to concepts in terminologies or code systems. Each mapping should include a coding and a relation. */ - min: number; + mappings?: ConceptMapping[] | null; /** - * The maximum value; inclusive + * A sha512t24u digest created using the VRS Computed Identifier algorithm. */ - max: number; -} -/** - * A half-bounded range of numbers represented as a number bound and associated - * comparator. The bound operator is interpreted as follows: '>=' are all numbers - * greater than and including `value`, '<=' are all numbers less than and including - * `value`. - */ -export interface IndefiniteRange { - type?: "IndefiniteRange"; + digest?: string | null; /** - * The bounded value; inclusive + * A reference to a `Sequence` on which the location is defined. */ - value: number; + sequenceReference?: IRI | SequenceReference | null; /** - * MUST be one of '<=' or '>=', indicating which direction the range is indefinite + * The start coordinate or range of the SequenceLocation. The minimum value of this coordinate or range is 0. MUST represent a coordinate or range less than the value of `end`. */ - comparator: Comparator; -} -/** - * A simple integer value as a VRS class. - */ -export interface Number { - type?: "Number"; + start?: Range | number | null; + /** + * The end coordinate or range of the SequenceLocation. The minimum value of this coordinate or range is 0. MUST represent a coordinate or range greater than the value of `start`. + */ + end?: Range | number | null; /** - * The value represented by Number + * The literal sequence encoded by the `sequenceReference` at these coordinates. */ - value: number; + sequence?: SequenceString | null; + [k: string]: unknown; } /** - * DEPRECATED: A SimpleInterval represents a span of sequence. Positions are always - * represented by contiguous spans using interbase coordinates. - * This class is deprecated. Use SequenceInterval instead. + * A sequence of nucleic or amino acid character codes. */ -export interface SimpleInterval { - type?: "SimpleInterval"; +export interface SequenceReference { /** - * The start coordinate + * The 'logical' identifier of the entity in the system of record, e.g. a UUID. This 'id' is unique within a given system. The identified entity may have a different 'id' in a different system, or may refer to an 'id' for the shared concept in another system (e.g. a CURIE). */ - start: number; + id?: string | null; /** - * The end coordinate + * MUST be "SequenceReference" */ - end: number; -} -/** - * A Location on a chromosome defined by a species and chromosome name. - */ -export interface ChromosomeLocation { + type?: "SequenceReference"; /** - * Location Id. MUST be unique within document. + * A primary label for the entity. */ - _id?: CURIE; - type?: "ChromosomeLocation"; + label?: string | null; /** - * CURIE representing a species from the `NCBI species taxonomy `_. Default: 'taxonomy:9606' (human) + * A free-text description of the entity. */ - species_id?: CURIE & string; + description?: string | null; /** - * The symbolic chromosome name. For humans, For humans, chromosome names MUST be one of 1..22, X, Y (case-sensitive) + * Alternative name(s) for the Entity. */ - chr: string; + alternativeLabels?: string[] | null; /** - * The chromosome region defined by a CytobandInterval + * A list of extensions to the entity. Extensions are not expected to be natively understood, but may be used for pre-negotiated exchange of message attributes between systems. */ - interval: CytobandInterval; -} -/** - * A contiguous span on a chromosome defined by cytoband features. The span includes - * the constituent regions described by the start and end cytobands, as well as any - * intervening regions. - */ -export interface CytobandInterval { - type?: "CytobandInterval"; + extensions?: Extension[] | null; /** - * The start cytoband region. MUST specify a region nearer the terminal end (telomere) of the chromosome p-arm than `end`. + * A list of mappings to concepts in terminologies or code systems. Each mapping should include a coding and a relation. */ - start: HumanCytoband; + mappings?: ConceptMapping[] | null; /** - * The end cytoband region. MUST specify a region nearer the terminal end (telomere) of the chromosome q-arm than `start`. + * A `GA4GH RefGet ` identifier for the referenced sequence, using the sha512t24u digest. */ - end: HumanCytoband; + refgetAccession: string; + /** + * The interpretation of the character codes referred to by the refget accession, where 'aa' specifies an amino acid character set, and 'na' specifies a nucleic acid character set. + */ + residueAlphabet?: ResidueAlphabet | null; + /** + * A boolean indicating whether the molecule represented by the sequence is circular (true) or linear (false). + */ + circular?: boolean | null; + [k: string]: unknown; } /** * Define TranscriptSegment class */ export interface TranscriptSegmentElement { type?: "TranscriptSegmentElement"; - transcript: CURIE; - exon_start?: number; - exon_start_offset?: number; - exon_end?: number; - exon_end_offset?: number; - gene_descriptor: GeneDescriptor; - element_genomic_start?: LocationDescriptor; - element_genomic_end?: LocationDescriptor; + transcript: string; + exonStart?: number | null; + exonStartOffset?: number | null; + exonEnd?: number | null; + exonEndOffset?: number | null; + gene: Gene; + elementGenomicStart?: SequenceLocation | null; + elementGenomicEnd?: SequenceLocation | null; } /** * Define Gene Element class. */ export interface GeneElement { type?: "GeneElement"; - gene_descriptor: GeneDescriptor; + gene: Gene; } /** * Define Templated Sequence Element class. @@ -304,7 +353,7 @@ export interface GeneElement { */ export interface TemplatedSequenceElement { type?: "TemplatedSequenceElement"; - region: LocationDescriptor; + region: SequenceLocation; strand: Strand; } /** @@ -312,22 +361,45 @@ export interface TemplatedSequenceElement { */ export interface LinkerElement { type?: "LinkerSequenceElement"; - linker_sequence: SequenceDescriptor; + linkerSequence: LiteralSequenceExpression; } /** - * This descriptor is intended to reference VRS Sequence value objects. + * An explicit expression of a Sequence. */ -export interface SequenceDescriptor { - id: CURIE; - type?: "SequenceDescriptor"; - label?: string; - description?: string; - xrefs?: CURIE[]; - alternate_labels?: string[]; - extensions?: Extension[]; - sequence_id?: CURIE; - sequence?: Sequence; - residue_type?: CURIE; +export interface LiteralSequenceExpression { + /** + * The 'logical' identifier of the entity in the system of record, e.g. a UUID. This 'id' is unique within a given system. The identified entity may have a different 'id' in a different system, or may refer to an 'id' for the shared concept in another system (e.g. a CURIE). + */ + id?: string | null; + /** + * MUST be "LiteralSequenceExpression" + */ + type?: "LiteralSequenceExpression"; + /** + * A primary label for the entity. + */ + label?: string | null; + /** + * A free-text description of the entity. + */ + description?: string | null; + /** + * Alternative name(s) for the Entity. + */ + alternativeLabels?: string[] | null; + /** + * A list of extensions to the entity. Extensions are not expected to be natively understood, but may be used for pre-negotiated exchange of message attributes between systems. + */ + extensions?: Extension[] | null; + /** + * A list of mappings to concepts in terminologies or code systems. Each mapping should include a coding and a relation. + */ + mappings?: ConceptMapping[] | null; + /** + * the literal sequence + */ + sequence: SequenceString; + [k: string]: unknown; } /** * Define UnknownGene class. This is primarily intended to represent a @@ -348,36 +420,26 @@ export interface UnknownGeneElement { */ export interface CausativeEvent { type?: "CausativeEvent"; - event_type: EventType; - event_description?: string; -} -/** - * Information pertaining to the assay used in identifying the fusion. - */ -export interface Assay { - type?: "Assay"; - assay_name: string; - assay_id: CURIE; - method_uri: CURIE; - fusion_detection: Evidence; + eventType: EventType; + eventDescription?: string | null; } /** * Response model for domain ID autocomplete suggestion endpoint. */ export interface AssociatedDomainResponse { - warnings?: string[]; + warnings?: string[] | null; gene_id: string; - suggestions?: DomainParams[]; + suggestions?: DomainParams[] | null; } /** * Fields for individual domain suggestion entries */ export interface DomainParams { - interpro_id: CURIE; - domain_name: string; + interproId: string; + domainName: string; start: number; end: number; - refseq_ac: string; + refseqAc: string; } /** * Categorical gene fusions are generalized concepts representing a class @@ -387,16 +449,16 @@ export interface DomainParams { */ export interface CategoricalFusion { type?: "CategoricalFusion"; - regulatory_element?: RegulatoryElement; - structural_elements: ( + regulatoryElement?: RegulatoryElement | null; + structure: ( | TranscriptSegmentElement | GeneElement | TemplatedSequenceElement | LinkerElement | MultiplePossibleGenesElement )[]; - r_frame_preserved?: boolean; - critical_functional_domains?: FunctionalDomain[]; + readingFramePreserved?: boolean | null; + criticalFunctionalDomains?: FunctionalDomain[] | null; } /** * Define MultiplePossibleGenesElement class. This is primarily intended to @@ -417,10 +479,10 @@ export interface MultiplePossibleGenesElement { export interface FunctionalDomain { type?: "FunctionalDomain"; status: DomainStatus; - associated_gene: GeneDescriptor; - _id?: CURIE; - label?: string; - sequence_location?: LocationDescriptor; + associatedGene: Gene; + id: string | null; + label?: string | null; + sequenceLocation?: SequenceLocation | null; } /** * Assayed fusion with client-oriented structural element models. Used in @@ -428,92 +490,94 @@ export interface FunctionalDomain { */ export interface ClientAssayedFusion { type?: "AssayedFusion"; - regulatory_element?: ClientRegulatoryElement; - structural_elements: ( + regulatoryElement?: ClientRegulatoryElement | null; + structure: ( | ClientTranscriptSegmentElement | ClientGeneElement | ClientTemplatedSequenceElement | ClientLinkerElement | ClientUnknownGeneElement )[]; - causative_event: CausativeEvent; - assay: Assay; + readingFramePreserved?: boolean | null; + causativeEvent?: CausativeEvent | null; + assay?: Assay | null; } /** * Define regulatory element object used client-side. */ export interface ClientRegulatoryElement { - type?: "RegulatoryElement"; - regulatory_class: RegulatoryClass; - feature_id?: string; - associated_gene?: GeneDescriptor; - feature_location?: LocationDescriptor; - display_class: string; + elementId: string; nomenclature: string; + type?: "RegulatoryElement"; + regulatoryClass: RegulatoryClass; + featureId?: string | null; + associatedGene?: Gene | null; + featureLocation?: SequenceLocation | null; + displayClass: string; } /** * TranscriptSegment element class used client-side. */ export interface ClientTranscriptSegmentElement { - element_id: string; + elementId: string; nomenclature: string; type?: "TranscriptSegmentElement"; - transcript: CURIE; - exon_start?: number; - exon_start_offset?: number; - exon_end?: number; - exon_end_offset?: number; - gene_descriptor: GeneDescriptor; - element_genomic_start?: LocationDescriptor; - element_genomic_end?: LocationDescriptor; - input_type: "genomic_coords_gene" | "genomic_coords_tx" | "exon_coords_tx"; - input_tx?: string; - input_strand?: Strand; - input_gene?: string; - input_chr?: string; - input_genomic_start?: string; - input_genomic_end?: string; - input_exon_start?: string; - input_exon_start_offset?: string; - input_exon_end?: string; - input_exon_end_offset?: string; + transcript: string; + exonStart?: number | null; + exonStartOffset?: number | null; + exonEnd?: number | null; + exonEndOffset?: number | null; + gene: Gene; + elementGenomicStart?: SequenceLocation | null; + elementGenomicEnd?: SequenceLocation | null; + inputType: "genomic_coords_gene" | "genomic_coords_tx" | "exon_coords_tx"; + inputTx?: string | null; + inputStrand?: Strand | null; + inputGene?: string | null; + inputChr?: string | null; + inputGenomicStart?: string | null; + inputGenomicEnd?: string | null; + inputExonStart?: string | null; + inputExonStartOffset?: string | null; + inputExonEnd?: string | null; + inputExonEndOffset?: string | null; } /** * Gene element used client-side. */ export interface ClientGeneElement { - element_id: string; + elementId: string; nomenclature: string; type?: "GeneElement"; - gene_descriptor: GeneDescriptor; + gene: Gene; } /** * Templated sequence element used client-side. */ export interface ClientTemplatedSequenceElement { - element_id: string; + elementId: string; nomenclature: string; type?: "TemplatedSequenceElement"; - region: LocationDescriptor; + region: SequenceLocation; strand: Strand; - input_chromosome?: string; - input_start?: string; - input_end?: string; + inputChromosome: string | null; + inputStart: string | null; + inputEnd: string | null; } /** * Linker element class used client-side. */ export interface ClientLinkerElement { - element_id: string; + elementId: string; nomenclature: string; type?: "LinkerSequenceElement"; - linker_sequence: SequenceDescriptor; + linkerSequence: LiteralSequenceExpression; } /** * Unknown gene element used client-side. */ export interface ClientUnknownGeneElement { - element_id: string; + elementId: string; nomenclature: string; type?: "UnknownGeneElement"; } @@ -523,22 +587,22 @@ export interface ClientUnknownGeneElement { */ export interface ClientCategoricalFusion { type?: "CategoricalFusion"; - regulatory_element?: ClientRegulatoryElement; - structural_elements: ( + regulatoryElement?: ClientRegulatoryElement | null; + structure: ( | ClientTranscriptSegmentElement | ClientGeneElement | ClientTemplatedSequenceElement | ClientLinkerElement | ClientMultiplePossibleGenesElement )[]; - r_frame_preserved?: boolean; - critical_functional_domains?: ClientFunctionalDomain[]; + readingFramePreserved?: boolean | null; + criticalFunctionalDomains: ClientFunctionalDomain[] | null; } /** * Multiple possible gene element used client-side. */ export interface ClientMultiplePossibleGenesElement { - element_id: string; + elementId: string; nomenclature: string; type?: "MultiplePossibleGenesElement"; } @@ -548,25 +612,25 @@ export interface ClientMultiplePossibleGenesElement { export interface ClientFunctionalDomain { type?: "FunctionalDomain"; status: DomainStatus; - associated_gene: GeneDescriptor; - _id?: CURIE; - label?: string; - sequence_location?: LocationDescriptor; - domain_id: string; + associatedGene: Gene; + id: string | null; + label?: string | null; + sequenceLocation?: SequenceLocation | null; + domainId: string; } /** * Abstract class to provide identification properties used by client. */ export interface ClientStructuralElement { - element_id: string; + elementId: string; nomenclature: string; } /** * Response model for genomic coordinates retrieval */ export interface CoordsUtilsResponse { - warnings?: string[]; - coordinates_data?: GenomicData; + warnings?: string[] | null; + coordinates_data: GenomicData | null; } /** * Model containing genomic and transcript exon data. @@ -574,53 +638,88 @@ export interface CoordsUtilsResponse { export interface GenomicData { gene: string; chr: string; - start?: number; - end?: number; - exon_start?: number; - exon_start_offset?: number; - exon_end?: number; - exon_end_offset?: number; + start?: number | null; + end?: number | null; + exon_start?: number | null; + exon_start_offset?: number | null; + exon_end?: number | null; + exon_end_offset?: number | null; transcript: string; - strand: number; + strand: Strand; } /** * Response model for demo fusion object retrieval endpoints. */ export interface DemoResponse { - warnings?: string[]; + warnings?: string[] | null; fusion: ClientAssayedFusion | ClientCategoricalFusion; } /** * Request model for genomic coordinates retrieval */ export interface ExonCoordsRequest { - tx_ac: string; - gene?: string; - exon_start?: number; - exon_start_offset?: number; - exon_end?: number; - exon_end_offset?: number; + txAc: string; + gene?: string | null; + exonStart?: number | null; + exonStartOffset?: number | null; + exonEnd?: number | null; + exonEndOffset?: number | null; +} +/** + * Assayed fusion with parameters defined as expected in fusor assayed_fusion function + * validate attempts to validate a fusion by constructing it by sending kwargs. In the models and frontend, these are camelCase, + * but the assayed_fusion and categorical_fusion constructors expect snake_case + */ +export interface FormattedAssayedFusion { + fusion_type?: AssayedFusion & string; + structure: + | TranscriptSegmentElement + | GeneElement + | TemplatedSequenceElement + | LinkerElement + | UnknownGeneElement; + causative_event?: CausativeEvent | null; + assay?: Assay | null; + regulatory_element?: RegulatoryElement | null; + reading_frame_preserved?: boolean | null; +} +/** + * Categorical fusion with parameters defined as expected in fusor categorical_fusion function + * validate attempts to validate a fusion by constructing it by sending kwargs. In the models and frontend, these are camelCase, + * but the assayed_fusion and categorical_fusion constructors expect snake_case + */ +export interface FormattedCategoricalFusion { + fusion_type?: CategoricalFusion & string; + structure: + | TranscriptSegmentElement + | GeneElement + | TemplatedSequenceElement + | LinkerElement + | MultiplePossibleGenesElement; + regulatory_element?: RegulatoryElement | null; + critical_functional_domains?: FunctionalDomain[] | null; + reading_frame_preserved?: boolean | null; } /** * Response model for gene element construction endoint. */ export interface GeneElementResponse { - warnings?: string[]; - element?: GeneElement; + warnings?: string[] | null; + element: GeneElement | null; } /** * Response model for functional domain constructor endpoint. */ export interface GetDomainResponse { - warnings?: string[]; - domain?: FunctionalDomain; + warnings?: string[] | null; + domain: FunctionalDomain | null; } /** * Response model for MANE transcript retrieval endpoint. */ export interface GetTranscriptsResponse { - warnings?: string[]; - transcripts?: ManeGeneTranscript[]; + warnings?: string[] | null; + transcripts: ManeGeneTranscript[] | null; } /** * Base object containing MANE-provided gene transcript metadata @@ -637,55 +736,55 @@ export interface ManeGeneTranscript { Ensembl_prot: string; MANE_status: string; GRCh38_chr: string; - chr_start: string; - chr_end: string; + chr_start: number; + chr_end: number; chr_strand: string; } /** * Response model for regulatory element nomenclature endpoint. */ export interface NomenclatureResponse { - warnings?: string[]; - nomenclature?: string; + warnings?: string[] | null; + nomenclature: string | null; } /** * Response model for gene normalization endpoint. */ export interface NormalizeGeneResponse { - warnings?: string[]; + warnings?: string[] | null; term: string; - concept_id?: CURIE; - symbol?: string; - cased?: string; + concept_id: string | null; + symbol: string | null; + cased: string | null; } /** * Response model for regulatory element constructor. */ export interface RegulatoryElementResponse { - warnings?: string[]; - regulatory_element: RegulatoryElement; + warnings?: string[] | null; + regulatoryElement: RegulatoryElement; } /** * Abstract Response class for defining API response structures. */ export interface Response { - warnings?: string[]; + warnings?: string[] | null; } /** * Response model for sequence ID retrieval endpoint. */ export interface SequenceIDResponse { - warnings?: string[]; + warnings?: string[] | null; sequence: string; - refseq_id?: string; - ga4gh_id?: string; - aliases?: string[]; + refseq_id: string | null; + ga4gh_id: string | null; + aliases: string[] | null; } /** * Response model for service_info endpoint. */ export interface ServiceInfoResponse { - warnings?: string[]; + warnings?: string[] | null; curfu_version: string; fusor_version: string; cool_seq_tool_version: string; @@ -694,32 +793,32 @@ export interface ServiceInfoResponse { * Response model for gene autocomplete suggestions endpoint. */ export interface SuggestGeneResponse { - warnings?: string[]; + warnings?: string[] | null; term: string; matches_count: number; - concept_id?: [string, string, string, string, string][]; - symbol?: [string, string, string, string, string][]; - prev_symbols?: [string, string, string, string, string][]; - aliases?: [string, string, string, string, string][]; + concept_id: [unknown, unknown, unknown, unknown, unknown][] | null; + symbol: [unknown, unknown, unknown, unknown, unknown][] | null; + prev_symbols: [unknown, unknown, unknown, unknown, unknown][] | null; + aliases: [unknown, unknown, unknown, unknown, unknown][] | null; } /** * Response model for transcript segment element construction endpoint. */ export interface TemplatedSequenceElementResponse { - warnings?: string[]; - element?: TemplatedSequenceElement; + warnings?: string[] | null; + element: TemplatedSequenceElement | null; } /** * Response model for transcript segment element construction endpoint. */ export interface TxSegmentElementResponse { - warnings?: string[]; - element?: TranscriptSegmentElement; + warnings?: string[] | null; + element: TranscriptSegmentElement | null; } /** * Response model for Fusion validation endpoint. */ export interface ValidateFusionResponse { - warnings?: string[]; - fusion?: CategoricalFusion | AssayedFusion; + warnings?: string[] | null; + fusion?: CategoricalFusion | AssayedFusion | null; } diff --git a/client/src/services/main.tsx b/client/src/services/main.tsx index 9c15b889..9b29ed83 100644 --- a/client/src/services/main.tsx +++ b/client/src/services/main.tsx @@ -30,13 +30,13 @@ import { ClientCategoricalFusion, ClientAssayedFusion, ValidateFusionResponse, - AssayedFusion, - CategoricalFusion, NomenclatureResponse, RegulatoryElement, RegulatoryClass, RegulatoryElementResponse, ClientRegulatoryElement, + FormattedAssayedFusion, + FormattedCategoricalFusion, } from "./ResponseModels"; export enum ElementType { @@ -67,6 +67,20 @@ export type ElementUnion = | TemplatedSequenceElement | TranscriptSegmentElement; +export type AssayedFusionElements = + | GeneElement + | LinkerElement + | UnknownGeneElement + | TemplatedSequenceElement + | TranscriptSegmentElement; + +export type CategoricalFusionElements = + | MultiplePossibleGenesElement + | GeneElement + | LinkerElement + | TemplatedSequenceElement + | TranscriptSegmentElement; + export type ClientFusion = ClientCategoricalFusion | ClientAssayedFusion; /** @@ -78,7 +92,7 @@ export type ClientFusion = ClientCategoricalFusion | ClientAssayedFusion; * to add additional annotations if we want to later. */ export const validateFusion = async ( - fusion: AssayedFusion | CategoricalFusion + fusion: FormattedAssayedFusion | FormattedCategoricalFusion ): Promise => { const response = await fetch("/api/validate", { method: "POST", @@ -217,9 +231,9 @@ export const getFunctionalDomain = async ( geneId: string ): Promise => { const url = - `/api/construct/domain?status=${domainStatus}&name=${domain.domain_name}` + - `&domain_id=${domain.interpro_id}&gene_id=${geneId}` + - `&sequence_id=${domain.refseq_ac}&start=${domain.start}&end=${domain.end}`; + `/api/construct/domain?status=${domainStatus}&name=${domain.domainName}` + + `&domain_id=${domain.interproId}&gene_id=${geneId}` + + `&sequence_id=${domain.refseqAc}&start=${domain.start}&end=${domain.end}`; const response = await fetch(url); const responseJson = await response.json(); return responseJson; @@ -244,10 +258,10 @@ export const getExonCoords = async ( const argsArray = [ `chromosome=${chromosome}`, `strand=${strand === "+" ? "%2B" : "-"}`, - gene !== "" ? `gene=${gene}` : "", - txAc !== "" ? `transcript=${txAc}` : "", - start !== "" ? `start=${start}` : "", - end !== "" ? `end=${end}` : "", + gene && gene !== "" ? `gene=${gene}` : "", + txAc && txAc !== "" ? `transcript=${txAc}` : "", + start && start !== "" ? `start=${start}` : "", + end && end !== "" ? `end=${end}` : "", ]; const args = argsArray.filter((a) => a !== "").join("&"); const response = await fetch(`/api/utilities/get_exon?${args}`); @@ -386,7 +400,7 @@ export const getGeneNomenclature = async ( * @returns nomenclature if successful */ export const getFusionNomenclature = async ( - fusion: AssayedFusion | CategoricalFusion + fusion: FormattedAssayedFusion | FormattedCategoricalFusion ): Promise => { const response = await fetch("/api/nomenclature/fusion", { method: "POST", diff --git a/requirements.txt b/requirements.txt index d19c649b..a03724bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,17 +18,16 @@ charset-normalizer==3.2.0 click==8.1.6 coloredlogs==15.0.1 configparser==6.0.0 -cool-seq-tool==0.1.14.dev0 +cool-seq-tool==0.5.1 cssselect==1.2.0 Cython==3.0.0 decorator==5.1.1 executing==1.2.0 fake-useragent==1.1.3 fastapi==0.100.0 -fusor==0.0.30.dev1 -ga4gh.vrs==0.8.4 -ga4gh.vrsatile.pydantic==0.0.13 -gene-normalizer==0.1.39 +fusor==0.2.0 +ga4gh.vrs==2.0.0a10 +gene-normalizer==0.4.0 h11==0.14.0 hgvs==1.5.4 humanfriendly==10.0 @@ -55,7 +54,7 @@ prompt-toolkit==3.0.39 psycopg2==2.9.6 ptyprocess==0.7.0 pure-eval==0.2.2 -pydantic==1.10.12 +pydantic==2.4.2 pyee==8.2.2 Pygments==2.15.1 pyliftover==0.4 diff --git a/server/pyproject.toml b/server/pyproject.toml index a60a13fe..1f02c35f 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -23,14 +23,15 @@ requires-python = ">=3.10" description = "Curation tool for gene fusions" dependencies = [ "fastapi >= 0.72.0", - "aiofiles", - "asyncpg", - "fusor ~= 0.0.30-dev1", - "sqlparse >= 0.4.2", - "urllib3 >= 1.26.5", + "starlette", + "jinja2", # required for file service "click", - "jinja2", "boto3", + "botocore", + "fusor ~= 0.2.0", + "cool-seq-tool ~= 0.5.1", + "pydantic == 2.4.2", + "gene-normalizer ~= 0.4.0", ] dynamic = ["version"] @@ -47,9 +48,7 @@ dev = [ "ruff == 0.5.0", "black", "pre-commit>=3.7.1", - "gene-normalizer ~= 0.1.39", - "pydantic-to-typescript", - "cool-seq-tool >= 0.4.1" + "pydantic-to-typescript2", ] [project.scripts] @@ -163,9 +162,12 @@ ignore = [ # INP001 - implicit-namespace-package # ARG001 - unused-function-argument # B008 - function-call-in-default-argument +# N803 - invalid-argument-name +# N805 - invalid-first-argument-name-for-method +# N815 - mixed-case-variable-in-class-scope "**/tests/*" = ["ANN001", "ANN2", "ANN102", "S101", "B011", "INP001", "ARG001"] "*__init__.py" = ["F401"] -"**/src/curfu/schemas.py" = ["ANN201", "N805", "ANN001"] +"**/src/curfu/schemas.py" = ["ANN201", "N805", "ANN001", "N803", "N805", "N815"] "**/src/curfu/routers/*" = ["D301", "B008"] "**/src/curfu/cli.py" = ["D301"] diff --git a/server/src/curfu/devtools/build_client_types.py b/server/src/curfu/devtools/build_client_types.py index 04655e4a..f600c7df 100644 --- a/server/src/curfu/devtools/build_client_types.py +++ b/server/src/curfu/devtools/build_client_types.py @@ -7,7 +7,7 @@ def build_client_types() -> None: """Construct type definitions for front-end client.""" - client_dir = Path(__file__).resolve().parents[3] / "client" + client_dir = Path(__file__).resolve().parents[4] / "client" generate_typescript_defs( "curfu.schemas", str((client_dir / "src" / "services" / "ResponseModels.ts").absolute()), diff --git a/server/src/curfu/devtools/build_interpro.py b/server/src/curfu/devtools/build_interpro.py index 4c4c3366..5f75a96d 100644 --- a/server/src/curfu/devtools/build_interpro.py +++ b/server/src/curfu/devtools/build_interpro.py @@ -85,8 +85,8 @@ def get_uniprot_refs() -> UniprotRefs: if uniprot_id in uniprot_ids: continue norm_response = q.normalize(uniprot_id) - norm_id = norm_response.gene_descriptor.gene_id - norm_label = norm_response.gene_descriptor.label + norm_id = norm_response.gene.gene_id + norm_label = norm_response.gene.label uniprot_ids[uniprot_id] = (norm_id, norm_label) if not last_evaluated_key: break diff --git a/server/src/curfu/domain_services.py b/server/src/curfu/domain_services.py index 920545a7..a4239ead 100644 --- a/server/src/curfu/domain_services.py +++ b/server/src/curfu/domain_services.py @@ -38,11 +38,11 @@ def load_mapping(self) -> None: for row in reader: gene_id = row[0].lower() domain_data = { - "interpro_id": f"interpro:{row[2]}", - "domain_name": row[3], + "interproId": f"interpro:{row[2]}", + "domainName": row[3], "start": int(row[4]), "end": int(row[5]), - "refseq_ac": f"{row[6]}", + "refseqAc": f"{row[6]}", } if gene_id in self.domains: self.domains[gene_id].append(domain_data) diff --git a/server/src/curfu/gene_services.py b/server/src/curfu/gene_services.py index e8998310..f6d7ee6d 100644 --- a/server/src/curfu/gene_services.py +++ b/server/src/curfu/gene_services.py @@ -3,7 +3,6 @@ import csv from pathlib import Path -from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE from gene.query import QueryHandler from gene.schemas import MatchType @@ -50,8 +49,9 @@ def __init__(self, suggestions_file: Path | None = None) -> None: @staticmethod def get_normalized_gene( term: str, normalizer: QueryHandler - ) -> tuple[CURIE, str, str | CURIE | None]: + ) -> tuple[str, str, str | None]: """Get normalized ID given gene symbol/label/alias. + :param term: user-entered gene term :param normalizer: gene normalizer instance :return: concept ID, str, if successful @@ -59,13 +59,13 @@ def get_normalized_gene( """ response = normalizer.normalize(term) if response.match_type != MatchType.NO_MATCH: - gd = response.gene_descriptor - if not gd or not gd.gene_id: + concept_id = response.normalized_id + gene = response.gene + if not concept_id or not response.gene: msg = f"Unexpected null property in normalized response for `{term}`" logger.error(msg) raise LookupServiceError(msg) - concept_id = gd.gene_id - symbol = gd.label + symbol = gene.label if not symbol: msg = f"Unable to retrieve symbol for gene {concept_id}" logger.error(msg) @@ -78,7 +78,7 @@ def get_normalized_gene( elif term_lower == concept_id.lower(): term_cased = concept_id elif response.match_type == 80: - for ext in gd.extensions: + for ext in gene.extensions: if ext.name == "previous_symbols": for prev_symbol in ext.value: if term_lower == prev_symbol.lower(): @@ -86,18 +86,18 @@ def get_normalized_gene( break break elif response.match_type == 60: - if gd.alternate_labels: - for alias in gd.alternate_labels: + if gene.alternate_labels: + for alias in gene.alternate_labels: if term_lower == alias.lower(): term_cased = alias break - if not term_cased and gd.xrefs: - for xref in gd.xrefs: + if not term_cased and gene.xrefs: + for xref in gene.xrefs: if term_lower == xref.lower(): term_cased = xref break if not term_cased: - for ext in gd.extensions: + for ext in gene.extensions: if ext.name == "associated_with": for assoc in ext.value: if term_lower == assoc.lower(): @@ -106,7 +106,7 @@ def get_normalized_gene( break if not term_cased: logger.warning( - f"Couldn't find cased version for search term {term} matching gene ID {response.gene_descriptor.gene_id}" + f"Couldn't find cased version for search term {term} matching gene ID {response.normalized_id}" ) return (concept_id, symbol, term_cased) warn = f"Lookup of gene term {term} failed." diff --git a/server/src/curfu/main.py b/server/src/curfu/main.py index 0f5df845..4a1529dd 100644 --- a/server/src/curfu/main.py +++ b/server/src/curfu/main.py @@ -1,5 +1,6 @@ """Provide FastAPI application and route declarations.""" +import logging from collections.abc import AsyncGenerator from contextlib import asynccontextmanager @@ -25,6 +26,23 @@ validate, ) +_logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator: + """Configure FastAPI instance lifespan. + + :param app: FastAPI app instance + :return: async context handler + """ + app.state.fusor = await start_fusor() + app.state.genes = get_gene_services() + app.state.domains = get_domain_services() + yield + await app.state.fusor.cool_seq_tool.uta_db._connection_pool.close() # noqa: SLF001 + + fastapi_app = FastAPI( title="Fusion Curation API", description="Provide data functions to support [VICC Fusion Curation interface](fusion-builder.cancervariants.org/).", @@ -41,6 +59,7 @@ swagger_ui_parameters={"tryItOutEnabled": True}, docs_url="/docs", openapi_url="/openapi.json", + lifespan=lifespan, ) fastapi_app.include_router(utilities.router) @@ -66,32 +85,46 @@ def serve_react_app(app: FastAPI) -> FastAPI: - """Wrap application initialization in Starlette route param converter. + """Wrap application initialization in Starlette route param converter. This ensures + that the static web client files can be served from the backend. + + Client source must be available at the location specified by `BUILD_DIR` in a + production environment. However, this may not be necessary during local development, + so the `RuntimeError` is simply caught and logged. + + For the live service, `.ebextensions/01_build.config` includes code to build a + production version of the client and move it to the proper location. :param app: FastAPI application instance :return: application with React frontend mounted """ - app.mount( - "/static/", - StaticFiles(directory=BUILD_DIR / "static"), - name="React application static files", - ) - templates = Jinja2Templates(directory=BUILD_DIR.as_posix()) - - @app.get("/{full_path:path}", include_in_schema=False) - async def serve_react_app(request: Request, full_path: str) -> TemplateResponse: # noqa: ARG001 - """Add arbitrary path support to FastAPI service. - - React-router provides something akin to client-side routing based out - of the Javascript embedded in index.html. However, FastAPI will intercede - and handle all client requests, and will 404 on any non-server-defined paths. - This function reroutes those otherwise failed requests against the React-Router - client, allowing it to redirect the client to the appropriate location. - :param request: client request object - :param full_path: request path - :return: Starlette template response object - """ - return templates.TemplateResponse("index.html", {"request": request}) + try: + static_files = StaticFiles(directory=BUILD_DIR / "static") + except RuntimeError: + _logger.error("Unable to access static build files -- does the folder exist?") + else: + app.mount( + "/static/", + static_files, + name="React application static files", + ) + templates = Jinja2Templates(directory=BUILD_DIR.as_posix()) + + @app.get("/{full_path:path}", include_in_schema=False) + async def serve_react_app(request: Request, full_path: str) -> TemplateResponse: # noqa: ARG001 + """Add arbitrary path support to FastAPI service. + + React-router provides something akin to client-side routing based out + of the Javascript embedded in index.html. However, FastAPI will intercede + and handle all client requests, and will 404 on any non-server-defined paths. + This function reroutes those otherwise failed requests against the React-Router + client, allowing it to redirect the client to the appropriate location. + + :param request: client request object + :param full_path: request path + :return: Starlette template response object + """ + return templates.TemplateResponse("index.html", {"request": request}) return app @@ -125,17 +158,3 @@ def get_domain_services() -> DomainService: domain_service = DomainService() domain_service.load_mapping() return domain_service - - -@asynccontextmanager -async def lifespan(app: FastAPI) -> AsyncGenerator: - """Configure FastAPI instance lifespan. - - :param app: FastAPI app instance - :return: async context handler - """ - app.state.fusor = await start_fusor() - app.state.genes = get_gene_services() - app.state.domains = get_domain_services() - yield - await app.state.fusor.cool_seq_tool.uta_db._connection_pool.close() # noqa: SLF001 diff --git a/server/src/curfu/routers/constructors.py b/server/src/curfu/routers/constructors.py index a7cae563..13366249 100644 --- a/server/src/curfu/routers/constructors.py +++ b/server/src/curfu/routers/constructors.py @@ -1,7 +1,7 @@ """Provide routes for element construction endpoints""" from fastapi import APIRouter, Query, Request -from fusor.models import DomainStatus, RegulatoryClass, Strand +from fusor.models import DomainStatus, RegulatoryClass from pydantic import ValidationError from curfu import logger @@ -198,7 +198,7 @@ def build_templated_sequence_element( otherwise """ try: - strand_n = Strand(strand) + strand_n = get_strand(strand) except ValueError: warning = f"Received invalid strand value: {strand}" logger.warning(warning) @@ -208,7 +208,6 @@ def build_templated_sequence_element( end=end, sequence_id=parse_identifier(sequence_id), strand=strand_n, - add_location_id=True, ) return TemplatedSequenceElementResponse(element=element, warnings=[]) @@ -284,4 +283,4 @@ def build_regulatory_element( element, warnings = request.app.state.fusor.regulatory_element( normalized_class, gene_name ) - return {"regulatory_element": element, "warnings": warnings} + return {"regulatoryElement": element, "warnings": warnings} diff --git a/server/src/curfu/routers/demo.py b/server/src/curfu/routers/demo.py index bd395c84..c155b8b3 100644 --- a/server/src/curfu/routers/demo.py +++ b/server/src/curfu/routers/demo.py @@ -65,14 +65,14 @@ def clientify_structural_element( fusor_instance: FUSOR, ) -> ClientElementUnion: """Add fields required by client to structural element object. - \f + :param element: a structural element object :param fusor_instance: instantiated FUSOR object, passed down from FastAPI request context :return: client-ready structural element """ element_args = element.dict() - element_args["element_id"] = str(uuid4()) + element_args["elementId"] = str(uuid4()) if element.type == StructuralElementType.UNKNOWN_GENE_ELEMENT: element_args["nomenclature"] = "?" @@ -81,17 +81,17 @@ def clientify_structural_element( element_args["nomenclature"] = "v" return ClientMultiplePossibleGenesElement(**element_args) if element.type == StructuralElementType.LINKER_SEQUENCE_ELEMENT: - nm = element.linker_sequence.sequence + nm = element.linkerSequence.sequence.root element_args["nomenclature"] = nm return ClientLinkerElement(**element_args) if element.type == StructuralElementType.TEMPLATED_SEQUENCE_ELEMENT: nm = templated_seq_nomenclature(element, fusor_instance.seqrepo) element_args["nomenclature"] = nm - element_args["input_chromosome"] = element.region.location.sequence_id.split( + element_args["inputChromosome"] = element.region.sequenceReference.id.split( ":" )[1] - element_args["input_start"] = element.region.location.interval.start.value - element_args["input_end"] = element.region.location.interval.end.value + element_args["inputStart"] = element.region.start + element_args["inputEnd"] = element.region.end return ClientTemplatedSequenceElement(**element_args) if element.type == StructuralElementType.GENE_ELEMENT: nm = gene_nomenclature(element) @@ -100,12 +100,13 @@ def clientify_structural_element( if element.type == StructuralElementType.TRANSCRIPT_SEGMENT_ELEMENT: nm = tx_segment_nomenclature(element) element_args["nomenclature"] = nm - element_args["input_type"] = "exon_coords_tx" - element_args["input_tx"] = element.transcript.split(":")[1] - element_args["input_exon_start"] = element.exon_start - element_args["input_exon_start_offset"] = element.exon_start_offset - element_args["input_exon_end"] = element.exon_end - element_args["input_exon_end_offset"] = element.exon_end_offset + element_args["inputType"] = "exon_coords_tx" + element_args["inputTx"] = element.transcript.split(":")[1] + element_args["inputExonStart"] = str(element.exonStart) + element_args["inputExonStartOffset"] = str(element.exonStartOffset) + element_args["inputExonEnd"] = str(element.exonEnd) + element_args["inputExonEndOffset"] = str(element.exonEndOffset) + element_args["inputGene"] = element.gene.label return ClientTranscriptSegmentElement(**element_args) msg = "Unknown element type provided" raise ValueError(msg) @@ -121,32 +122,33 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: fusion_args = fusion.dict() client_elements = [ clientify_structural_element(element, fusor_instance) - for element in fusion.structural_elements + for element in fusion.structure ] - fusion_args["structural_elements"] = client_elements + fusion_args["structure"] = client_elements - if fusion_args.get("regulatory_element"): - reg_element_args = fusion_args["regulatory_element"] + if fusion_args.get("regulatoryElement"): + reg_element_args = fusion_args["regulatoryElement"] nomenclature = reg_element_nomenclature( RegulatoryElement(**reg_element_args), fusor_instance.seqrepo ) reg_element_args["nomenclature"] = nomenclature - regulatory_class = fusion_args["regulatory_element"]["regulatory_class"] + regulatory_class = fusion_args["regulatoryElement"]["regulatoryClass"] if regulatory_class == "enhancer": - reg_element_args["display_class"] = "Enhancer" + reg_element_args["displayClass"] = "Enhancer" else: msg = "Undefined reg element class used in demo" raise Exception(msg) - fusion_args["regulatory_element"] = reg_element_args + reg_element_args["elementId"] = str(uuid4()) + fusion_args["regulatoryElement"] = reg_element_args if fusion.type == FUSORTypes.CATEGORICAL_FUSION: - if fusion.critical_functional_domains: + if fusion.criticalFunctionalDomains: client_domains = [] - for domain in fusion.critical_functional_domains: + for domain in fusion.criticalFunctionalDomains: client_domain = domain.dict() - client_domain["domain_id"] = str(uuid4()) + client_domain["domainId"] = str(uuid4()) client_domains.append(client_domain) - fusion_args["critical_functional_domains"] = client_domains + fusion_args["criticalFunctionalDomains"] = client_domains return ClientCategoricalFusion(**fusion_args) if fusion.type == FUSORTypes.ASSAYED_FUSION: return ClientAssayedFusion(**fusion_args) @@ -163,6 +165,7 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: ) def get_alk(request: Request) -> DemoResponse: """Retrieve ALK assayed fusion. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -181,6 +184,7 @@ def get_alk(request: Request) -> DemoResponse: ) def get_ewsr1(request: Request) -> DemoResponse: """Retrieve EWSR1 assayed fusion. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -217,6 +221,7 @@ def get_bcr_abl1(request: Request) -> DemoResponse: ) def get_tpm3_ntrk1(request: Request) -> DemoResponse: """Retrieve TPM3-NTRK1 assayed fusion. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -236,6 +241,7 @@ def get_tpm3_ntrk1(request: Request) -> DemoResponse: ) def get_tpm3_pdgfrb(request: Request) -> DemoResponse: """Retrieve TPM3-PDGFRB assayed fusion. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -255,6 +261,8 @@ def get_tpm3_pdgfrb(request: Request) -> DemoResponse: ) def get_igh_myc(request: Request) -> DemoResponse: """Retrieve IGH-MYC assayed fusion. + + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. """ diff --git a/server/src/curfu/routers/lookup.py b/server/src/curfu/routers/lookup.py index 6ebaf038..12b22766 100644 --- a/server/src/curfu/routers/lookup.py +++ b/server/src/curfu/routers/lookup.py @@ -15,7 +15,7 @@ response_model_exclude_none=True, tags=[RouteTag.LOOKUP], ) -def normalize_gene(request: Request, term: str = Query("")) -> ResponseDict: +def normalize_gene(request: Request, term: str = Query("")) -> NormalizeGeneResponse: """Normalize gene term provided by user. \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR @@ -33,4 +33,7 @@ def normalize_gene(request: Request, term: str = Query("")) -> ResponseDict: response["cased"] = cased except LookupServiceError as e: response["warnings"] = [str(e)] - return response + response["concept_id"] = None + response["symbol"] = None + response["cased"] = None + return NormalizeGeneResponse(**response) diff --git a/server/src/curfu/routers/meta.py b/server/src/curfu/routers/meta.py index 506cd103..a0a29073 100644 --- a/server/src/curfu/routers/meta.py +++ b/server/src/curfu/routers/meta.py @@ -1,6 +1,6 @@ """Provide service meta information""" -from cool_seq_tool.version import __version__ as cool_seq_tool_version +from cool_seq_tool import __version__ as cool_seq_tool_version from fastapi import APIRouter from fusor import __version__ as fusor_version diff --git a/server/src/curfu/routers/nomenclature.py b/server/src/curfu/routers/nomenclature.py index 55b83743..66c9db6c 100644 --- a/server/src/curfu/routers/nomenclature.py +++ b/server/src/curfu/routers/nomenclature.py @@ -18,6 +18,7 @@ from curfu import logger from curfu.schemas import NomenclatureResponse, ResponseDict, RouteTag +from curfu.sequence_services import get_strand router = APIRouter() @@ -47,7 +48,7 @@ def generate_regulatory_element_nomenclature( logger.warning( f"Encountered ValidationError: {error_msg} for regulatory element: {regulatory_element}" ) - return {"warnings": [error_msg]} + return {"nomenclature": "", "warnings": [error_msg]} try: nomenclature = reg_element_nomenclature( structured_reg_element, request.app.state.fusor.seqrepo @@ -57,9 +58,10 @@ def generate_regulatory_element_nomenclature( f"Encountered parameter errors for regulatory element: {regulatory_element}" ) return { + "nomenclature": "", "warnings": [ f"Unable to validate regulatory element with provided parameters: {regulatory_element}" - ] + ], } return {"nomenclature": nomenclature} @@ -87,7 +89,7 @@ def generate_tx_segment_nomenclature(tx_segment: dict = Body()) -> ResponseDict: logger.warning( f"Encountered ValidationError: {error_msg} for tx segment: {tx_segment}" ) - return {"warnings": [error_msg]} + return {"nomenclature": "", "warnings": [error_msg]} nomenclature = tx_segment_nomenclature(structured_tx_segment) return {"nomenclature": nomenclature} @@ -110,13 +112,20 @@ def generate_templated_seq_nomenclature( :return: response with nomenclature if successful and warnings otherwise """ try: + # convert client input of +/- for strand + strand = ( + get_strand(templated_sequence.get("strand")) + if templated_sequence.get("strand") is not None + else None + ) + templated_sequence["strand"] = strand structured_templated_seq = TemplatedSequenceElement(**templated_sequence) except ValidationError as e: error_msg = str(e) logger.warning( f"Encountered ValidationError: {error_msg} for templated sequence element: {templated_sequence}" ) - return {"warnings": [error_msg]} + return {"nomenclature": "", "warnings": [error_msg]} try: nomenclature = templated_seq_nomenclature( structured_templated_seq, request.app.state.fusor.seqrepo @@ -126,9 +135,10 @@ def generate_templated_seq_nomenclature( f"Encountered parameter errors for templated sequence: {templated_sequence}" ) return { + "nomenclature": "", "warnings": [ f"Unable to validate templated sequence with provided parameters: {templated_sequence}" - ] + ], } return {"nomenclature": nomenclature} @@ -155,15 +165,16 @@ def generate_gene_nomenclature(gene_element: dict = Body()) -> ResponseDict: logger.warning( f"Encountered ValidationError: {error_msg} for gene element: {gene_element}" ) - return {"warnings": [error_msg]} + return {"nomenclature": "", "warnings": [error_msg]} try: nomenclature = gene_nomenclature(valid_gene_element) except ValueError: logger.warning(f"Encountered parameter errors for gene element: {gene_element}") return { + "nomenclature": "", "warnings": [ f"Unable to validate gene element with provided parameters: {gene_element}" - ] + ], } return {"nomenclature": nomenclature} @@ -188,6 +199,6 @@ def generate_fusion_nomenclature( try: valid_fusion = request.app.state.fusor.fusion(**fusion) except FUSORParametersException as e: - return {"warnings": [str(e)]} + return {"nomenclature": "", "warnings": [str(e)]} nomenclature = request.app.state.fusor.generate_nomenclature(valid_fusion) return {"nomenclature": nomenclature} diff --git a/server/src/curfu/routers/utilities.py b/server/src/curfu/routers/utilities.py index 7f5d88da..c221fe1c 100644 --- a/server/src/curfu/routers/utilities.py +++ b/server/src/curfu/routers/utilities.py @@ -38,10 +38,10 @@ def get_mane_transcripts(request: Request, term: str) -> dict: """ normalized = request.app.state.fusor.gene_normalizer.normalize(term) if normalized.match_type == gene_schemas.MatchType.NO_MATCH: - return {"warnings": [f"Normalization error: {term}"]} - if not normalized.gene_descriptor.gene_id.lower().startswith("hgnc"): - return {"warnings": [f"No HGNC symbol: {term}"]} - symbol = normalized.gene_descriptor.label + return {"warnings": [f"Normalization error: {term}"], "transcripts": None} + if not normalized.normalized_id.startswith("hgnc"): + return {"warnings": [f"No HGNC symbol: {term}"], "transcripts": None} + symbol = normalized.gene.label transcripts = request.app.state.fusor.cool_seq_tool.mane_transcript_mappings.get_gene_mane_data( symbol ) @@ -107,16 +107,13 @@ async def get_genome_coords( if exon_end is not None and exon_end_offset is None: exon_end_offset = 0 - response = ( - await request.app.state.fusor.cool_seq_tool.transcript_to_genomic_coordinates( - gene=gene, - transcript=transcript, - exon_start=exon_start, - exon_end=exon_end, - exon_start_offset=exon_start_offset, - exon_end_offset=exon_end_offset, - residue_mode="inter-residue", - ) + response = await request.app.state.fusor.cool_seq_tool.ex_g_coords_mapper.transcript_to_genomic_coordinates( + transcript=transcript, + gene=gene, + exon_start=exon_start, + exon_end=exon_end, + exon_start_offset=exon_start_offset, + exon_end_offset=exon_end_offset, ) warnings = response.warnings if warnings: @@ -170,8 +167,8 @@ async def get_exon_coords( logger.warning(warning) return CoordsUtilsResponse(warnings=warnings, coordinates_data=None) - response = await request.app.state.fusor.cool_seq_tool.genomic_to_transcript_exon_coordinates( - chromosome, + response = await request.app.state.fusor.cool_seq_tool.ex_g_coords_mapper.genomic_to_transcript_exon_coordinates( + alt_ac=chromosome, start=start, end=end, strand=strand_validated, @@ -200,7 +197,7 @@ async def get_sequence_id(request: Request, sequence: str) -> SequenceIDResponse :param sequence_id: user-provided sequence identifier to translate :return: Response object with ga4gh ID and aliases """ - params: dict[str, Any] = {"sequence": sequence, "ga4gh_id": None, "aliases": []} + params: dict[str, Any] = {"sequence": sequence} sr = request.app.state.fusor.cool_seq_tool.seqrepo_access sr_ids, errors = sr.translate_identifier(sequence) @@ -237,8 +234,7 @@ async def get_sequence_id(request: Request, sequence: str) -> SequenceIDResponse @router.get( "/api/utilities/download_sequence", summary="Get sequence for ID", - description="Given a known accession identifier, retrieve sequence data and return" - "as a FASTA file", + description="Given a known accession identifier, retrieve sequence data and return as a FASTA file", response_class=FileResponse, tags=[RouteTag.UTILITIES], ) @@ -250,6 +246,7 @@ async def get_sequence( ), ) -> FileResponse: """Get sequence for requested sequence ID. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -260,7 +257,9 @@ async def get_sequence( """ _, path = tempfile.mkstemp(suffix=".fasta") try: - request.app.state.fusor.cool_seq_tool.get_fasta_file(sequence_id, Path(path)) + request.app.state.fusor.cool_seq_tool.seqrepo_access.get_fasta_file( + sequence_id, Path(path) + ) except KeyError as ke: resp = ( request.app.state.fusor.cool_seq_tool.seqrepo_access.translate_identifier( diff --git a/server/src/curfu/routers/validate.py b/server/src/curfu/routers/validate.py index 5087b810..8ee72c54 100644 --- a/server/src/curfu/routers/validate.py +++ b/server/src/curfu/routers/validate.py @@ -17,6 +17,9 @@ ) def validate_fusion(request: Request, fusion: dict = Body()) -> ResponseDict: """Validate proposed Fusion object. Return warnings if invalid. + + For reasons that hopefully change someday, messages transmitted to this endpoint + should use snake_case for property keys at the first level of depth. \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR. :param proposed_fusion: the POSTed object generated by the client. This should diff --git a/server/src/curfu/schemas.py b/server/src/curfu/schemas.py index 5976d939..b5818ee3 100644 --- a/server/src/curfu/schemas.py +++ b/server/src/curfu/schemas.py @@ -5,10 +5,15 @@ from cool_seq_tool.schemas import GenomicData from fusor.models import ( + Assay, AssayedFusion, + AssayedFusionElements, CategoricalFusion, + CategoricalFusionElements, + CausativeEvent, FunctionalDomain, Fusion, + FusionType, GeneElement, LinkerElement, MultiplePossibleGenesElement, @@ -18,14 +23,20 @@ TranscriptSegmentElement, UnknownGeneElement, ) -from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE -from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, validator +from pydantic import ( + BaseModel, + ConfigDict, + Field, + StrictInt, + StrictStr, + field_validator, +) ResponseWarnings = list[StrictStr] | None ResponseDict = dict[ str, - str | int | CURIE | list[str] | list[tuple[str, str, str, str]] | FunctionalDomain, + str | int | list[str] | list[tuple[str, str, str, str]] | FunctionalDomain | None, ] Warnings = list[str] @@ -33,28 +44,28 @@ class ClientStructuralElement(BaseModel): """Abstract class to provide identification properties used by client.""" - element_id: StrictStr + elementId: StrictStr nomenclature: StrictStr class ClientTranscriptSegmentElement(TranscriptSegmentElement, ClientStructuralElement): """TranscriptSegment element class used client-side.""" - input_type: ( + inputType: ( Literal["genomic_coords_gene"] | Literal["genomic_coords_tx"] | Literal["exon_coords_tx"] ) - input_tx: str | None - input_strand: Strand | None - input_gene: str | None - input_chr: str | None - input_genomic_start: str | None - input_genomic_end: str | None - input_exon_start: str | None - input_exon_start_offset: str | None - input_exon_end: str | None - input_exon_end_offset: str | None + inputTx: str | None = None + inputStrand: Strand | None = None + inputGene: str | None = None + inputChr: str | None = None + inputGenomicStart: str | None = None + inputGenomicEnd: str | None = None + inputExonStart: str | None = None + inputExonStartOffset: str | None = None + inputExonEnd: str | None = None + inputExonEndOffset: str | None = None class ClientLinkerElement(LinkerElement, ClientStructuralElement): @@ -64,9 +75,9 @@ class ClientLinkerElement(LinkerElement, ClientStructuralElement): class ClientTemplatedSequenceElement(TemplatedSequenceElement, ClientStructuralElement): """Templated sequence element used client-side.""" - input_chromosome: str | None - input_start: str | None - input_end: str | None + inputChromosome: str | None + inputStart: str | None + inputEnd: str | None class ClientGeneElement(GeneElement, ClientStructuralElement): @@ -86,22 +97,22 @@ class ClientMultiplePossibleGenesElement( class ClientFunctionalDomain(FunctionalDomain): """Define functional domain object used client-side.""" - domain_id: str + domainId: str model_config = ConfigDict(extra="forbid") -class ClientRegulatoryElement(RegulatoryElement): +class ClientRegulatoryElement(RegulatoryElement, ClientStructuralElement): """Define regulatory element object used client-side.""" - display_class: str + displayClass: str nomenclature: str class Response(BaseModel): """Abstract Response class for defining API response structures.""" - warnings: ResponseWarnings + warnings: ResponseWarnings | None = None model_config = ConfigDict(extra="forbid") @@ -128,7 +139,7 @@ class NormalizeGeneResponse(Response): """Response model for gene normalization endpoint.""" term: StrictStr - concept_id: CURIE | None + concept_id: StrictStr | None symbol: StrictStr | None cased: StrictStr | None @@ -148,11 +159,11 @@ class SuggestGeneResponse(Response): class DomainParams(BaseModel): """Fields for individual domain suggestion entries""" - interpro_id: CURIE - domain_name: StrictStr + interproId: StrictStr + domainName: StrictStr start: int end: int - refseq_ac: StrictStr + refseqAc: StrictStr class GetDomainResponse(Response): @@ -165,33 +176,33 @@ class AssociatedDomainResponse(Response): """Response model for domain ID autocomplete suggestion endpoint.""" gene_id: StrictStr - suggestions: list[DomainParams] | None + suggestions: list[DomainParams] | None = None class ValidateFusionResponse(Response): """Response model for Fusion validation endpoint.""" - fusion: Fusion | None + fusion: Fusion | None = None class ExonCoordsRequest(BaseModel): """Request model for genomic coordinates retrieval""" - tx_ac: StrictStr + txAc: StrictStr gene: StrictStr | None = "" - exon_start: StrictInt | None = 0 - exon_start_offset: StrictInt | None = 0 - exon_end: StrictInt | None = 0 - exon_end_offset: StrictInt | None = 0 + exonStart: StrictInt | None = 0 + exonStartOffset: StrictInt | None = 0 + exonEnd: StrictInt | None = 0 + exonEndOffset: StrictInt | None = 0 - @validator("gene") + @field_validator("gene") def validate_gene(cls, v) -> str: """Replace None with empty string.""" if v is None: return "" return v - @validator("exon_start", "exon_start_offset", "exon_end", "exon_end_offset") + @field_validator("exonStart", "exonStartOffset", "exonEnd", "exonEndOffset") def validate_number(cls, v) -> int: """Replace None with 0 for numeric fields.""" if v is None: @@ -209,9 +220,9 @@ class SequenceIDResponse(Response): """Response model for sequence ID retrieval endpoint.""" sequence: StrictStr - refseq_id: StrictStr | None - ga4gh_id: StrictStr | None - aliases: list[StrictStr] | None + refseq_id: StrictStr | None = None + ga4gh_id: StrictStr | None = None + aliases: list[StrictStr] | None = None class ManeGeneTranscript(BaseModel): @@ -228,8 +239,8 @@ class ManeGeneTranscript(BaseModel): Ensembl_prot: str MANE_status: str GRCh38_chr: str - chr_start: str - chr_end: str + chr_start: int + chr_end: int chr_strand: str @@ -257,15 +268,15 @@ class ClientCategoricalFusion(CategoricalFusion): global FusionContext. """ - regulatory_element: ClientRegulatoryElement | None = None - structural_elements: list[ + regulatoryElement: ClientRegulatoryElement | None = None + structure: list[ ClientTranscriptSegmentElement | ClientGeneElement | ClientTemplatedSequenceElement | ClientLinkerElement | ClientMultiplePossibleGenesElement ] - critical_functional_domains: list[ClientFunctionalDomain] | None + criticalFunctionalDomains: list[ClientFunctionalDomain] | None class ClientAssayedFusion(AssayedFusion): @@ -273,8 +284,8 @@ class ClientAssayedFusion(AssayedFusion): global FusionContext. """ - regulatory_element: ClientRegulatoryElement | None = None - structural_elements: list[ + regulatoryElement: ClientRegulatoryElement | None = None + structure: list[ ClientTranscriptSegmentElement | ClientGeneElement | ClientTemplatedSequenceElement @@ -283,6 +294,33 @@ class ClientAssayedFusion(AssayedFusion): ] +class FormattedAssayedFusion(BaseModel): + """Assayed fusion with parameters defined as expected in fusor assayed_fusion function + validate attempts to validate a fusion by constructing it by sending kwargs. In the models and frontend, these are camelCase, + but the assayed_fusion and categorical_fusion constructors expect snake_case + """ + + fusion_type: FusionType.ASSAYED_FUSION = FusionType.ASSAYED_FUSION + structure: AssayedFusionElements + causative_event: CausativeEvent | None = None + assay: Assay | None = None + regulatory_element: RegulatoryElement | None = None + reading_frame_preserved: bool | None = None + + +class FormattedCategoricalFusion(BaseModel): + """Categorical fusion with parameters defined as expected in fusor categorical_fusion function + validate attempts to validate a fusion by constructing it by sending kwargs. In the models and frontend, these are camelCase, + but the assayed_fusion and categorical_fusion constructors expect snake_case + """ + + fusion_type: FusionType.CATEGORICAL_FUSION = FusionType.CATEGORICAL_FUSION + structure: CategoricalFusionElements + regulatory_element: RegulatoryElement | None = None + critical_functional_domains: list[FunctionalDomain] | None = None + reading_frame_preserved: bool | None = None + + class NomenclatureResponse(Response): """Response model for regulatory element nomenclature endpoint.""" @@ -292,7 +330,7 @@ class NomenclatureResponse(Response): class RegulatoryElementResponse(Response): """Response model for regulatory element constructor.""" - regulatory_element: RegulatoryElement + regulatoryElement: RegulatoryElement class DemoResponse(Response): diff --git a/server/src/curfu/sequence_services.py b/server/src/curfu/sequence_services.py index 376a7a21..eea3d12e 100644 --- a/server/src/curfu/sequence_services.py +++ b/server/src/curfu/sequence_services.py @@ -2,6 +2,8 @@ import logging +from cool_seq_tool.schemas import Strand + logger = logging.getLogger("curfu") logger.setLevel(logging.DEBUG) @@ -18,7 +20,7 @@ def get_strand(strand_input: str) -> int: :raise InvalidInputException: if strand arg is invalid """ if strand_input == "+": - return 1 + return Strand.POSITIVE if strand_input == "-": - return -1 + return Strand.NEGATIVE raise InvalidInputError diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 0a6275a9..4b70125b 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -3,11 +3,12 @@ from collections.abc import Callable import pytest +import pytest_asyncio from curfu.main import app, get_domain_services, get_gene_services, start_fusor from httpx import ASGITransport, AsyncClient -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def async_client(): """Provide httpx async client fixture.""" app.state.fusor = await start_fusor() @@ -21,7 +22,7 @@ async def async_client(): response_callback_type = Callable[[dict, dict], None] -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def check_response(async_client): """Provide base response check function. Use in individual tests.""" @@ -53,113 +54,103 @@ async def check_response( return check_response +@pytest.fixture(scope="session") +def check_sequence_location(): + """Check that a sequence location is valid + :param dict sequence_location: sequence location structure + """ + + def check_sequence_location(sequence_location): + assert "ga4gh:SL." in sequence_location.get("id") + assert sequence_location.get("type") == "SequenceLocation" + sequence_reference = sequence_location.get("sequenceReference", {}) + assert "refseq:" in sequence_reference.get("id") + assert sequence_reference.get("refgetAccession") + assert sequence_reference.get("type") == "SequenceReference" + + return check_sequence_location + + @pytest.fixture(scope="module") -def alk_descriptor(): - """Gene descriptor for ALK gene""" +def alk_gene(): + """Gene object for ALK""" return { - "id": "normalize.gene:hgnc%3A427", - "type": "GeneDescriptor", + "type": "Gene", "label": "ALK", - "gene_id": "hgnc:427", + "id": "hgnc:427", } @pytest.fixture(scope="module") -def tpm3_descriptor(): - """Gene descriptor for TPM3 gene""" +def tpm3_gene(): + """Gene object for TPM3""" return { - "id": "normalize.gene:TPM3", - "type": "GeneDescriptor", + "type": "Gene", "label": "TPM3", - "gene_id": "hgnc:12012", + "id": "hgnc:12012", } @pytest.fixture(scope="module") -def ntrk1_descriptor(): - """Gene descriptor for NTRK1 gene""" +def ntrk1_gene(): + """Gene object for NTRK1""" return { - "id": "normalize.gene:NTRK1", - "type": "GeneDescriptor", + "type": "Gene", "label": "NTRK1", - "gene_id": "hgnc:8031", + "id": "hgnc:8031", } @pytest.fixture(scope="module") -def alk_gene_element(alk_descriptor): +def alk_gene_element(alk_gene): """Provide GeneElement containing ALK gene""" - return {"type": "GeneElement", "gene_descriptor": alk_descriptor} + return {"type": "GeneElement", "gene": alk_gene} @pytest.fixture(scope="module") -def ntrk1_tx_element_start(ntrk1_descriptor): +def ntrk1_tx_element_start(ntrk1_gene): """Provide TranscriptSegmentElement for NTRK1 constructed with exon coordinates, and only providing starting position. """ return { "type": "TranscriptSegmentElement", "transcript": "refseq:NM_002529.3", - "exon_start": 2, - "exon_start_offset": 1, - "gene_descriptor": ntrk1_descriptor, - "element_genomic_start": { + "exonStart": 2, + "exonStartOffset": 1, + "gene": ntrk1_gene, + "elementGenomicStart": { "id": "fusor.location_descriptor:NC_000001.11", - "type": "LocationDescriptor", - "label": "NC_000001.11", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000001.11", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 156864429}, - "end": {"type": "Number", "value": 156864430}, - }, - }, + "type": "SequenceLocation", + "start": 156864429, + "end": 156864430, }, } @pytest.fixture(scope="module") -def tpm3_tx_t_element(tpm3_descriptor): +def tpm3_tx_t_element(tpm3_gene): """Provide TranscriptSegmentElement for TPM3 gene constructed using genomic coordinates and transcript. """ return { "type": "TranscriptSegmentElement", "transcript": "refseq:NM_152263.4", - "exon_start": 6, - "exon_start_offset": 72, - "exon_end": 6, - "exon_end_offset": -5, - "gene_descriptor": tpm3_descriptor, - "element_genomic_start": { + "exonStart": 6, + "exonStartOffset": 71, + "exonEnd": 6, + "exonEndOffset": -4, + "gene": tpm3_gene, + "elementGenomicStart": { "id": "fusor.location_descriptor:NC_000001.11", - "type": "LocationDescriptor", - "label": "NC_000001.11", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000001.11", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 154171416}, - "end": {"type": "Number", "value": 154171417}, - }, - }, + "type": "SequenceLocation", + "start": 154171416, + "end": 154171417, }, - "element_genomic_end": { + "elementGenomicEnd": { "id": "fusor.location_descriptor:NC_000001.11", - "type": "LocationDescriptor", - "label": "NC_000001.11", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000001.11", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 154171417}, - "end": {"type": "Number", "value": 154171418}, - }, - }, + "type": "SequenceLocation", + "start": 154171417, + "end": 154171418, }, } @@ -172,37 +163,21 @@ def tpm3_tx_g_element(tpm3_descriptor): return { "type": "TranscriptSegmentElement", "transcript": "refseq:NM_152263.4", - "exon_start": 6, - "exon_start_offset": 5, - "exon_end": 6, - "exon_end_offset": -70, - "gene_descriptor": tpm3_descriptor, - "element_genomic_start": { + "exonStart": 6, + "exonStartOffset": 5, + "exonEnd": 6, + "exonEndOffset": -71, + "gene": tpm3_descriptor, + "elementGenomicStart": { "id": "fusor.location_descriptor:NC_000001.11", - "type": "LocationDescriptor", - "label": "NC_000001.11", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000001.11", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 154171483}, - "end": {"type": "Number", "value": 154171484}, - }, - }, + "type": "SequenceLocation", + "start": 154171483, + "end": 154171484, }, - "element_genomic_end": { + "elementGenomicEnd": { "id": "fusor.location_descriptor:NC_000001.11", - "type": "LocationDescriptor", - "label": "NC_000001.11", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000001.11", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 154171482}, - "end": {"type": "Number", "value": 154171483}, - }, - }, + "type": "SequenceLocation", + "start": 154171482, + "end": 154171483, }, } diff --git a/server/tests/integration/test_complete.py b/server/tests/integration/test_complete.py index a4a12eb6..743b1da1 100644 --- a/server/tests/integration/test_complete.py +++ b/server/tests/integration/test_complete.py @@ -7,6 +7,7 @@ @pytest.mark.asyncio() async def test_complete_gene(async_client: AsyncClient): """Test /complete/gene endpoint""" + # test simple completion response = await async_client.get("/api/complete/gene?term=NTRK") assert response.status_code == 200 assert response.json() == { @@ -23,43 +24,90 @@ async def test_complete_gene(async_client: AsyncClient): "aliases": [], } + # test huge # of valid completions response = await async_client.get("/api/complete/gene?term=a") assert response.status_code == 200 - assert response.json() == { - "warnings": [ - "Exceeds max matches: Got 2096 possible matches for a (limit: 50)" - ], - "term": "a", - "matches_count": 2096, - "concept_id": [], - "symbol": [], - "prev_symbols": [ - ["A", "LOC100420587", "ncbigene:100420587", "NCBI:NC_000019.10", "-"] - ], - "aliases": [ - ["A", "LOC110467529", "ncbigene:110467529", "NCBI:NC_000021.9", "+"] - ], - } + response_json = response.json() + assert len(response_json["warnings"]) == 1 + assert "Exceeds max matches" in response_json["warnings"][0] + assert ( + response_json["matches_count"] >= 2000 + ), "should be a whole lot of matches (2081 as of last prod data dump)" + # test concept ID match response = await async_client.get("/api/complete/gene?term=hgnc:1097") assert response.status_code == 200 + response_json = response.json() + assert ( + response_json["matches_count"] >= 11 + ), "at least 11 matches are expected as of last prod data dump" + assert response_json["concept_id"][0] == [ + "hgnc:1097", + "BRAF", + "hgnc:1097", + "NCBI:NC_000007.14", + "-", + ], "BRAF should be first" + assert response_json["symbol"] == [] + assert response_json["prev_symbols"] == [] + assert response_json["aliases"] == [] + + +@pytest.mark.asyncio() +async def test_complete_domain(async_client: AsyncClient): + """Test /complete/domain endpoint""" + response = await async_client.get("/api/complete/domain?gene_id=hgnc%3A1097") assert response.json() == { - "term": "hgnc:1097", - "matches_count": 11, - "concept_id": [ - ["hgnc:1097", "BRAF", "hgnc:1097", "NCBI:NC_000007.14", "-"], - ["hgnc:10970", "SLC22A6", "hgnc:10970", "NCBI:NC_000011.10", "-"], - ["hgnc:10971", "SLC22A7", "hgnc:10971", "NCBI:NC_000006.12", "+"], - ["hgnc:10972", "SLC22A8", "hgnc:10972", "NCBI:NC_000011.10", "-"], - ["hgnc:10973", "SLC23A2", "hgnc:10973", "NCBI:NC_000020.11", "-"], - ["hgnc:10974", "SLC23A1", "hgnc:10974", "NCBI:NC_000005.10", "-"], - ["hgnc:10975", "SLC24A1", "hgnc:10975", "NCBI:NC_000015.10", "+"], - ["hgnc:10976", "SLC24A2", "hgnc:10976", "NCBI:NC_000009.12", "-"], - ["hgnc:10977", "SLC24A3", "hgnc:10977", "NCBI:NC_000020.11", "+"], - ["hgnc:10978", "SLC24A4", "hgnc:10978", "NCBI:NC_000014.9", "+"], - ["hgnc:10979", "SLC25A1", "hgnc:10979", "NCBI:NC_000022.11", "-"], + "gene_id": "hgnc:1097", + "suggestions": [ + { + "interproId": "interpro:IPR000719", + "domainName": "Protein kinase domain", + "start": 457, + "end": 717, + "refseqAc": "NP_004324.2", + }, + { + "interproId": "interpro:IPR001245", + "domainName": "Serine-threonine/tyrosine-protein kinase, catalytic domain", + "start": 458, + "end": 712, + "refseqAc": "NP_004324.2", + }, + { + "interproId": "interpro:IPR002219", + "domainName": "Protein kinase C-like, phorbol ester/diacylglycerol-binding domain", + "start": 235, + "end": 280, + "refseqAc": "NP_004324.2", + }, + { + "interproId": "interpro:IPR003116", + "domainName": "Raf-like Ras-binding", + "start": 157, + "end": 225, + "refseqAc": "NP_004324.2", + }, + { + "interproId": "interpro:IPR008271", + "domainName": "Serine/threonine-protein kinase, active site", + "start": 572, + "end": 584, + "refseqAc": "NP_004324.2", + }, + { + "interproId": "interpro:IPR017441", + "domainName": "Protein kinase, ATP binding site", + "start": 463, + "end": 483, + "refseqAc": "NP_004324.2", + }, + { + "interproId": "interpro:IPR020454", + "domainName": "Diacylglycerol/phorbol-ester binding", + "start": 232, + "end": 246, + "refseqAc": "NP_004324.2", + }, ], - "symbol": [], - "prev_symbols": [], - "aliases": [], } diff --git a/server/tests/integration/test_constructors.py b/server/tests/integration/test_constructors.py index 4e316613..ef6db0be 100644 --- a/server/tests/integration/test_constructors.py +++ b/server/tests/integration/test_constructors.py @@ -14,12 +14,11 @@ def check_gene_element_response( if ("element" not in response) and ("element" not in expected_response): return assert response["element"]["type"] == expected_response["element"]["type"] - response_gd = response["element"]["gene_descriptor"] - expected_gd = expected_response["element"]["gene_descriptor"] + response_gd = response["element"]["gene"] + expected_gd = expected_response["element"]["gene"] assert response_gd["id"] == expected_id assert response_gd["type"] == expected_gd["type"] assert response_gd["label"] == expected_gd["label"] - assert response_gd["gene_id"] == expected_gd["gene_id"] alk_gene_response = {"warnings": [], "element": alk_gene_element} @@ -27,13 +26,13 @@ def check_gene_element_response( "/api/construct/structural_element/gene?term=hgnc:427", alk_gene_response, check_gene_element_response, - expected_id="normalize.gene:hgnc%3A427", + expected_id="hgnc:427", ) await check_response( "/api/construct/structural_element/gene?term=ALK", alk_gene_response, check_gene_element_response, - expected_id="normalize.gene:ALK", + expected_id="hgnc:427", ) fake_id = "hgnc:99999999" await check_response( @@ -44,7 +43,7 @@ def check_gene_element_response( @pytest.fixture(scope="session") -def check_tx_element_response(): +def check_tx_element_response(check_sequence_location): """Provide callback function to check correctness of transcript element constructor.""" def check_tx_element_response(response: dict, expected_response: dict): @@ -56,58 +55,54 @@ def check_tx_element_response(response: dict, expected_response: dict): response_element = response["element"] expected_element = expected_response["element"] assert response_element["transcript"] == expected_element["transcript"] - assert ( - response_element["gene_descriptor"] == expected_element["gene_descriptor"] - ) - assert response_element.get("exon_start") == expected_element.get("exon_start") - assert response_element.get("exon_start_offset") == expected_element.get( - "exon_start_offset" - ) - assert response_element.get("exon_end") == expected_element.get("exon_end") - assert response_element.get("exon_end_offset") == expected_element.get( - "exon_end_offset" - ) - assert response_element.get("element_genomic_start") == expected_element.get( - "element_genomic_start" - ) - assert response_element.get("element_genomic_end") == expected_element.get( - "element_genomic_end" - ) + assert response_element["gene"] == expected_element["gene"] + assert response_element.get("exonStart") == expected_element.get("exonStart") + assert response_element.get("exonStartOffset") == expected_element.get( + "exonStartOffset" + ) + assert response_element.get("exonEnd") == expected_element.get("exonEnd") + assert response_element.get("exonEndOffset") == expected_element.get( + "exonEndOffset" + ) + genomic_start = response_element.get("elementGenomicStart", {}) + genomic_end = response_element.get("elementGenomicEnd", {}) + if genomic_start: + check_sequence_location(genomic_start) + if genomic_end: + check_sequence_location(genomic_end) return check_tx_element_response @pytest.fixture(scope="session") -def check_reg_element_response(): +def check_reg_element_response(check_sequence_location): """Provide callback function check correctness of regulatory element constructor.""" def check_re_response(response: dict, expected_response: dict): - assert ("regulatory_element" in response) == ( - "regulatory_element" in expected_response + assert ("regulatoryElement" in response) == ( + "regulatoryElement" in expected_response ) - if ("regulatory_element" not in response) and ( - "regulatory_element" not in expected_response + if ("regulatoryElement" not in response) and ( + "regulatoryElement" not in expected_response ): assert "warnings" in response assert set(response["warnings"]) == set(expected_response["warnings"]) return - response_re = response["regulatory_element"] - expected_re = expected_response["regulatory_element"] + response_re = response["regulatoryElement"] + expected_re = expected_response["regulatoryElement"] assert response_re["type"] == expected_re["type"] - assert response_re.get("regulatory_class") == expected_re.get( - "regulatory_class" - ) - assert response_re.get("feature_id") == expected_re.get("feature_id") - assert response_re.get("associated_gene") == expected_re.get("associated_gene") - assert response_re.get("location_descriptor") == expected_re.get( - "location_descriptor" - ) + assert response_re.get("regulatoryClass") == expected_re.get("regulatoryClass") + assert response_re.get("featureId") == expected_re.get("featureId") + assert response_re.get("associatedGene") == expected_re.get("associatedGene") + sequence_location = response_re.get("sequenceLocation") + if sequence_location: + check_sequence_location(sequence_location) return check_re_response @pytest.fixture(scope="session") -def check_templated_sequence_response(): +def check_templated_sequence_response(check_sequence_location): """Provide callback function to check templated sequence constructor response""" def check_temp_seq_response(response: dict, expected_response: dict): @@ -121,39 +116,9 @@ def check_temp_seq_response(response: dict, expected_response: dict): assert response_elem["type"] == expected_elem["type"] assert response_elem["strand"] == expected_elem["strand"] assert response_elem["region"]["id"] == expected_elem["region"]["id"] - assert response_elem["region"]["type"] == expected_elem["region"]["type"] - assert ( - response_elem["region"]["location_id"] - == expected_elem["region"]["location_id"] - ) - assert ( - response_elem["region"]["location"]["type"] - == expected_elem["region"]["location"]["type"] - ) - assert ( - response_elem["region"]["location"]["sequence_id"] - == expected_elem["region"]["location"]["sequence_id"] - ) - assert ( - response_elem["region"]["location"]["interval"]["type"] - == expected_elem["region"]["location"]["interval"]["type"] - ) - assert ( - response_elem["region"]["location"]["interval"]["start"]["type"] - == expected_elem["region"]["location"]["interval"]["start"]["type"] - ) - assert ( - response_elem["region"]["location"]["interval"]["start"]["value"] - == expected_elem["region"]["location"]["interval"]["start"]["value"] - ) - assert ( - response_elem["region"]["location"]["interval"]["end"]["type"] - == expected_elem["region"]["location"]["interval"]["end"]["type"] - ) - assert ( - response_elem["region"]["location"]["interval"]["end"]["value"] - == expected_elem["region"]["location"]["interval"]["end"]["value"] - ) + check_sequence_location(response_elem["region"] or {}) + assert response_elem["region"]["start"] == expected_elem["region"]["start"] + assert response_elem["region"]["end"] == expected_elem["region"]["end"] return check_temp_seq_response @@ -171,7 +136,7 @@ async def test_build_tx_segment_ect( check_tx_element_response, ) - # test require exon_start or exon_end + # test require exonStart or exonEnd await check_response( "/api/construct/structural_element/tx_segment_ect?transcript=NM_002529.3", {"warnings": ["Must provide either `exon_start` or `exon_end`"]}, @@ -225,14 +190,13 @@ async def test_build_reg_element(check_response, check_reg_element_response): await check_response( "/api/construct/regulatory_element?element_class=promoter&gene_name=braf", { - "regulatory_element": { - "associated_gene": { - "gene_id": "hgnc:1097", - "id": "normalize.gene:braf", + "regulatoryElement": { + "associatedGene": { + "id": "hgnc:1097", "label": "BRAF", - "type": "GeneDescriptor", + "type": "Gene", }, - "regulatory_class": "promoter", + "regulatoryClass": "promoter", "type": "RegulatoryElement", } }, @@ -245,52 +209,31 @@ async def test_build_templated_sequence( check_response, check_templated_sequence_response ): """Test correct functioning of templated sequence constructor""" - await check_response( - "/api/construct/structural_element/templated_sequence?start=154171415&end=154171417&sequence_id=NC_000001.11&strand=-", - { - "element": { - "type": "TemplatedSequenceElement", - "region": { - "id": "fusor.location_descriptor:NC_000001.11", - "type": "LocationDescriptor", - "location_id": "ga4gh:VSL.K_suWpotWJZL0EFYUqoZckNq4bqEjH-z", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000001.11", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 154171414}, - "end": {"type": "Number", "value": 154171417}, - }, - }, + expected = { + "element": { + "type": "TemplatedSequenceElement", + "region": { + "id": "ga4gh:SL.thjDCmA1u2mB0vLGjgQbCOEg81eP5hdO", + "type": "SequenceLocation", + "sequenceReference": { + "id": "refseq:NC_000001.11", + "refgetAccession": "", + "type": "SequenceReference", }, - "strand": "-", + "start": 154171414, + "end": 154171417, }, + "strand": -1, }, + } + await check_response( + "/api/construct/structural_element/templated_sequence?start=154171415&end=154171417&sequence_id=NC_000001.11&strand=-", + expected, check_templated_sequence_response, ) await check_response( "/api/construct/structural_element/templated_sequence?start=154171415&end=154171417&sequence_id=refseq%3ANC_000001.11&strand=-", - { - "element": { - "type": "TemplatedSequenceElement", - "region": { - "id": "fusor.location_descriptor:NC_000001.11", - "type": "LocationDescriptor", - "location_id": "ga4gh:VSL.K_suWpotWJZL0EFYUqoZckNq4bqEjH-z", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000001.11", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 154171414}, - "end": {"type": "Number", "value": 154171417}, - }, - }, - }, - "strand": "-", - }, - }, + expected, check_templated_sequence_response, ) diff --git a/server/tests/integration/test_lookup.py b/server/tests/integration/test_lookup.py index 0ecf52a3..336aef91 100644 --- a/server/tests/integration/test_lookup.py +++ b/server/tests/integration/test_lookup.py @@ -23,7 +23,7 @@ async def test_normalize_gene(async_client: AsyncClient): "concept_id": "hgnc:8031", "symbol": "NTRK1", "cased": "NTRK1", - } + }, "Results should be properly cased regardless of input" response = await async_client.get("/api/lookup/gene?term=acee") assert response.status_code == 200 @@ -32,7 +32,7 @@ async def test_normalize_gene(async_client: AsyncClient): "concept_id": "hgnc:108", "symbol": "ACHE", "cased": "ACEE", - } + }, "Lookup by alias should work" response = await async_client.get("/api/lookup/gene?term=c9ORF72") assert response.status_code == 200 @@ -41,11 +41,11 @@ async def test_normalize_gene(async_client: AsyncClient): "concept_id": "hgnc:28337", "symbol": "C9orf72", "cased": "C9orf72", - } + }, "Correct capitalization for orf genes should be observed" response = await async_client.get("/api/lookup/gene?term=sdfliuwer") assert response.status_code == 200 assert response.json() == { "term": "sdfliuwer", "warnings": ["Lookup of gene term sdfliuwer failed."], - } + }, "Failed lookup should still respond successfully" diff --git a/server/tests/integration/test_main.py b/server/tests/integration/test_main.py index 8aacae8d..634f3d9c 100644 --- a/server/tests/integration/test_main.py +++ b/server/tests/integration/test_main.py @@ -1,27 +1,15 @@ """Test main service routes.""" -import re - import pytest @pytest.mark.asyncio() async def test_service_info(async_client): - """Test /service_info endpoint - - uses semver-provided regex to check version numbers: - https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string # noqa: E501 - """ + """Simple test of /service_info endpoint""" response = await async_client.get("/api/service_info") assert response.status_code == 200 response_json = response.json() assert response_json["warnings"] == [] - semver_pattern = r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" - assert re.match(semver_pattern, response_json["curfu_version"]) - assert re.match(semver_pattern, response_json["fusor_version"]) - assert re.match(semver_pattern, response_json["cool_seq_tool_version"]) - # not sure if I want to include vrs-python - # also its current version number isn't legal semver - # assert re.match( - # SEMVER_PATTERN, response_json["vrs_python_version"] - # ) + assert response_json["curfu_version"] + assert response_json["fusor_version"] + assert response_json["cool_seq_tool_version"] diff --git a/server/tests/integration/test_nomenclature.py b/server/tests/integration/test_nomenclature.py index 7577e668..811e9cfe 100644 --- a/server/tests/integration/test_nomenclature.py +++ b/server/tests/integration/test_nomenclature.py @@ -9,12 +9,8 @@ def regulatory_element(): """Provide regulatory element fixture.""" return { - "regulatory_class": "promoter", - "associated_gene": { - "id": "gene:G1", - "gene": {"gene_id": "hgnc:9339"}, - "label": "G1", - }, + "regulatoryClass": "promoter", + "associatedGene": {"id": "hgnc:9339", "label": "G1", "type": "Gene"}, } @@ -24,26 +20,21 @@ def epcam_5_prime(): return { "type": "TranscriptSegmentElement", "transcript": "refseq:NM_002354.2", - "exon_end": 5, - "exon_end_offset": 0, - "gene_descriptor": { - "id": "normalize.gene:EPCAM", - "type": "GeneDescriptor", + "exonEnd": 5, + "exonEndOffset": 0, + "gene": { + "type": "Gene", "label": "EPCAM", - "gene_id": "hgnc:11529", + "id": "hgnc:11529", }, - "element_genomic_end": { + "elementGenomicEnd": { "id": "fusor.location_descriptor:NC_000002.12", - "type": "LocationDescriptor", + "type": "SequenceLocation", "label": "NC_000002.12", "location": { "type": "SequenceLocation", - "sequence_id": "refseq:NC_000002.12", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 47377013}, - "end": {"type": "Number", "value": 47377014}, - }, + "start": 47377013, + "end": 47377014, }, }, } @@ -55,27 +46,18 @@ def epcam_3_prime(): return { "type": "TranscriptSegmentElement", "transcript": "refseq:NM_002354.2", - "exon_start": 5, - "exon_start_offset": 0, - "gene_descriptor": { - "id": "normalize.gene:EPCAM", - "type": "GeneDescriptor", + "exonStart": 5, + "exonStartOffset": 0, + "gene": { + "type": "Gene", "label": "EPCAM", - "gene_id": "hgnc:11529", + "id": "hgnc:11529", }, - "element_genomic_start": { + "elementGenomicStart": { "id": "fusor.location_descriptor:NC_000002.12", - "type": "LocationDescriptor", - "label": "NC_000002.12", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000002.12", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 47377013}, - "end": {"type": "Number", "value": 47377014}, - }, - }, + "type": "SequenceLocation", + "start": 47377013, + "end": 47377014, }, } @@ -85,27 +67,18 @@ def epcam_invalid(): """Provide invalidly-constructed EPCAM transcript segment element.""" return { "type": "TranscriptSegmentElement", - "exon_end": 5, - "exon_end_offset": 0, - "gene_descriptor": { - "id": "normalize.gene:EPCAM", - "type": "GeneDescriptor", + "exonEnd": 5, + "exonEndOffset": 0, + "gene": { + "type": "Gene", "label": "EPCAM", - "gene_id": "hgnc:11529", + "id": "hgnc:11529", }, - "element_genomic_end": { + "elementGenomicEnd": { "id": "fusor.location_descriptor:NC_000002.12", - "type": "LocationDescriptor", - "label": "NC_000002.12", - "location": { - "type": "SequenceLocation", - "sequence_id": "refseq:NC_000002.12", - "interval": { - "type": "SequenceInterval", - "start": {"type": "Number", "value": 47377013}, - "end": {"type": "Number", "value": 47377014}, - }, - }, + "type": "SequenceLocation", + "start": 47377013, + "end": 47377014, }, } @@ -117,17 +90,15 @@ def templated_sequence_element(): "type": "TemplatedSequenceElement", "strand": "-", "region": { - "id": "NC_000001.11:15455-15566", - "type": "LocationDescriptor", - "location": { - "sequence_id": "refseq:NC_000001.11", - "interval": { - "start": {"type": "Number", "value": 15455}, - "end": {"type": "Number", "value": 15566}, - }, - "type": "SequenceLocation", + "id": "ga4gh:SL.sKl255JONKva_LKJeyfkmlmqXTaqHcWq", + "type": "SequenceLocation", + "sequenceReference": { + "id": "refseq:NC_000001.11", + "refgetAccession": "SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO", + "type": "SequenceReference", }, - "label": "NC_000001.11:15455-15566", + "start": 15455, + "end": 15566, }, } @@ -176,9 +147,12 @@ async def test_tx_segment_nomenclature( "/api/nomenclature/transcript_segment?first=true&last=false", json=epcam_invalid ) assert response.status_code == 200 - assert response.json().get("warnings", []) == [ - "1 validation error for TranscriptSegmentElement\ntranscript\n field required (type=value_error.missing)" + expected_warnings = [ + "validation error for TranscriptSegmentElement", + "Field required", ] + for expected in expected_warnings: + assert expected in response.json().get("warnings", [])[0] @pytest.mark.asyncio() @@ -192,12 +166,12 @@ async def test_gene_element_nomenclature( response = await async_client.post( "/api/nomenclature/gene", - json={"type": "GeneElement", "associated_gene": {"id": "hgnc:427"}}, + json={"type": "GeneElement", "associatedGene": {"id": "hgnc:427"}}, ) assert response.status_code == 200 - assert response.json().get("warnings", []) == [ - "2 validation errors for GeneElement\ngene_descriptor\n field required (type=value_error.missing)\nassociated_gene\n extra fields not permitted (type=value_error.extra)" - ] + expected_warnings = ["validation error for GeneElement", "Field required"] + for expected in expected_warnings: + assert expected in response.json().get("warnings", [])[0] @pytest.mark.asyncio() @@ -220,28 +194,37 @@ async def test_templated_sequence_nomenclature( "type": "TemplatedSequenceElement", "region": { "id": "NC_000001.11:15455-15566", - "type": "LocationDescriptor", - "location": { - "interval": { - "start": {"type": "Number", "value": 15455}, - "end": {"type": "Number", "value": 15566}, - }, - "sequence_id": "refseq:NC_000001.11", - "type": "SequenceLocation", - }, + "type": "SequenceLocation", + "start": 15455, + "end": 15566, }, }, ) assert response.status_code == 200 - assert response.json().get("warnings", []) == [ - "1 validation error for TemplatedSequenceElement\nstrand\n field required (type=value_error.missing)" + expected_warnings = [ + "validation error for TemplatedSequenceElement", + "Input should be a valid integer", ] + for expected in expected_warnings: + assert expected in response.json().get("warnings", [])[0] @pytest.mark.asyncio() async def test_fusion_nomenclature(async_client: AsyncClient): """Test correctness of fusion nomneclature endpoint.""" - response = await async_client.post("/api/nomenclature/fusion", json=bcr_abl1.dict()) + bcr_abl1_formatted = bcr_abl1.model_dump() + bcr_abl1_json = { + "structure": bcr_abl1_formatted.get("structure"), + "fusion_type": "CategoricalFusion", + "reading_frame_preserved": True, + "regulatory_element": None, + "critical_functional_domains": bcr_abl1_formatted.get( + "criticalFunctionalDomains" + ), + } + response = await async_client.post( + "/api/nomenclature/fusion?skip_vaidation=true", json=bcr_abl1_json + ) assert response.status_code == 200 assert ( response.json().get("nomenclature", "") diff --git a/server/tests/integration/test_utilities.py b/server/tests/integration/test_utilities.py index 522dcbe4..a74428d1 100644 --- a/server/tests/integration/test_utilities.py +++ b/server/tests/integration/test_utilities.py @@ -42,8 +42,8 @@ def check_mane_response(response: dict, expected_response: dict): "Ensembl_prot": "ENSP00000496776.1", "MANE_status": "MANE Plus Clinical", "GRCh38_chr": "NC_000007.14", - "chr_start": "140719337", - "chr_end": "140924929", + "chr_start": 140719337, + "chr_end": 140924929, "chr_strand": "-", }, { @@ -58,8 +58,8 @@ def check_mane_response(response: dict, expected_response: dict): "Ensembl_prot": "ENSP00000493543.1", "MANE_status": "MANE Select", "GRCh38_chr": "NC_000007.14", - "chr_start": "140730665", - "chr_end": "140924929", + "chr_start": 140730665, + "chr_end": 140924929, "chr_strand": "-", }, ] diff --git a/server/tests/integration/test_validate.py b/server/tests/integration/test_validate.py index 7b18153c..fdd8dadb 100644 --- a/server/tests/integration/test_validate.py +++ b/server/tests/integration/test_validate.py @@ -10,14 +10,13 @@ def alk_fusion(): return { "input": { "type": "CategoricalFusion", - "structural_elements": [ + "structure": [ { "type": "GeneElement", - "gene_descriptor": { - "id": "normalize.gene:ALK", - "type": "GeneDescriptor", + "gene": { + "id": "hgnc:427", + "type": "Gene", "label": "ALK", - "gene_id": "hgnc:427", }, }, {"type": "MultiplePossibleGenesElement"}, @@ -25,14 +24,13 @@ def alk_fusion(): }, "output": { "type": "CategoricalFusion", - "structural_elements": [ + "structure": [ { "type": "GeneElement", - "gene_descriptor": { - "id": "normalize.gene:ALK", - "type": "GeneDescriptor", + "gene": { + "id": "hgnc:427", + "type": "Gene", "label": "ALK", - "gene_id": "hgnc:427", }, }, {"type": "MultiplePossibleGenesElement"}, @@ -48,54 +46,38 @@ def ewsr1_fusion(): return { "input": { "type": "AssayedFusion", - "structural_elements": [ + "structure": [ { "type": "GeneElement", - "gene_descriptor": { - "type": "GeneDescriptor", - "id": "normalize.gene:EWSR1", - "label": "EWSR1", - "gene_id": "hgnc:3508", - }, + "gene": {"type": "Gene", "label": "EWSR1", "id": "hgnc:3508"}, }, {"type": "UnknownGeneElement"}, ], - "causative_event": { - "type": "CausativeEvent", - "event_type": "rearrangement", - }, + "causative_event": {"type": "CausativeEvent", "eventType": "rearrangement"}, "assay": { "type": "Assay", - "method_uri": "pmid:33576979", - "assay_id": "obi:OBI_0003094", - "assay_name": "fluorescence in-situ hybridization assay", - "fusion_detection": "inferred", + "methodUri": "pmid:33576979", + "assayId": "obi:OBI_0003094", + "assayName": "fluorescence in-situ hybridization assay", + "fusionDetection": "inferred", }, }, "output": { "type": "AssayedFusion", - "structural_elements": [ + "structure": [ { "type": "GeneElement", - "gene_descriptor": { - "type": "GeneDescriptor", - "id": "normalize.gene:EWSR1", - "label": "EWSR1", - "gene_id": "hgnc:3508", - }, + "gene": {"type": "Gene", "label": "EWSR1", "id": "hgnc:3508"}, }, {"type": "UnknownGeneElement"}, ], - "causative_event": { - "type": "CausativeEvent", - "event_type": "rearrangement", - }, + "causativeEvent": {"type": "CausativeEvent", "eventType": "rearrangement"}, "assay": { "type": "Assay", - "method_uri": "pmid:33576979", - "assay_id": "obi:OBI_0003094", - "assay_name": "fluorescence in-situ hybridization assay", - "fusion_detection": "inferred", + "methodUri": "pmid:33576979", + "assayId": "obi:OBI_0003094", + "assayName": "fluorescence in-situ hybridization assay", + "fusionDetection": "inferred", }, }, "warnings": None, @@ -109,51 +91,37 @@ def ewsr1_fusion_fill_types(): """ return { "input": { - "structural_elements": [ + "type": "AssayedFusion", + "structure": [ { - "gene_descriptor": { - "id": "normalize.gene:EWSR1", - "label": "EWSR1", - "gene_id": "hgnc:3508", - }, + "gene": {"type": "Gene", "label": "EWSR1", "id": "hgnc:3508"}, }, {"type": "UnknownGeneElement"}, ], - "causative_event": { - "type": "CausativeEvent", - "event_type": "rearrangement", - }, + "causative_event": {"eventType": "rearrangement"}, "assay": { - "method_uri": "pmid:33576979", - "assay_id": "obi:OBI_0003094", - "assay_name": "fluorescence in-situ hybridization assay", - "fusion_detection": "inferred", + "methodUri": "pmid:33576979", + "assayId": "obi:OBI_0003094", + "assayName": "fluorescence in-situ hybridization assay", + "fusionDetection": "inferred", }, }, "output": { "type": "AssayedFusion", - "structural_elements": [ + "structure": [ { "type": "GeneElement", - "gene_descriptor": { - "type": "GeneDescriptor", - "id": "normalize.gene:EWSR1", - "label": "EWSR1", - "gene_id": "hgnc:3508", - }, + "gene": {"type": "Gene", "label": "EWSR1", "id": "hgnc:3508"}, }, {"type": "UnknownGeneElement"}, ], - "causative_event": { - "type": "CausativeEvent", - "event_type": "rearrangement", - }, + "causativeEvent": {"type": "CausativeEvent", "eventType": "rearrangement"}, "assay": { "type": "Assay", - "method_uri": "pmid:33576979", - "assay_id": "obi:OBI_0003094", - "assay_name": "fluorescence in-situ hybridization assay", - "fusion_detection": "inferred", + "methodUri": "pmid:33576979", + "assayId": "obi:OBI_0003094", + "assayName": "fluorescence in-situ hybridization assay", + "fusionDetection": "inferred", }, }, "warnings": None, @@ -166,11 +134,11 @@ def wrong_type_fusion(): return { "input": { "type": "CategoricalFusion", - "structural_elements": [ + "structure": [ { "type": "GeneElement", - "gene_descriptor": { - "type": "GeneDescriptor", + "gene": { + "type": "Gene", "id": "normalize.gene:EWSR1", "label": "EWSR1", "gene_id": "hgnc:3508", @@ -180,20 +148,19 @@ def wrong_type_fusion(): ], "causative_event": { "type": "CausativeEvent", - "event_type": "rearrangement", + "eventType": "rearrangement", }, "assay": { "type": "Assay", - "method_uri": "pmid:33576979", - "assay_id": "obi:OBI_0003094", - "assay_name": "fluorescence in-situ hybridization assay", - "fusion_detection": "inferred", + "methodUri": "pmid:33576979", + "assayId": "obi:OBI_0003094", + "assayName": "fluorescence in-situ hybridization assay", + "fusionDetection": "inferred", }, }, "output": None, "warnings": [ - "Unable to construct fusion with provided args: FUSOR.categorical_fusion()" - " got an unexpected keyword argument 'causative_event'" + "Unable to construct fusion with provided args: FUSOR.categorical_fusion() got an unexpected keyword argument 'causative_event'" ], }