From 91e04e72e93e773b77629ae09b85c3010c88c8e3 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Wed, 7 Aug 2024 15:47:28 -0400 Subject: [PATCH 1/2] fix: handle unnecessary checks for build files (#309) Does a few things @korikuzma noticed that the README description of copying build files had an incorrect path. However, this instruction is actually unnecessary (and impractical tbh). In development you'd be better off letting yarn start handle service of client files because it has hot-reloading on changes. I removed it. Rather than requiring client files to be present, catches + logs their absence if they're not there. This is better for development. Originally I added this code in the big VRS update PR but it should've been a separate issue. I would like to see us reexamine our logging initialization/setup in another issue, because it could be bad if it's not working properly. Adds some additional description of why the client service code is there + what it needs. --- README.md | 7 ----- server/src/curfu/main.py | 61 +++++++++++++++++++++++++--------------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index a37d72f3..f93ed702 100644 --- a/README.md +++ b/README.md @@ -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/server/src/curfu/main.py b/server/src/curfu/main.py index 0f5df845..694cf2d4 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,8 @@ validate, ) +_logger = logging.getLogger(__name__) + fastapi_app = FastAPI( title="Fusion Curation API", description="Provide data functions to support [VICC Fusion Curation interface](fusion-builder.cancervariants.org/).", @@ -66,32 +69,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 From 43d32d30a2f3f18cacb319b1f8bd45b9385925fe Mon Sep 17 00:00:00 2001 From: Katie Stahl Date: Mon, 12 Aug 2024 10:37:45 -0400 Subject: [PATCH 2/2] feat!: use latest object schemas and dependency releases (#293) * build!: updating to vrs 2.0 models * update pydantic-to-ts2 and update models * wip: updating models, making variable casing consistent, converting descriptors * fix: utilities requests * fix: response model casing * wip: updating models * bug fixes * updating models and fixing validation * pass around formatted fusion to reduce repeated code * fixing demo data * specifying fusion type since null param gets dropped and fusor can't infer the type * fixing tests and updating data with new models, bug fixes * bug fixes for input elements * pin latest fusor * update dependencies * fix clientification of demos * fix formatting in docstrings * fix type handling of failed lookup * fix up gene lookup api * fix tx utils bug * fix bug where region and strand were incorrect in templated sequence, fix regulatory element missing field, pin pydantic version * removing console logs * fix: bug where error messages no longer displayed on summary page * fixing tests and adjusting variable casing * add reusable function for checking valid sequence locations * add reusable function for checking valid sequence locations * fixing nomenclature bugs * fixing nomenclature bugs * DOn't bother w/ semver checks (out of control of this app) and use proper fixture mode * add assertion notes * stash changes * stash * fix a few fixtures * sequence util fixes * minor rearrange * fix int/str problem * commit this * catch static files error * validation tests * fix reg element URL * remove todos * review comments --------- Co-authored-by: James Stevenson --- README.md | 4 +- client/src/components/Pages/Assay/Assay.tsx | 36 +- .../Pages/CausativeEvent/CausativeEvent.tsx | 18 +- .../Pages/Domains/DomainForm/DomainForm.tsx | 18 +- .../components/Pages/Domains/Main/Domains.tsx | 8 +- .../StructureDiagram/StructureDiagram.tsx | 4 +- .../Pages/ReadingFrame/ReadingFrame.tsx | 18 +- .../Pages/Structure/Builder/Builder.tsx | 90 ++- .../GeneElementInput/GeneElementInput.tsx | 16 +- .../LinkerElementInput/LinkerElementInput.tsx | 7 +- .../RegulatoryElementInput.tsx | 19 +- .../Input/StructuralElementInputAccordion.tsx | 34 +- .../TemplatedSequenceElementInput.tsx | 33 +- .../TxSegmentElementInput.tsx | 57 +- .../Pages/Structure/Main/Structure.tsx | 4 +- .../Pages/Summary/Invalid/Invalid.tsx | 42 +- .../Pages/Summary/JSON/SummaryJSON.tsx | 107 +-- .../components/Pages/Summary/Main/Summary.tsx | 73 +- .../Pages/Summary/Readable/Readable.tsx | 43 +- .../Pages/Summary/Success/Success.tsx | 11 +- client/src/components/main/App/App.tsx | 63 +- client/src/services/ResponseModels.ts | 679 ++++++++++-------- client/src/services/main.tsx | 36 +- requirements.txt | 11 +- server/pyproject.toml | 21 +- .../src/curfu/devtools/build_client_types.py | 2 +- server/src/curfu/devtools/build_interpro.py | 4 +- server/src/curfu/domain_services.py | 6 +- server/src/curfu/gene_services.py | 26 +- server/src/curfu/main.py | 30 +- server/src/curfu/routers/constructors.py | 7 +- server/src/curfu/routers/demo.py | 54 +- server/src/curfu/routers/lookup.py | 7 +- server/src/curfu/routers/meta.py | 2 +- server/src/curfu/routers/nomenclature.py | 27 +- server/src/curfu/routers/utilities.py | 39 +- server/src/curfu/routers/validate.py | 3 + server/src/curfu/schemas.py | 130 ++-- server/src/curfu/sequence_services.py | 6 +- server/tests/conftest.py | 163 ++--- server/tests/integration/test_complete.py | 112 ++- server/tests/integration/test_constructors.py | 179 ++--- server/tests/integration/test_lookup.py | 8 +- server/tests/integration/test_main.py | 20 +- server/tests/integration/test_nomenclature.py | 151 ++-- server/tests/integration/test_utilities.py | 8 +- server/tests/integration/test_validate.py | 125 ++-- 47 files changed, 1284 insertions(+), 1277 deletions(-) diff --git a/README.md b/README.md index f93ed702..5609a50f 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 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 84f70f77..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,8 +48,7 @@ dev = [ "ruff == 0.5.0", "black", "pre-commit>=3.7.1", - "gene-normalizer ~= 0.1.39", - "pydantic-to-typescript", + "pydantic-to-typescript2", ] [project.scripts] @@ -162,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 694cf2d4..4a1529dd 100644 --- a/server/src/curfu/main.py +++ b/server/src/curfu/main.py @@ -28,6 +28,21 @@ _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/).", @@ -44,6 +59,7 @@ swagger_ui_parameters={"tryItOutEnabled": True}, docs_url="/docs", openapi_url="/openapi.json", + lifespan=lifespan, ) fastapi_app.include_router(utilities.router) @@ -142,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'" ], }