diff --git a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx index cabeb193..0b9aef51 100644 --- a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx @@ -10,18 +10,24 @@ import { TranscriptSegmentElement, TxSegmentElementResponse, } from "../../../../../services/ResponseModels"; -import React, { useEffect, useState, KeyboardEvent, useContext } from "react"; +import React, { + useEffect, + useState, + KeyboardEvent, + useContext, + ChangeEvent, +} from "react"; import { getTxSegmentElementECT, getTxSegmentElementGCG, getTxSegmentElementGCT, getTxSegmentNomenclature, + TxElementInputType, } from "../../../../../services/main"; import { GeneAutocomplete } from "../../../../main/shared/GeneAutocomplete/GeneAutocomplete"; import { StructuralElementInputProps } from "../StructuralElementInputProps"; import StructuralElementInputAccordion from "../StructuralElementInputAccordion"; import { FusionContext } from "../../../../../global/contexts/FusionContext"; -import StrandSwitch from "../../../../main/shared/StrandSwitch/StrandSwitch"; import HelpTooltip from "../../../../main/shared/HelpTooltip/HelpTooltip"; import ChromosomeField from "../../../../main/shared/ChromosomeField/ChromosomeField"; import TranscriptField from "../../../../main/shared/TranscriptField/TranscriptField"; @@ -30,13 +36,6 @@ interface TxSegmentElementInputProps extends StructuralElementInputProps { element: ClientTranscriptSegmentElement; } -export enum InputType { - default = "default", - gcg = "genomic_coords_gene", - gct = "genomic_coords_tx", - ect = "exon_coords_tx", -} - const TxSegmentCompInput: React.FC = ({ element, index, @@ -46,8 +45,8 @@ const TxSegmentCompInput: React.FC = ({ }) => { const { fusion } = useContext(FusionContext); - const [txInputType, setTxInputType] = useState( - (element.inputType as InputType) || InputType.default + const [txInputType, setTxInputType] = useState( + (element.inputType as TxElementInputType) || TxElementInputType.default ); // "Text" variables refer to helper or warning text to set under input fields @@ -89,30 +88,20 @@ const TxSegmentCompInput: React.FC = ({ const [pendingResponse, setPendingResponse] = useState(false); - /* - Depending on this element's location in the structure array, the user - needs to provide some kind of coordinate input for either one or both ends - of the element. This can change as the user drags the element around the structure - array, or adds other elements to the array. - */ const hasRequiredEnds = - index !== 0 && index < fusion.length - ? (txStartingGenomic && txEndingGenomic) || (startingExon && endingExon) - : index === 0 - ? txEndingGenomic || endingExon - : txStartingGenomic || startingExon; + txEndingGenomic || endingExon || txStartingGenomic || startingExon; // programming horror const inputComplete = - (txInputType === InputType.gcg && + (txInputType === TxElementInputType.gcg && txGene !== "" && txChrom !== "" && (txStartingGenomic !== "" || txEndingGenomic !== "")) || - (txInputType === InputType.gct && + (txInputType === TxElementInputType.gct && txAc !== "" && txChrom !== "" && (txStartingGenomic !== "" || txEndingGenomic !== "")) || - (txInputType === InputType.ect && + (txInputType === TxElementInputType.ect && txAc !== "" && (startingExon !== "" || endingExon !== "")); @@ -233,7 +222,7 @@ const TxSegmentCompInput: React.FC = ({ setPendingResponse(true); // fire constructor request switch (txInputType) { - case InputType.gcg: + case TxElementInputType.gcg: clearGenomicCoordWarnings(); getTxSegmentElementGCG( txGene, @@ -261,14 +250,13 @@ const TxSegmentCompInput: React.FC = ({ } }); break; - case InputType.gct: + case TxElementInputType.gct: clearGenomicCoordWarnings(); getTxSegmentElementGCT( txAc, txChrom, txStartingGenomic, - txEndingGenomic, - txStrand + txEndingGenomic ).then((txSegmentResponse) => { if ( txSegmentResponse.warnings && @@ -290,7 +278,7 @@ const TxSegmentCompInput: React.FC = ({ } }); break; - case InputType.ect: + case TxElementInputType.ect: getTxSegmentElementECT( txAc, startingExon as string, @@ -436,13 +424,18 @@ const TxSegmentCompInput: React.FC = ({ ); + const handleChromosomeChange = (e: ChangeEvent) => { + setTxChrom(e.target.value); + }; + const genomicCoordinateInfo = ( <> - - - - + {renderTxGenomicCoords()} @@ -450,7 +443,7 @@ const TxSegmentCompInput: React.FC = ({ const renderTxOptions = () => { switch (txInputType) { - case InputType.gcg: + case TxElementInputType.gcg: return ( @@ -467,14 +460,14 @@ const TxSegmentCompInput: React.FC = ({ {genomicCoordinateInfo} ); - case InputType.gct: + case TxElementInputType.gct: return ( {txInputField} {genomicCoordinateInfo} ); - case InputType.ect: + case TxElementInputType.ect: return ( {txInputField} @@ -611,7 +604,7 @@ const TxSegmentCompInput: React.FC = ({ * Wrapper around input type selection to ensure unused inputs/warnings get cleared * @param selection selection from input type dropdown menu */ - const selectInputType = (selection: InputType) => { + const selectInputType = (selection: TxElementInputType) => { if (txInputType !== "default") { if (selection === "exon_coords_tx") { clearGenomicCoordWarnings(); @@ -644,7 +637,7 @@ const TxSegmentCompInput: React.FC = ({ diff --git a/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx b/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx index 26d02456..a35b28a4 100644 --- a/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx +++ b/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx @@ -1,17 +1,20 @@ import { makeStyles, TextField, Typography } from "@material-ui/core"; -import React, { KeyboardEventHandler } from "react"; +import React, { ChangeEvent } from "react"; import HelpTooltip from "../HelpTooltip/HelpTooltip"; interface Props { fieldValue: string; errorText: string; width?: number | undefined; + editable?: boolean; + onChange?: (event: ChangeEvent) => void; } const ChromosomeField: React.FC = ({ fieldValue, errorText, width, + onChange, }) => { const useStyles = makeStyles(() => ({ textField: { @@ -41,7 +44,7 @@ const ChromosomeField: React.FC = ({ error={errorText != ""} label="Chromosome" helperText={errorText != "" ? errorText : null} - contentEditable={false} + onChange={onChange} className={classes.textField} /> diff --git a/client/src/services/ResponseModels.ts b/client/src/services/ResponseModels.ts index 4bbae998..268d73af 100644 --- a/client/src/services/ResponseModels.ts +++ b/client/src/services/ResponseModels.ts @@ -638,22 +638,66 @@ export interface ClientStructuralElement { */ export interface CoordsUtilsResponse { warnings?: string[] | null; - coordinates_data: GenomicData | null; + coordinates_data: GenomicTxSegService | null; } /** - * Model containing genomic and transcript exon data. + * Service model for genomic and transcript data. */ -export interface GenomicData { - gene: string; - chr: string; - 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: Strand; +export interface GenomicTxSegService { + /** + * HGNC gene symbol. + */ + gene?: string | null; + /** + * RefSeq genomic accession. + */ + genomic_ac?: string | null; + /** + * RefSeq transcript accession. + */ + tx_ac?: string | null; + /** + * Start transcript segment. + */ + seg_start?: TxSegment | null; + /** + * End transcript segment. + */ + seg_end?: TxSegment | null; + /** + * Error messages. + */ + errors?: string[]; + /** + * Service metadata. + */ + service_meta: ServiceMeta; +} +/** + * Model for representing transcript segment data. + */ +export interface TxSegment { + /** + * Exon number. 0-based. + */ + exon_ord: number; + /** + * The value added to or subtracted from the `genomic_location` to find the start or end of an exon. + */ + offset?: number; + /** + * The genomic position of a transcript segment. + */ + genomic_location: SequenceLocation; +} +/** + * Metadata for cool_seq_tool service + */ +export interface ServiceMeta { + name?: "cool_seq_tool"; + version: string; + response_datetime: string; + url?: "https://github.com/GenomicMedLab/cool-seq-tool"; } /** * Response model for demo fusion object retrieval endpoints. diff --git a/client/src/services/main.tsx b/client/src/services/main.tsx index 9b29ed83..0c4edeed 100644 --- a/client/src/services/main.tsx +++ b/client/src/services/main.tsx @@ -49,6 +49,13 @@ export enum ElementType { regulatoryElement = "RegulatoryElement", } +export enum TxElementInputType { + default = "default", + gcg = "genomic_coords_gene", + gct = "genomic_coords_tx", + ect = "exon_coords_tx", +} + export type ClientElementUnion = | ClientMultiplePossibleGenesElement | ClientRegulatoryElement @@ -163,13 +170,11 @@ export const getTxSegmentElementGCT = async ( transcript: string, chromosome: string, start: string, - end: string, - strand: string + end: string ): Promise => { const params: Array = [ `transcript=${transcript}`, `chromosome=${chromosome}`, - `strand=${strand === "+" ? "%2B" : "-"}`, ]; if (start !== "") params.push(`start=${start}`); if (end !== "") params.push(`end=${end}`); @@ -251,13 +256,11 @@ export const getExonCoords = async ( chromosome: string, start: string, end: string, - strand: string, gene?: string, txAc?: string ): Promise => { const argsArray = [ `chromosome=${chromosome}`, - `strand=${strand === "+" ? "%2B" : "-"}`, gene && gene !== "" ? `gene=${gene}` : "", txAc && txAc !== "" ? `transcript=${txAc}` : "", start && start !== "" ? `start=${start}` : "", diff --git a/requirements.txt b/requirements.txt index a03724bb..0c7c0fdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,89 +1,66 @@ -aiofiles==23.1.0 -anyio==3.7.1 -appdirs==1.4.4 -appnope==0.1.3 -asttokens==2.2.1 -asyncpg==0.28.0 -attrs==23.1.0 -backcall==0.2.0 -beautifulsoup4==4.12.2 -biocommons.seqrepo==0.6.5 +agct==0.1.0.dev2 +aiofiles==24.1.0 +annotated-types==0.7.0 +anyio==4.4.0 +asttokens==2.4.1 +async-timeout==4.0.3 +asyncpg==0.29.0 +attrs==24.2.0 +biocommons.seqrepo==0.6.9 bioutils==0.5.8.post1 -boto3==1.28.11 -botocore==1.31.11 -bs4==0.0.1 +boto3==1.35.4 +botocore==1.35.4 canonicaljson==2.0.0 -certifi==2023.7.22 -charset-normalizer==3.2.0 -click==8.1.6 +certifi==2024.7.4 +charset-normalizer==3.3.2 +click==8.1.7 coloredlogs==15.0.1 -configparser==6.0.0 -cool-seq-tool==0.5.1 -cssselect==1.2.0 -Cython==3.0.0 +configparser==7.1.0 +cool_seq_tool==0.7.0 decorator==5.1.1 -executing==1.2.0 -fake-useragent==1.1.3 -fastapi==0.100.0 -fusor==0.2.0 +executing==2.0.1 +fastapi==0.112.1 +fusor==0.4.2 ga4gh.vrs==2.0.0a10 gene-normalizer==0.4.0 h11==0.14.0 hgvs==1.5.4 humanfriendly==10.0 -idna==3.4 -importlib-metadata==6.8.0 -inflection==0.5.1 -ipython==8.14.0 -jedi==0.18.2 -Jinja2==3.1.2 +idna==3.7 +importlib_metadata==8.4.0 +ipython==8.26.0 +jedi==0.19.1 +Jinja2==3.1.4 jmespath==1.0.1 -jsonschema==3.2.0 -lxml==4.9.3 -Markdown==3.4.4 -MarkupSafe==2.1.3 -matplotlib-inline==0.1.6 -numpy==1.25.1 -pandas==2.0.3 -parse==1.19.1 +MarkupSafe==2.1.5 +matplotlib-inline==0.1.7 Parsley==1.3 -parso==0.8.3 -pexpect==4.8.0 -pickleshare==0.7.5 -prompt-toolkit==3.0.39 -psycopg2==2.9.6 +parso==0.8.4 +pexpect==4.9.0 +polars==1.5.0 +prompt_toolkit==3.0.47 +psycopg2==2.9.9 ptyprocess==0.7.0 -pure-eval==0.2.2 +pure_eval==0.2.3 pydantic==2.4.2 -pyee==8.2.2 -Pygments==2.15.1 -pyliftover==0.4 -pyppeteer==1.0.2 -pyquery==2.0.0 -pyrsistent==0.19.3 -pysam==0.21.0 -python-dateutil==2.8.2 -python-jsonschema-objects==0.4.2 -pytz==2023.3 -PyYAML==6.0.1 -requests==2.31.0 -requests-html==0.10.0 -s3transfer==0.6.1 +pydantic_core==2.10.1 +Pygments==2.18.0 +pysam==0.22.1 +python-dateutil==2.9.0.post0 +requests==2.32.3 +s3transfer==0.10.2 six==1.16.0 -sniffio==1.3.0 -soupsieve==2.4.1 -sqlparse==0.4.4 -stack-data==0.6.2 -starlette==0.27.0 +sniffio==1.3.1 +sqlparse==0.5.1 +stack-data==0.6.3 +starlette==0.38.2 tabulate==0.9.0 -tqdm==4.65.0 -traitlets==5.9.0 -typing_extensions==4.7.1 -tzdata==2023.3 -urllib3==1.26.16 -uvicorn==0.23.1 -w3lib==2.1.1 -wcwidth==0.2.6 -websockets==10.4 +tqdm==4.66.5 +traitlets==5.14.3 +typing_extensions==4.12.2 +urllib3==1.26.19 +uvicorn==0.30.6 +wags_tails==0.1.4 +wcwidth==0.2.13 yoyo-migrations==8.2.0 -zipp==3.16.2 +zipp==3.20.0 diff --git a/server/pyproject.toml b/server/pyproject.toml index 912ad42b..93ae38e1 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -28,8 +28,8 @@ dependencies = [ "click", "boto3", "botocore", - "fusor ~= 0.3.0", - "cool-seq-tool ~= 0.5.1", + "fusor ~= 0.4.2", + "cool-seq-tool ~= 0.7.0", "pydantic == 2.4.2", "gene-normalizer ~= 0.4.0", ] diff --git a/server/src/curfu/routers/constructors.py b/server/src/curfu/routers/constructors.py index 13366249..d4d4d94d 100644 --- a/server/src/curfu/routers/constructors.py +++ b/server/src/curfu/routers/constructors.py @@ -15,7 +15,7 @@ TemplatedSequenceElementResponse, TxSegmentElementResponse, ) -from curfu.sequence_services import InvalidInputError, get_strand +from curfu.sequence_services import get_strand router = APIRouter() @@ -93,7 +93,6 @@ async def build_tx_segment_gct( chromosome: str, start: int | None = Query(None), end: int | None = Query(None), - strand: str | None = Query(None), ) -> TxSegmentElementResponse: """Construct Transcript Segment element by providing transcript and genomic coordinates (chromosome, start, end positions). @@ -107,23 +106,12 @@ async def build_tx_segment_gct( :return: Pydantic class with TranscriptSegment element if successful, and warnings otherwise. """ - if strand is not None: - try: - strand_validated = get_strand(strand) - except InvalidInputError: - warning = f"Received invalid strand value: {strand}" - logger.warning(warning) - return TxSegmentElementResponse(warnings=[warning], element=None) - else: - strand_validated = strand tx_segment, warnings = await request.app.state.fusor.transcript_segment_element( tx_to_genomic_coords=False, transcript=parse_identifier(transcript), - chromosome=parse_identifier(chromosome), - start=start, - end=end, - strand=strand_validated, - residue_mode="inter-residue", + genomic_ac=parse_identifier(chromosome), + seg_start_genomic=start, + seg_end_genomic=end, ) return TxSegmentElementResponse(element=tx_segment, warnings=warnings) @@ -141,7 +129,6 @@ async def build_tx_segment_gcg( chromosome: str, start: int | None = Query(None), end: int | None = Query(None), - strand: str | None = Query(None), ) -> TxSegmentElementResponse: """Construct Transcript Segment element by providing gene and genomic coordinates (chromosome, start, end positions). @@ -155,23 +142,12 @@ async def build_tx_segment_gcg( :return: Pydantic class with TranscriptSegment element if successful, and warnings otherwise. """ - if strand is not None: - try: - strand_validated = get_strand(strand) - except InvalidInputError: - warning = f"Received invalid strand value: {strand}" - logger.warning(warning) - return TxSegmentElementResponse(warnings=[warning], element=None) - else: - strand_validated = strand tx_segment, warnings = await request.app.state.fusor.transcript_segment_element( tx_to_genomic_coords=False, gene=gene, - chromosome=parse_identifier(chromosome), - strand=strand_validated, - start=start, - end=end, - residue_mode="inter-residue", + genomic_ac=parse_identifier(chromosome), + seg_start_genomic=start, + seg_end_genomic=end, ) return TxSegmentElementResponse(element=tx_segment, warnings=warnings) diff --git a/server/src/curfu/routers/demo.py b/server/src/curfu/routers/demo.py index c155b8b3..f2d6646c 100644 --- a/server/src/curfu/routers/demo.py +++ b/server/src/curfu/routers/demo.py @@ -71,7 +71,7 @@ def clientify_structural_element( context :return: client-ready structural element """ - element_args = element.dict() + element_args = element.model_dump() element_args["elementId"] = str(uuid4()) if element.type == StructuralElementType.UNKNOWN_GENE_ELEMENT: @@ -119,7 +119,7 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: :param fusor_instance: FUSOR object instance provided by FastAPI request context :return: completed client-ready fusion """ - fusion_args = fusion.dict() + fusion_args = fusion.model_dump() client_elements = [ clientify_structural_element(element, fusor_instance) for element in fusion.structure @@ -145,7 +145,7 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: if fusion.criticalFunctionalDomains: client_domains = [] for domain in fusion.criticalFunctionalDomains: - client_domain = domain.dict() + client_domain = domain.model_dump() client_domain["domainId"] = str(uuid4()) client_domains.append(client_domain) fusion_args["criticalFunctionalDomains"] = client_domains diff --git a/server/src/curfu/routers/utilities.py b/server/src/curfu/routers/utilities.py index c221fe1c..0f08d955 100644 --- a/server/src/curfu/routers/utilities.py +++ b/server/src/curfu/routers/utilities.py @@ -16,7 +16,6 @@ RouteTag, SequenceIDResponse, ) -from curfu.sequence_services import InvalidInputError, get_strand router = APIRouter() @@ -107,7 +106,7 @@ 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.ex_g_coords_mapper.transcript_to_genomic_coordinates( + response = await request.app.state.fusor.cool_seq_tool.ex_g_coords_mapper.tx_segment_to_genomic( transcript=transcript, gene=gene, exon_start=exon_start, @@ -115,11 +114,11 @@ async def get_genome_coords( exon_start_offset=exon_start_offset, exon_end_offset=exon_end_offset, ) - warnings = response.warnings + warnings = response.errors if warnings: return CoordsUtilsResponse(warnings=warnings, coordinates_data=None) - return CoordsUtilsResponse(coordinates_data=response.genomic_data, warnings=None) + return CoordsUtilsResponse(coordinates_data=response, warnings=None) @router.get( @@ -134,7 +133,6 @@ async def get_exon_coords( chromosome: str, start: int | None = None, end: int | None = None, - strand: str | None = None, gene: str | None = None, transcript: str | None = None, ) -> CoordsUtilsResponse: @@ -145,7 +143,6 @@ async def get_exon_coords( :param chromosome: chromosome, either as a number/X/Y or as an accession :param start: genomic start position :param end: genomic end position - :param strand: strand of genomic position :param gene: gene symbol or ID :param transcript: transcript accession ID :return: response with exon coordinates if successful, or warnings if failed @@ -155,31 +152,23 @@ async def get_exon_coords( warnings.append("Must provide start and/or end coordinates") if transcript is None and gene is None: warnings.append("Must provide gene and/or transcript") - if strand is not None: - try: - strand_validated = get_strand(strand) - except InvalidInputError: - warnings.append(f"Received invalid strand value: {strand}") - else: - strand_validated = strand if warnings: for warning in warnings: logger.warning(warning) return CoordsUtilsResponse(warnings=warnings, coordinates_data=None) - 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, + response = await request.app.state.fusor.cool_seq_tool.ex_g_coords_mapper.genomic_to_tx_segment( + genomic_ac=chromosome, + seg_start_genomic=start, + seg_end_genomic=end, transcript=transcript, gene=gene, ) - warnings = response.warnings + warnings = response.errors if warnings: return CoordsUtilsResponse(warnings=warnings, coordinates_data=None) - return CoordsUtilsResponse(coordinates_data=response.genomic_data, warnings=None) + return CoordsUtilsResponse(coordinates_data=response, warnings=None) @router.get( diff --git a/server/src/curfu/schemas.py b/server/src/curfu/schemas.py index eee530d5..007c6d9b 100644 --- a/server/src/curfu/schemas.py +++ b/server/src/curfu/schemas.py @@ -3,7 +3,7 @@ from enum import Enum from typing import Literal -from cool_seq_tool.schemas import GenomicData +from cool_seq_tool.mappers.exon_genomic_coords import GenomicTxSegService from fusor.models import ( Assay, AssayedFusion, @@ -213,7 +213,7 @@ def validate_number(cls, v) -> int: class CoordsUtilsResponse(Response): """Response model for genomic coordinates retrieval""" - coordinates_data: GenomicData | None + coordinates_data: GenomicTxSegService | None class SequenceIDResponse(Response): diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 4b70125b..3f9e95a2 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -1,5 +1,6 @@ """Provide core fixtures for testing Flask functions.""" +import asyncio from collections.abc import Callable import pytest @@ -8,6 +9,14 @@ from httpx import ASGITransport, AsyncClient +@pytest_asyncio.fixture(scope="session") +def event_loop(): + """Create an instance of the event loop with session scope.""" + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() + + @pytest_asyncio.fixture(scope="session") async def async_client(): """Provide httpx async client fixture.""" @@ -60,7 +69,7 @@ def check_sequence_location(): :param dict sequence_location: sequence location structure """ - def check_sequence_location(sequence_location): + def check_sequence_location(sequence_location, expected_sequence_location): assert "ga4gh:SL." in sequence_location.get("id") assert sequence_location.get("type") == "SequenceLocation" sequence_reference = sequence_location.get("sequenceReference", {}) @@ -68,6 +77,9 @@ def check_sequence_location(sequence_location): assert sequence_reference.get("refgetAccession") assert sequence_reference.get("type") == "SequenceReference" + assert sequence_location.get("start") == expected_sequence_location.get("start") + assert sequence_location.get("end") == expected_sequence_location.get("end") + return check_sequence_location @@ -121,8 +133,7 @@ def ntrk1_tx_element_start(ntrk1_gene): "elementGenomicStart": { "id": "fusor.location_descriptor:NC_000001.11", "type": "SequenceLocation", - "start": 156864429, - "end": 156864430, + "start": 156864354, }, } @@ -136,27 +147,25 @@ def tpm3_tx_t_element(tpm3_gene): "type": "TranscriptSegmentElement", "transcript": "refseq:NM_152263.4", "exonStart": 6, - "exonStartOffset": 71, + "exonStartOffset": 72, "exonEnd": 6, - "exonEndOffset": -4, + "exonEndOffset": -5, "gene": tpm3_gene, "elementGenomicStart": { "id": "fusor.location_descriptor:NC_000001.11", "type": "SequenceLocation", - "start": 154171416, - "end": 154171417, + "end": 154171416, }, "elementGenomicEnd": { "id": "fusor.location_descriptor:NC_000001.11", "type": "SequenceLocation", "start": 154171417, - "end": 154171418, }, } @pytest.fixture(scope="module") -def tpm3_tx_g_element(tpm3_descriptor): +def tpm3_tx_g_element(tpm3_gene): """Provide TranscriptSegmentElement for TPM3 gene constructed using genomic coordinates and gene name. """ @@ -164,20 +173,18 @@ def tpm3_tx_g_element(tpm3_descriptor): "type": "TranscriptSegmentElement", "transcript": "refseq:NM_152263.4", "exonStart": 6, - "exonStartOffset": 5, + "exonStartOffset": 72, "exonEnd": 6, - "exonEndOffset": -71, - "gene": tpm3_descriptor, + "exonEndOffset": -5, + "gene": tpm3_gene, "elementGenomicStart": { "id": "fusor.location_descriptor:NC_000001.11", "type": "SequenceLocation", - "start": 154171483, - "end": 154171484, + "end": 154171416, }, "elementGenomicEnd": { "id": "fusor.location_descriptor:NC_000001.11", "type": "SequenceLocation", - "start": 154171482, - "end": 154171483, + "start": 154171417, }, } diff --git a/server/tests/integration/test_constructors.py b/server/tests/integration/test_constructors.py index ef6db0be..15e0576b 100644 --- a/server/tests/integration/test_constructors.py +++ b/server/tests/integration/test_constructors.py @@ -66,10 +66,12 @@ def check_tx_element_response(response: dict, expected_response: dict): ) genomic_start = response_element.get("elementGenomicStart", {}) genomic_end = response_element.get("elementGenomicEnd", {}) + expected_genomic_start = expected_element.get("elementGenomicStart", {}) + expected_genomic_end = expected_element.get("elementGenomicEnd", {}) if genomic_start: - check_sequence_location(genomic_start) + check_sequence_location(genomic_start, expected_genomic_start) if genomic_end: - check_sequence_location(genomic_end) + check_sequence_location(genomic_end, expected_genomic_end) return check_tx_element_response @@ -95,8 +97,9 @@ def check_re_response(response: dict, expected_response: dict): assert response_re.get("featureId") == expected_re.get("featureId") assert response_re.get("associatedGene") == expected_re.get("associatedGene") sequence_location = response_re.get("sequenceLocation") + expected_sequence_location = expected_re.get("sequenceLocation") if sequence_location: - check_sequence_location(sequence_location) + check_sequence_location(sequence_location, expected_sequence_location) return check_re_response @@ -116,7 +119,7 @@ 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"] - check_sequence_location(response_elem["region"] or {}) + check_sequence_location(response_elem["region"] or {}, expected_elem["region"]) assert response_elem["region"]["start"] == expected_elem["region"]["start"] assert response_elem["region"]["end"] == expected_elem["region"]["end"] @@ -146,7 +149,7 @@ async def test_build_tx_segment_ect( # test handle invalid transcript await check_response( "/api/construct/structural_element/tx_segment_ect?transcript=NM_0012529.3&exon_start=3", - {"warnings": ["Unable to get exons for NM_0012529.3"]}, + {"warnings": ["No exons found given NM_0012529.3"]}, check_tx_element_response, ) @@ -159,12 +162,12 @@ async def test_build_segment_gct( genomic coordinates and transcript. """ await check_response( - "/api/construct/structural_element/tx_segment_gct?transcript=NM_152263.4&chromosome=NC_000001.11&start=154171416&end=154171417&strand=-", + "/api/construct/structural_element/tx_segment_gct?transcript=NM_152263.4&chromosome=NC_000001.11&start=154171416&end=154171417", {"element": tpm3_tx_t_element}, check_tx_element_response, ) await check_response( - "/api/construct/structural_element/tx_segment_gct?transcript=refseq%3ANM_152263.4&chromosome=NC_000001.11&start=154171416&end=154171417&strand=-", + "/api/construct/structural_element/tx_segment_gct?transcript=refseq%3ANM_152263.4&chromosome=NC_000001.11&start=154171416&end=154171417", {"element": tpm3_tx_t_element}, check_tx_element_response, ) @@ -178,7 +181,7 @@ async def test_build_segment_gcg( genomic coordinates and gene name. """ await check_response( - "/api/construct/structural_element/tx_segment_gcg?gene=TPM3&chromosome=NC_000001.11&start=154171416&end=154171417&strand=-", + "/api/construct/structural_element/tx_segment_gcg?gene=TPM3&chromosome=NC_000001.11&start=154171416&end=154171417", {"element": tpm3_tx_g_element}, check_tx_element_response, ) @@ -213,14 +216,14 @@ async def test_build_templated_sequence( "element": { "type": "TemplatedSequenceElement", "region": { - "id": "ga4gh:SL.thjDCmA1u2mB0vLGjgQbCOEg81eP5hdO", + "id": "ga4gh:SL._4tPimZ9AFATsAr2TKp-6VDZMNcQnIf8", "type": "SequenceLocation", "sequenceReference": { "id": "refseq:NC_000001.11", "refgetAccession": "", "type": "SequenceReference", }, - "start": 154171414, + "start": 154171415, "end": 154171417, }, "strand": -1, diff --git a/server/tests/integration/test_utilities.py b/server/tests/integration/test_utilities.py index a74428d1..c027335b 100644 --- a/server/tests/integration/test_utilities.py +++ b/server/tests/integration/test_utilities.py @@ -88,22 +88,55 @@ def check_genomic_coords_response(response: dict, expected_response: dict): assert "warnings" in response assert set(response["warnings"]) == set(expected_response["warnings"]) return - assert response["coordinates_data"] == expected_response["coordinates_data"] + actual_coord_data = response["coordinates_data"] + expected_coord_data = expected_response["coordinates_data"] + + assert actual_coord_data.get("gene") == expected_coord_data.get("gene") + assert actual_coord_data["genomic_ac"] == expected_coord_data.get("genomic_ac") + assert actual_coord_data.get("tx_ac") == expected_coord_data.get("tx_ac") + assert actual_coord_data.get("seg_start") == expected_coord_data.get( + "seg_start" + ) + assert actual_coord_data.get("seg_end") == expected_coord_data.get("seg_end") await check_response( "/api/utilities/get_genomic?transcript=NM_002529.3&exon_start=1&exon_end=6", { "coordinates_data": { "gene": "NTRK1", - "chr": "NC_000001.11", - "start": 156861146, - "end": 156868504, - "exon_start": 1, - "exon_start_offset": 0, - "exon_end": 6, - "exon_end_offset": 0, - "transcript": "NM_002529.3", - "strand": 1, + "genomic_ac": "NC_000001.11", + "tx_ac": "NM_002529.3", + "seg_start": { + "exon_ord": 0, + "offset": 0, + "genomic_location": { + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO", + }, + "start": 156860878, + }, + }, + "seg_end": { + "exon_ord": 5, + "offset": 0, + "genomic_location": { + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO", + }, + "end": 156868647, + }, + }, + "errors": [], + "service_meta": { + "name": "cool_seq_tool", + "version": "0.6.1.dev37+g1f798ae", + "response_datetime": "2024-08-22T17:42:06.009588Z", + "url": "https://github.com/GenomicMedLab/cool-seq-tool", + }, } }, check_genomic_coords_response, @@ -133,26 +166,44 @@ def check_coords_response(response: dict, expected_response: dict): "coordinates_data" not in expected_response ): return - assert response["coordinates_data"] == expected_response["coordinates_data"] + actual_coord_data = response["coordinates_data"] + expected_coord_data = expected_response["coordinates_data"] + + assert actual_coord_data.get("gene") == expected_coord_data.get("gene") + assert actual_coord_data["genomic_ac"] == expected_coord_data.get("genomic_ac") + assert actual_coord_data.get("tx_ac") == expected_coord_data.get("tx_ac") + assert actual_coord_data.get("seg_start") == expected_coord_data.get( + "seg_start" + ) + assert actual_coord_data.get("seg_end") == expected_coord_data.get("seg_end") await check_response( - "/api/utilities/get_exon?chromosome=1&transcript=NM_152263.3&start=154192135&strand=-", + "/api/utilities/get_exon?chromosome=NC_000001.11&transcript=NM_152263.3&start=154192135", { "coordinates_data": { "gene": "TPM3", - "chr": "NC_000001.11", - "start": 154192134, - "exon_start": 1, - "exon_start_offset": 1, - "transcript": "NM_152263.3", - "strand": -1, + "genomic_ac": "NC_000001.11", + "tx_ac": "NM_152263.3", + "seg_start": { + "exon_ord": 0, + "offset": 0, + "genomic_location": { + "type": "SequenceLocation", + "sequenceReference": { + "type": "SequenceReference", + "refgetAccession": "SQ.Ya6Rs7DHhDeg7YaOSg1EoNi3U_nQ9SvO", + }, + "end": 154192135, + }, + }, + "errors": [], } }, check_coords_response, ) await check_response( - "/api/utilities/get_exon?chromosome=1", + "/api/utilities/get_exon?chromosome=NC_000001.11", { "warnings": [ "Must provide start and/or end coordinates", @@ -164,11 +215,7 @@ def check_coords_response(response: dict, expected_response: dict): await check_response( "/api/utilities/get_exon?chromosome=NC_000001.11&start=154192131&gene=TPM3", - { - "warnings": [ - "Unable to find mane data for NC_000001.11 with position 154192130 on gene TPM3" - ] - }, + {"warnings": ["Must find exactly one row for genomic data, but found: 0"]}, check_coords_response, ) diff --git a/server/tests/integration/test_validate.py b/server/tests/integration/test_validate.py index fdd8dadb..d31fabac 100644 --- a/server/tests/integration/test_validate.py +++ b/server/tests/integration/test_validate.py @@ -197,9 +197,10 @@ async def test_validate_fusion( """Perform some basic tests on the fusion validation endpoint.""" await check_validated_fusion_response(async_client, alk_fusion, "ALK fusion") await check_validated_fusion_response(async_client, ewsr1_fusion, "EWSR1 fusion") - await check_validated_fusion_response( - async_client, ewsr1_fusion_fill_types, "EWSR1 fusion needing type inference" - ) + # TODO: add this test back in when https://github.com/cancervariants/fusor/issues/183 is addressed + # await check_validated_fusion_response( + # async_client, ewsr1_fusion_fill_types, "EWSR1 fusion needing type inference" + # ) await check_validated_fusion_response( async_client, wrong_type_fusion, "Wrong fusion type case" )