From dc2f1dc5591eedc9c5aa34e1bc1f7b9223536d18 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Tue, 16 Jul 2024 13:48:58 -0400 Subject: [PATCH 01/18] style: update ruff and drop black (#283) --- .github/workflows/backend_checks.yml | 15 +- .pre-commit-config.yaml | 10 +- server/pyproject.toml | 118 +++++++++++---- server/src/curfu/__init__.py | 8 +- server/src/curfu/cli.py | 27 +--- server/src/curfu/devtools/__init__.py | 5 +- .../src/curfu/devtools/build_client_types.py | 1 + .../src/curfu/devtools/build_gene_suggest.py | 17 ++- server/src/curfu/devtools/build_interpro.py | 109 +++++++------- server/src/curfu/domain_services.py | 13 +- server/src/curfu/gene_services.py | 50 +++---- server/src/curfu/main.py | 8 +- server/src/curfu/routers/complete.py | 7 +- server/src/curfu/routers/constructors.py | 25 ++-- server/src/curfu/routers/demo.py | 102 ++++++------- server/src/curfu/routers/lookup.py | 1 + server/src/curfu/routers/meta.py | 12 +- server/src/curfu/routers/nomenclature.py | 19 ++- server/src/curfu/routers/utilities.py | 96 ++++++------ server/src/curfu/routers/validate.py | 3 +- server/src/curfu/schemas.py | 141 ++++++++---------- server/src/curfu/sequence_services.py | 11 +- server/src/curfu/utils.py | 30 ++-- server/tests/conftest.py | 12 +- server/tests/integration/test_complete.py | 3 +- server/tests/integration/test_constructors.py | 37 +++-- server/tests/integration/test_demos.py | 3 +- server/tests/integration/test_lookup.py | 3 +- server/tests/integration/test_main.py | 5 +- server/tests/integration/test_nomenclature.py | 31 ++-- server/tests/integration/test_utilities.py | 29 ++-- server/tests/integration/test_validate.py | 6 +- 32 files changed, 489 insertions(+), 468 deletions(-) diff --git a/.github/workflows/backend_checks.yml b/.github/workflows/backend_checks.yml index 6f243f49..e60ef44f 100644 --- a/.github/workflows/backend_checks.yml +++ b/.github/workflows/backend_checks.yml @@ -20,12 +20,13 @@ jobs: steps: - uses: actions/checkout@v4 - - name: black - uses: psf/black@stable + - name: Setup Python + uses: actions/setup-python@v5 with: - src: "./server" + python-version: "3.11" - - name: ruff - uses: chartboost/ruff-action@v1 - with: - src: "./server" + - name: Install dependencies + run: python3 -m pip install "server/.[dev]" + + - name: Check style + run: python3 -m ruff check server/ && python3 -m ruff format --check server/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5ebc9143..d6bb6133 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,16 +8,12 @@ repos: - id: end-of-file-fixer - id: check-merge-conflict - id: detect-aws-credentials - - repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - language_version: python3.11 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.286 + rev: v0.5.0 hooks: + - id: ruff-format - id: ruff - args: [--fix, --exit-non-zero-on-fix] + args: [--fix, --exit-non-zero-on-fix, --config=server/pyproject.toml] - repo: https://github.com/pre-commit/mirrors-eslint rev: "v8.20.0" hooks: diff --git a/server/pyproject.toml b/server/pyproject.toml index c7a67021..5c3a601c 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -69,42 +69,104 @@ root = "../." addopts = "--cov=src --cov-report term-missing" testpaths = ["tests"] -[tool.black] -line-length = 88 - [tool.ruff] -# pycodestyle (E, W) -# Pyflakes (F) -# flake8-annotations (ANN) -# flake8-quotes (Q) -# pydocstyle (D) -# pep8-naming (N) -# isort (I) -select = ["E", "W", "F", "ANN", "Q", "D", "N", "I"] - -fixable = ["I"] +src = ["curfu"] +[tool.ruff.lint] +select = [ + "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f + "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "I", # https://docs.astral.sh/ruff/rules/#isort-i + "N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n + "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d + "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async + "S", # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "T10", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + "LOG", # https://docs.astral.sh/ruff/rules/#flake8-logging-log + "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "INP", # https://docs.astral.sh/ruff/rules/#flake8-no-pep420-inp + "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret + "SLF", # https://docs.astral.sh/ruff/rules/#flake8-self-slf + "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "ARG", # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg + "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "PGH", # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh + "PERF", # https://docs.astral.sh/ruff/rules/#perflint-perf + "FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb + "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf +] +fixable = [ + "I", + "F401", + "D", + "UP", + "ANN", + "B", + "C4", + "LOG", + "G", + "PIE", + "PT", + "RSE", + "SIM", + "PERF", + "FURB", + "RUF" +] +# ANN003 - missing-type-kwargs +# ANN101 - missing-type-self +# ANN102 - missing-type-cls +# D203 - one-blank-line-before-class # D205 - blank-line-after-summary +# D206 - indent-with-spaces* +# D213 - multi-line-summary-second-line +# D300 - triple-single-quotes* # D400 - ends-in-period # D415 - ends-in-punctuation -# ANN101 - missing-type-self -# ANN003 - missing-type-kwargs -# E501 - line-too-long -ignore = ["D205", "D400", "D415", "ANN101", "ANN003", "E501"] - -[tool.ruff.flake8-quotes] -docstring-quotes = "double" +# E111 - indentation-with-invalid-multiple* +# E114 - indentation-with-invalid-multiple-comment* +# E117 - over-indented* +# E501 - line-too-long* +# W191 - tab-indentation* +# S321 - suspicious-ftp-lib-usage +# *ignored for compatibility with formatter +ignore = [ + "ANN003", "ANN101", "ANN102", + "D203", "D205", "D206", "D213", "D300", "D400", "D415", + "E111", "E114", "E117", "E501", + "W191", + "S321", +] -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] # ANN001 - missing-type-function-argument # ANN2 - missing-return-type # ANN102 - missing-type-cls -# N805 - invalid-first-argument-name-for-method -# F821 - undefined-name +# S101 - assert +# B011 - assert-false # F401 - unused-import -"tests/*" = ["ANN001", "ANN2", "ANN102"] -"setup.py" = ["F821"] +# N805 - invalid-first-argument-name-for-method +# D301 - escape-sequence-in-docstring +# INP001 - implicit-namespace-package +# ARG001 - unused-function-argument +# B008 - function-call-in-default-argument +"**/tests/*" = ["ANN001", "ANN2", "ANN102", "S101", "B011", "INP001", "ARG001"] "*__init__.py" = ["F401"] -"curfu/schemas.py" = ["ANN201", "N805", "ANN001"] -"curfu/routers/*" = ["D301"] -"curfu/cli.py" = ["D301"] +"**/src/curfu/schemas.py" = ["ANN201", "N805", "ANN001"] +"**/src/curfu/routers/*" = ["D301", "B008"] +"**/src/curfu/cli.py" = ["D301"] + +[tool.ruff.format] +docstring-code-format = true diff --git a/server/src/curfu/__init__.py b/server/src/curfu/__init__.py index 405d6d56..bb833ca9 100644 --- a/server/src/curfu/__init__.py +++ b/server/src/curfu/__init__.py @@ -1,4 +1,5 @@ """Fusion curation interface.""" + import logging from importlib.metadata import PackageNotFoundError, version from os import environ @@ -17,7 +18,7 @@ # establish environment-dependent params if "FUSION_EB_PROD" in environ: environ["FUSION_EB_PROD"] = "true" - LOG_FN = "/tmp/curfu.log" + LOG_FN = "/tmp/curfu.log" # noqa: S108 else: LOG_FN = "curfu.log" @@ -50,10 +51,7 @@ UTA_DB_URL = "postgresql://uta_admin@localhost:5433/uta/uta_20210129" # get local seqrepo location -if "SEQREPO_DATA_PATH" not in environ: - SEQREPO_DATA_PATH = f"{APP_ROOT}/data/seqrepo/latest" -else: - SEQREPO_DATA_PATH = environ["SEQREPO_DATA_PATH"] +SEQREPO_DATA_PATH = environ.get("SEQREPO_DATA_PATH", f"{APP_ROOT}/data/seqrepo/latest") class LookupServiceError(Exception): diff --git a/server/src/curfu/cli.py b/server/src/curfu/cli.py index bb8ec26a..9f838c45 100644 --- a/server/src/curfu/cli.py +++ b/server/src/curfu/cli.py @@ -1,7 +1,7 @@ """Provide command-line interface to application and associated utilities.""" + import os from pathlib import Path -from typing import Optional import click @@ -21,8 +21,8 @@ def serve(port: int) -> None: """ # calling uvicorn.run() doesn't get logs printed to console -- # performing a syscall for now until a more elegant solution appears - os.system( - f"uvicorn curfu.main:app --reload --port={port} --reload-dir={str(APP_ROOT.absolute())}" + os.system( # noqa: S605 + f"uvicorn curfu.main:app --reload --port={port} --reload-dir={APP_ROOT.absolute()!s}" ) @@ -57,29 +57,16 @@ def devtools() -> None: "--uniprot", "-u", help="Path to uniprot_sprot_YYYYMMDD.xml", default=None ) def domains( - types: str, protein2ipr: Optional[str], refs: Optional[str], uniprot: Optional[str] + types: str, protein2ipr: str | None, refs: str | None, uniprot: str | None ) -> None: """Build domain mappings for use in Fusion Curation app. \f :param str types: comma-separated list """ types_split = set(types.lower().replace(" ", "").split(",")) - - if protein2ipr: - protein2ipr_path = Path(protein2ipr) - else: - protein2ipr_path = None - - if uniprot: - uniprot_path = Path(uniprot) - else: - uniprot_path = None - - if refs: - refs_path = Path(refs) - else: - refs_path = None - + protein2ipr_path = Path(protein2ipr) if protein2ipr else None + uniprot_path = Path(uniprot) if uniprot else None + refs_path = Path(refs) if refs else None build_gene_domain_maps( interpro_types=types_split, protein_ipr_path=protein2ipr_path, diff --git a/server/src/curfu/devtools/__init__.py b/server/src/curfu/devtools/__init__.py index c1ba6c1e..7863d01a 100644 --- a/server/src/curfu/devtools/__init__.py +++ b/server/src/curfu/devtools/__init__.py @@ -1,6 +1,7 @@ """Utility functions for application setup.""" + import ftplib -from typing import Callable +from collections.abc import Callable from curfu import logger @@ -18,7 +19,7 @@ def ftp_download(domain: str, path: str, fname: str, callback: Callable) -> None ftp.retrbinary(f"RETR {fname}", callback) except ftplib.all_errors as e: logger.error(f"FTP download failed: {e}") - raise Exception(e) + raise Exception(e) from e # default interpro entry types to try to gather for domains diff --git a/server/src/curfu/devtools/build_client_types.py b/server/src/curfu/devtools/build_client_types.py index 8e2a940d..04655e4a 100644 --- a/server/src/curfu/devtools/build_client_types.py +++ b/server/src/curfu/devtools/build_client_types.py @@ -1,4 +1,5 @@ """Provide client type generation tooling.""" + from pathlib import Path from pydantic2ts.cli.script import generate_typescript_defs diff --git a/server/src/curfu/devtools/build_gene_suggest.py b/server/src/curfu/devtools/build_gene_suggest.py index f56ff3ab..f4a1f449 100644 --- a/server/src/curfu/devtools/build_gene_suggest.py +++ b/server/src/curfu/devtools/build_gene_suggest.py @@ -1,9 +1,9 @@ """Provide tools to build backend data relating to gene identification.""" + import csv -from datetime import datetime as dt +import datetime from pathlib import Path from timeit import default_timer as timer -from typing import Dict, List, Optional import click from biocommons.seqrepo.seqrepo import SeqRepo @@ -25,7 +25,7 @@ def __init__(self) -> None: self.sr = SeqRepo(SEQREPO_DATA_PATH) self.genes = [] - def _get_chromosome(self, record: Dict) -> Optional[str]: + def _get_chromosome(self, record: dict) -> str | None: """Extract readable chromosome identifier from gene extensions. :param record: stored normalized record @@ -42,7 +42,7 @@ def _get_chromosome(self, record: Dict) -> Optional[str]: return None @staticmethod - def _make_list_column(values: List[str]) -> str: + def _make_list_column(values: list[str]) -> str: """Convert a list of strings into a comma-separated string, filtering out non-alphabetic values. @@ -62,12 +62,13 @@ def _make_list_column(values: List[str]) -> str: :param values: A list of strings to be converted into a comma-separated string. :return: A comma-separated string containing unique, alphabetic values from the input list. + """ unique = {v.upper() for v in values} filtered = {v for v in unique if any(char.isalpha() for char in v)} return ",".join(filtered) - def _process_gene_record(self, record: Dict) -> None: + def _process_gene_record(self, record: dict) -> None: """Add the gene record to processed suggestions. :param record: gene record object retrieved from DB @@ -117,8 +118,10 @@ def _save_suggest_file(self, output_dir: Path) -> None: "chromosome", "strand", ] - today = dt.strftime(dt.today(), "%Y%m%d") - with open(output_dir / f"gene_suggest_{today}.csv", "w") as csvfile: + today = datetime.datetime.strftime( + datetime.datetime.now(tz=datetime.timezone.utc), "%Y%m%d" + ) + with (output_dir / f"gene_suggest_{today}.csv").open("w") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() for row in self.genes: diff --git a/server/src/curfu/devtools/build_interpro.py b/server/src/curfu/devtools/build_interpro.py index 112fe277..17fddb8d 100644 --- a/server/src/curfu/devtools/build_interpro.py +++ b/server/src/curfu/devtools/build_interpro.py @@ -1,13 +1,12 @@ """Provide utilities relating to data fetched from InterPro service.""" + import csv +import datetime import gzip -import os import shutil import xml.etree.ElementTree as ET # noqa: N817 -from datetime import datetime from pathlib import Path from timeit import default_timer as timer -from typing import Dict, Optional, Set, Tuple import click from gene.database import create_db @@ -17,10 +16,10 @@ from curfu.devtools import ftp_download # uniprot accession id -> (normalized ID, normalized label) -UniprotRefs = Dict[str, Tuple[str, str]] +UniprotRefs = dict[str, tuple[str, str]] # (uniprot accession id, ncbi gene id) -> refseq NP_ accession -UniprotAcRefs = Dict[Tuple[str, str], str] +UniprotAcRefs = dict[tuple[str, str], str] # consistent formatting for saved files DATE_FMT = "%Y%m%d" @@ -33,21 +32,23 @@ def download_protein2ipr(output_dir: Path) -> None: logger.info("Retrieving Uniprot mapping data from InterPro") gz_file_path = output_dir / "protein2ipr.dat.gz" - with open(gz_file_path, "w") as fp: - - def writefile(data): # noqa - fp.write(data) - + with gz_file_path.open("w") as fp: ftp_download( - "ftp.ebi.ac.uk", "pub/databases/interpro", "protein2ipr.dat.gz", writefile + "ftp.ebi.ac.uk", + "pub/databases/interpro", + "protein2ipr.dat.gz", + lambda data: fp.write(data), ) - today = datetime.strftime(datetime.today(), DATE_FMT) + today = datetime.datetime.strftime( + datetime.datetime.now(tz=datetime.timezone.utc), DATE_FMT + ) outfile_path = output_dir / f"protein2ipr_{today}.dat" - with open(outfile_path, "wb") as f_out, gzip.open(gz_file_path, "rb") as f_in: + with outfile_path.open("wb") as f_out, gzip.open(gz_file_path, "rb") as f_in: shutil.copyfileobj(f_in, f_out) - os.remove(gz_file_path) - assert outfile_path.exists() + gz_file_path.unlink() + if not outfile_path.exists(): + raise Exception logger.info("Successfully retrieved UniProt mapping data for Interpro") @@ -84,9 +85,9 @@ 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 # type: ignore - norm_label = norm_response.gene_descriptor.label # type: ignore - uniprot_ids[uniprot_id] = (norm_id, norm_label) # type: ignore + norm_id = norm_response.gene_descriptor.gene_id + norm_label = norm_response.gene_descriptor.label + uniprot_ids[uniprot_id] = (norm_id, norm_label) if not last_evaluated_key: break @@ -95,9 +96,11 @@ def get_uniprot_refs() -> UniprotRefs: logger.info(msg) click.echo(msg) - today = datetime.strftime(datetime.today(), DATE_FMT) + today = datetime.datetime.strftime( + datetime.datetime.now(tz=datetime.timezone.utc), DATE_FMT + ) save_path = APP_ROOT / "data" / f"uniprot_refs_{today}.tsv" - with open(save_path, "w") as out: + with save_path.open("w") as out: for uniprot_ref, data in uniprot_ids.items(): out.write(f"{uniprot_ref.split(':')[1].upper()}\t{data[0]}\t{data[1]}\n") @@ -111,30 +114,33 @@ def download_uniprot_sprot(output_dir: Path) -> Path: logger.info("Retrieving UniProtKB data.") gz_file_path = output_dir / "uniprot_sprot.xml.gz" - with open(gz_file_path, "w") as fp: + with gz_file_path.open("w") as fp: ftp_download( "ftp.uniprot.org", "pub/databases/uniprot/current_release/knowledgebase/complete/", "uniprot_sprot.xml.gz", lambda data: fp.write(data), ) - today = datetime.strftime(datetime.today(), DATE_FMT) + today = datetime.datetime.strftime( + datetime.datetime.now(tz=datetime.timezone.utc), DATE_FMT + ) outfile_path = output_dir / f"uniprot_sprot_{today}.dat" - with open(outfile_path, "wb") as f_out, gzip.open(gz_file_path, "rb") as f_in: + with outfile_path.open("wb") as f_out, gzip.open(gz_file_path, "rb") as f_in: shutil.copyfileobj(f_in, f_out) - os.remove(gz_file_path) - assert outfile_path.exists() + gz_file_path.unlink() + if not outfile_path.exists(): + raise Exception logger.info("Successfully retrieved UniProtKB data.") return outfile_path def get_interpro_uniprot_rels( - protein_ipr_path: Optional[Path], + protein_ipr_path: Path | None, output_dir: Path, - domain_ids: Set[str], - uniprot_refs: Dict, -) -> Dict[str, Dict[str, Tuple[str, str, str, str, str]]]: + domain_ids: set[str], + uniprot_refs: dict, +) -> dict[str, dict[str, tuple[str, str, str, str, str]]]: """Process InterPro to UniProtKB relations, using UniProt references to connect genes with domains @@ -146,9 +152,11 @@ def get_interpro_uniprot_rels( """ if not protein_ipr_path: download_protein2ipr(output_dir) - today = datetime.strftime(datetime.today(), DATE_FMT) + today = datetime.datetime.strftime( + datetime.datetime.now(tz=datetime.timezone.utc), DATE_FMT + ) protein_ipr_path = output_dir / f"protein2ipr_{today}.dat" - protein_ipr = open(protein_ipr_path, "r") + protein_ipr = protein_ipr_path.open() protein_ipr_reader = csv.reader(protein_ipr, delimiter="\t") interpro_uniprot = {} @@ -179,8 +187,8 @@ def get_interpro_uniprot_rels( def get_protein_accessions( - relevant_proteins: Set[str], uniprot_sprot_path: Optional[Path] -) -> Dict[Tuple[str, str], str]: + relevant_proteins: set[str], uniprot_sprot_path: Path | None +) -> dict[tuple[str, str], str]: """Scan uniprot_sprot.xml and extract RefSeq protein accession identifiers for relevant Uniprot accessions. :param Set[str] relevant_proteins: captured Uniprot accessions, for proteins coded @@ -192,7 +200,7 @@ def get_protein_accessions( start = timer() if not uniprot_sprot_path: uniprot_sprot_path = download_uniprot_sprot(APP_ROOT / "data") - parser = ET.iterparse(uniprot_sprot_path, ("start", "end")) + parser = ET.iterparse(uniprot_sprot_path, ("start", "end")) # noqa: S314 accessions_map = {} cur_ac = "" cur_refseq_ac = "" @@ -262,10 +270,10 @@ def get_protein_accessions( def build_gene_domain_maps( - interpro_types: Set[str], - protein_ipr_path: Optional[Path] = None, - uniprot_sprot_path: Optional[Path] = None, - uniprot_refs_path: Optional[Path] = None, + interpro_types: set[str], + protein_ipr_path: Path | None = None, + uniprot_sprot_path: Path | None = None, + uniprot_refs_path: Path | None = None, output_dir: Path = APP_ROOT / "data", ) -> None: """Produce the gene-to-domain lookup table at out_path using the Interpro-Uniprot @@ -279,16 +287,15 @@ def build_gene_domain_maps( directory. """ start_time = timer() - today = datetime.strftime(datetime.today(), DATE_FMT) + today = datetime.strftime(datetime.datetime.now(tz=datetime.timezone.utc), DATE_FMT) # get relevant Interpro IDs interpro_data_bin = [] - - def get_interpro_data(data): # noqa - interpro_data_bin.append(data) - ftp_download( - "ftp.ebi.ac.uk", "pub/databases/interpro", "entry.list", get_interpro_data + "ftp.ebi.ac.uk", + "pub/databases/interpro", + "entry.list", + lambda data: interpro_data_bin.append(data), ) # load interpro IDs directly to memory -- no need to save to file interpro_data_tsv = "".join([d.decode("UTF-8") for d in interpro_data_bin]).split( @@ -297,16 +304,16 @@ def get_interpro_data(data): # noqa interpro_types = {t.lower() for t in interpro_types} interpro_reader = csv.reader(interpro_data_tsv, delimiter="\t") interpro_reader.__next__() # skip header - domain_ids = set( - [row[0] for row in interpro_reader if row and row[1].lower() in interpro_types] - ) + domain_ids = { + row[0] for row in interpro_reader if row and row[1].lower() in interpro_types + } # get Uniprot to gene references if not uniprot_refs_path: uniprot_refs: UniprotRefs = get_uniprot_refs() else: uniprot_refs = {} - with open(uniprot_refs_path, "r") as f: + with uniprot_refs_path.open() as f: reader = csv.reader(f, delimiter="\t") for row in reader: uniprot_refs[row[0]] = (row[1], row[2]) @@ -317,11 +324,11 @@ def get_interpro_data(data): # noqa ) # get refseq accessions for uniprot proteins - uniprot_acs = {k[0] for k in interpro_uniprot.keys()} + uniprot_acs = {k[0] for k in interpro_uniprot} prot_acs = get_protein_accessions(uniprot_acs, uniprot_sprot_path) outfile_path = output_dir / f"domain_lookup_{today}.tsv" - outfile = open(outfile_path, "w") + outfile = outfile_path.open("w") for k, v_list in interpro_uniprot.items(): for v in v_list.values(): if k[0] in uniprot_acs: @@ -329,7 +336,7 @@ def get_interpro_data(data): # noqa if not refseq_ac: logger.warning(f"Unable to lookup refseq ac for {k}, {v}") continue - items = [k[1]] + list(v) + [refseq_ac] + items = [k[1], *list(v), refseq_ac] line = "\t".join(items) + "\n" outfile.write(line) outfile.close() diff --git a/server/src/curfu/domain_services.py b/server/src/curfu/domain_services.py index ca1b08af..920545a7 100644 --- a/server/src/curfu/domain_services.py +++ b/server/src/curfu/domain_services.py @@ -6,8 +6,9 @@ * get_possible_domains shouldn't have to force uniqueness """ + import csv -from typing import Dict, List +from typing import ClassVar from curfu import LookupServiceError, logger from curfu.utils import get_data_file @@ -16,7 +17,7 @@ class DomainService: """Handler class providing requisite services for functional domain lookup.""" - domains: Dict[str, List[Dict]] = {} + domains: ClassVar[dict[str, list[dict]]] = {} def load_mapping(self) -> None: """Load mapping file. @@ -32,7 +33,7 @@ def load_mapping(self) -> None: * RefSeq protein accession """ domain_file = get_data_file("domain_lookup") - with open(domain_file, "r") as df: + with domain_file.open() as df: reader = csv.reader(df, delimiter="\t") for row in reader: gene_id = row[0].lower() @@ -48,7 +49,7 @@ def load_mapping(self) -> None: else: self.domains[gene_id] = [domain_data] - def get_possible_domains(self, gene_id: str) -> List[Dict]: + def get_possible_domains(self, gene_id: str) -> list[dict]: """Given normalized gene ID, return associated domain names and IDs :return: List of valid domain names (up to n names) paired with domain IDs @@ -56,7 +57,7 @@ def get_possible_domains(self, gene_id: str) -> List[Dict]: """ try: domains = self.domains[gene_id.lower()] - except KeyError: + except KeyError as e: logger.warning(f"Unable to retrieve associated domains for {gene_id}") - raise LookupServiceError + raise LookupServiceError from e return domains diff --git a/server/src/curfu/gene_services.py b/server/src/curfu/gene_services.py index 5d43529b..5147a5c9 100644 --- a/server/src/curfu/gene_services.py +++ b/server/src/curfu/gene_services.py @@ -1,7 +1,7 @@ """Wrapper for required Gene Normalization services.""" + import csv from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE from gene.query import QueryHandler @@ -11,18 +11,18 @@ from curfu.utils import get_data_file # term -> (normalized ID, normalized label) -Map = Dict[str, Tuple[str, str, str]] +Map = dict[str, tuple[str, str, str]] # term -> (normalized ID, normalized label) -Map = Dict[str, Tuple[str, str, str]] +Map = dict[str, tuple[str, str, str]] # term, symbol, concept ID, chromosome, strand -Suggestion = Tuple[str, str, str, str, str] +Suggestion = tuple[str, str, str, str, str] class GeneService: """Provide gene ID resolution and term autocorrect suggestions.""" - def __init__(self, suggestions_file: Optional[Path] = None) -> None: + def __init__(self, suggestions_file: Path | None = None) -> None: """Initialize gene service provider class. :param suggestions_file: path to existing suggestions file. If not provided, @@ -31,28 +31,26 @@ def __init__(self, suggestions_file: Optional[Path] = None) -> None: if not suggestions_file: suggestions_file = get_data_file("gene_suggest") - self.concept_id_map: Dict[str, Suggestion] = {} - self.symbol_map: Dict[str, Suggestion] = {} - self.aliases_map: Dict[str, Suggestion] = {} - self.prev_symbols_map: Dict[str, Suggestion] = {} + self.concept_id_map: dict[str, Suggestion] = {} + self.symbol_map: dict[str, Suggestion] = {} + self.aliases_map: dict[str, Suggestion] = {} + self.prev_symbols_map: dict[str, Suggestion] = {} - for row in csv.DictReader(open(suggestions_file, "r")): + for row in csv.DictReader(suggestions_file.open()): symbol = row["symbol"] concept_id = row["concept_id"] suggestion = [symbol, concept_id, row["chromosome"], row["strand"]] - self.concept_id_map[concept_id.upper()] = tuple([concept_id] + suggestion) - self.symbol_map[symbol.upper()] = tuple([symbol] + suggestion) + self.concept_id_map[concept_id.upper()] = (concept_id, *suggestion) + self.symbol_map[symbol.upper()] = (symbol, *suggestion) for alias in row.get("aliases", []): - self.aliases_map[alias.upper()] = tuple([alias] + suggestion) + self.aliases_map[alias.upper()] = (alias, *suggestion) for prev_symbol in row.get("previous_symbols", []): - self.prev_symbols_map[prev_symbol.upper()] = tuple( - [prev_symbol] + suggestion - ) + self.prev_symbols_map[prev_symbol.upper()] = (prev_symbol, *suggestion) @staticmethod def get_normalized_gene( term: str, normalizer: QueryHandler - ) -> Tuple[CURIE, str, Union[str, CURIE, None]]: + ) -> tuple[CURIE, str, str | CURIE | None]: """Get normalized ID given gene symbol/label/alias. :param str term: user-entered gene term :param QueryHandler normalizer: gene normalizer instance @@ -108,16 +106,15 @@ 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}" # noqa: E501 - ) # noqa: E501 + f"Couldn't find cased version for search term {term} matching gene ID {response.gene_descriptor.gene_id}" + ) return (concept_id, symbol, term_cased) - else: - warn = f"Lookup of gene term {term} failed." - logger.warning(warn) - raise LookupServiceError(warn) + warn = f"Lookup of gene term {term} failed." + logger.warning(warn) + raise LookupServiceError(warn) @staticmethod - def _get_completion_results(term: str, lookup: Dict) -> List[Suggestion]: + def _get_completion_results(term: str, lookup: dict) -> list[Suggestion]: """Filter valid completions for term. :param term: user-entered text @@ -129,10 +126,9 @@ def _get_completion_results(term: str, lookup: Dict) -> List[Suggestion]: for key, data in lookup.items(): if key.startswith(term): matches.append(data) - matches = sorted(matches, key=lambda s: s[0]) - return matches + return sorted(matches, key=lambda s: s[0]) - def suggest_genes(self, query: str) -> Dict[str, List[Suggestion]]: + def suggest_genes(self, query: str) -> dict[str, list[Suggestion]]: """Provide autocomplete suggestions based on submitted term. :param str query: text entered by user diff --git a/server/src/curfu/main.py b/server/src/curfu/main.py index a14cb86b..f56edef4 100644 --- a/server/src/curfu/main.py +++ b/server/src/curfu/main.py @@ -1,4 +1,5 @@ """Provide FastAPI application and route declarations.""" + from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles @@ -75,7 +76,7 @@ def serve_react_app(app: FastAPI) -> FastAPI: 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: + 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 @@ -110,8 +111,7 @@ def get_gene_services() -> GeneService: :return: GeneService instance """ - gene_services = GeneService() - return gene_services + return GeneService() def get_domain_services() -> DomainService: @@ -135,4 +135,4 @@ async def startup() -> None: @app.on_event("shutdown") async def shutdown() -> None: """Clean up thread pool.""" - await app.state.fusor.cool_seq_tool.uta_db._connection_pool.close() + await app.state.fusor.cool_seq_tool.uta_db._connection_pool.close() # noqa: SLF001 diff --git a/server/src/curfu/routers/complete.py b/server/src/curfu/routers/complete.py index 5b03bb71..ff5c9c33 100644 --- a/server/src/curfu/routers/complete.py +++ b/server/src/curfu/routers/complete.py @@ -1,5 +1,6 @@ """Provide routes for autocomplete/term suggestion methods""" -from typing import Any, Dict + +from typing import Any from fastapi import APIRouter, Query, Request @@ -41,7 +42,7 @@ def suggest_gene(request: Request, term: str = Query("")) -> ResponseDict: response["matches_count"] = n if n > MAX_SUGGESTIONS: - warn = f"Exceeds max matches: Got {n} possible matches for {term} (limit: {MAX_SUGGESTIONS})" # noqa: E501 + warn = f"Exceeds max matches: Got {n} possible matches for {term} (limit: {MAX_SUGGESTIONS})" response["warnings"] = [warn] term_upper = term.upper() for match_type in ("concept_id", "symbol", "prev_symbols", "aliases"): @@ -69,7 +70,7 @@ def suggest_domain(request: Request, gene_id: str = Query("")) -> ResponseDict: :return: JSON response with a list of possible domain name and ID options, or warning(s) if relevant """ - response: Dict[str, Any] = {"gene_id": gene_id} + response: dict[str, Any] = {"gene_id": gene_id} try: possible_matches = request.app.state.domains.get_possible_domains(gene_id) response["suggestions"] = possible_matches diff --git a/server/src/curfu/routers/constructors.py b/server/src/curfu/routers/constructors.py index fab4542e..a7cae563 100644 --- a/server/src/curfu/routers/constructors.py +++ b/server/src/curfu/routers/constructors.py @@ -1,5 +1,4 @@ """Provide routes for element construction endpoints""" -from typing import Optional from fastapi import APIRouter, Query, Request from fusor.models import DomainStatus, RegulatoryClass, Strand @@ -38,11 +37,9 @@ def build_gene_element(request: Request, term: str = Query("")) -> GeneElementRe :return: Pydantic class with gene element if successful and warnings otherwise """ gene_element, warnings = request.app.state.fusor.gene_element(term) - if not warnings: - warnings_l = [] - else: - warnings_l = [warnings] - return GeneElementResponse(element=gene_element, warnings=warnings_l) + return GeneElementResponse( + element=gene_element, warnings=[] if not warnings else [warnings] + ) @router.get( @@ -55,9 +52,9 @@ def build_gene_element(request: Request, term: str = Query("")) -> GeneElementRe async def build_tx_segment_ect( request: Request, transcript: str, - exon_start: Optional[int] = Query(None), + exon_start: int | None = Query(None), exon_start_offset: int = Query(0), - exon_end: Optional[int] = Query(None), + exon_end: int | None = Query(None), exon_end_offset: int = Query(0), ) -> TxSegmentElementResponse: """Construct Transcript Segment element by providing transcript and exon @@ -94,9 +91,9 @@ async def build_tx_segment_gct( request: Request, transcript: str, chromosome: str, - start: Optional[int] = Query(None), - end: Optional[int] = Query(None), - strand: Optional[str] = Query(None), + 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). @@ -142,9 +139,9 @@ async def build_tx_segment_gcg( request: Request, gene: str, chromosome: str, - start: Optional[int] = Query(None), - end: Optional[int] = Query(None), - strand: Optional[str] = Query(None), + 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). diff --git a/server/src/curfu/routers/demo.py b/server/src/curfu/routers/demo.py index 434f3e13..2434479a 100644 --- a/server/src/curfu/routers/demo.py +++ b/server/src/curfu/routers/demo.py @@ -1,5 +1,5 @@ """Provide routes for accessing demo objects to client.""" -from typing import Union + from uuid import uuid4 from fastapi import APIRouter, Request @@ -40,24 +40,24 @@ router = APIRouter() -ElementUnion = Union[ - TranscriptSegmentElement, - LinkerElement, - TemplatedSequenceElement, - GeneElement, - UnknownGeneElement, - MultiplePossibleGenesElement, -] -ClientElementUnion = Union[ - ClientTranscriptSegmentElement, - ClientLinkerElement, - ClientTemplatedSequenceElement, - ClientGeneElement, - ClientUnknownGeneElement, - ClientMultiplePossibleGenesElement, -] -Fusion = Union[CategoricalFusion, AssayedFusion] -ClientFusion = Union[ClientCategoricalFusion, ClientAssayedFusion] +ElementUnion = ( + TranscriptSegmentElement + | LinkerElement + | TemplatedSequenceElement + | GeneElement + | UnknownGeneElement + | MultiplePossibleGenesElement +) +ClientElementUnion = ( + ClientTranscriptSegmentElement + | ClientLinkerElement + | ClientTemplatedSequenceElement + | ClientGeneElement + | ClientUnknownGeneElement + | ClientMultiplePossibleGenesElement +) +Fusion = CategoricalFusion | AssayedFusion +ClientFusion = ClientCategoricalFusion | ClientAssayedFusion def clientify_structural_element( @@ -77,14 +77,14 @@ def clientify_structural_element( if element.type == StructuralElementType.UNKNOWN_GENE_ELEMENT: element_args["nomenclature"] = "?" return ClientUnknownGeneElement(**element_args) - elif element.type == StructuralElementType.MULTIPLE_POSSIBLE_GENES_ELEMENT: + if element.type == StructuralElementType.MULTIPLE_POSSIBLE_GENES_ELEMENT: element_args["nomenclature"] = "v" return ClientMultiplePossibleGenesElement(**element_args) - elif element.type == StructuralElementType.LINKER_SEQUENCE_ELEMENT: + if element.type == StructuralElementType.LINKER_SEQUENCE_ELEMENT: nm = element.linker_sequence.sequence element_args["nomenclature"] = nm return ClientLinkerElement(**element_args) - elif element.type == StructuralElementType.TEMPLATED_SEQUENCE_ELEMENT: + 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( @@ -93,11 +93,11 @@ def clientify_structural_element( element_args["input_start"] = element.region.location.interval.start.value element_args["input_end"] = element.region.location.interval.end.value return ClientTemplatedSequenceElement(**element_args) - elif element.type == StructuralElementType.GENE_ELEMENT: + if element.type == StructuralElementType.GENE_ELEMENT: nm = gene_nomenclature(element) element_args["nomenclature"] = nm return ClientGeneElement(**element_args) - elif element.type == StructuralElementType.TRANSCRIPT_SEGMENT_ELEMENT: + if element.type == StructuralElementType.TRANSCRIPT_SEGMENT_ELEMENT: nm = tx_segment_nomenclature(element) element_args["nomenclature"] = nm element_args["input_type"] = "exon_coords_tx" @@ -107,8 +107,8 @@ def clientify_structural_element( element_args["input_exon_end"] = element.exon_end element_args["input_exon_end_offset"] = element.exon_end_offset return ClientTranscriptSegmentElement(**element_args) - else: - raise ValueError("Unknown element type provided") + msg = "Unknown element type provided" + raise ValueError(msg) def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: @@ -119,12 +119,13 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: :return: completed client-ready fusion """ fusion_args = fusion.dict() - client_elements = [] - for element in fusion.structural_elements: - client_elements.append(clientify_structural_element(element, fusor_instance)) + client_elements = [ + clientify_structural_element(element, fusor_instance) + for element in fusion.structural_elements + ] fusion_args["structural_elements"] = client_elements - if "regulatory_element" in fusion_args and fusion_args["regulatory_element"]: + if fusion_args.get("regulatory_element"): reg_element_args = fusion_args["regulatory_element"] nomenclature = reg_element_nomenclature( RegulatoryElement(**reg_element_args), fusor_instance.seqrepo @@ -134,7 +135,8 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: if regulatory_class == "enhancer": reg_element_args["display_class"] = "Enhancer" else: - raise Exception("Undefined reg element class used in demo") + msg = "Undefined reg element class used in demo" + raise Exception(msg) fusion_args["regulatory_element"] = reg_element_args if fusion.type == FUSORTypes.CATEGORICAL_FUSION: @@ -146,10 +148,10 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: client_domains.append(client_domain) fusion_args["critical_functional_domains"] = client_domains return ClientCategoricalFusion(**fusion_args) - elif fusion.type == FUSORTypes.ASSAYED_FUSION: + if fusion.type == FUSORTypes.ASSAYED_FUSION: return ClientAssayedFusion(**fusion_args) - else: - raise ValueError("Unknown fusion type provided") + msg = "Unknown fusion type provided" + raise ValueError(msg) @router.get( @@ -166,10 +168,7 @@ def get_alk(request: Request) -> DemoResponse: FUSOR and UTA-associated tools. """ return DemoResponse( - **{ - "fusion": clientify_fusion(examples.alk, request.app.state.fusor), - "warnings": [], - } + fusion=clientify_fusion(examples.alk, request.app.state.fusor), warnings=[] ) @@ -187,10 +186,7 @@ def get_ewsr1(request: Request) -> DemoResponse: FUSOR and UTA-associated tools. """ return DemoResponse( - **{ - "fusion": clientify_fusion(examples.ewsr1, request.app.state.fusor), - "warnings": [], - } + fusion=clientify_fusion(examples.ewsr1, request.app.state.fusor), warnings=[] ) @@ -208,10 +204,7 @@ def get_bcr_abl1(request: Request) -> DemoResponse: FUSOR and UTA-associated tools. """ return DemoResponse( - **{ - "fusion": clientify_fusion(examples.bcr_abl1, request.app.state.fusor), - "warnings": [], - } + fusion=clientify_fusion(examples.bcr_abl1, request.app.state.fusor), warnings=[] ) @@ -229,10 +222,8 @@ def get_tpm3_ntrk1(request: Request) -> DemoResponse: FUSOR and UTA-associated tools. """ return DemoResponse( - **{ - "fusion": clientify_fusion(examples.tpm3_ntrk1, request.app.state.fusor), - "warnings": [], - } + fusion=clientify_fusion(examples.tpm3_ntrk1, request.app.state.fusor), + warnings=[], ) @@ -250,10 +241,8 @@ def get_tpm3_pdgfrb(request: Request) -> DemoResponse: FUSOR and UTA-associated tools. """ return DemoResponse( - **{ - "fusion": clientify_fusion(examples.tpm3_pdgfrb, request.app.state.fusor), - "warnings": [], - } + fusion=clientify_fusion(examples.tpm3_pdgfrb, request.app.state.fusor), + warnings=[], ) @@ -270,8 +259,5 @@ def get_igh_myc(request: Request) -> DemoResponse: FUSOR and UTA-associated tools. """ return DemoResponse( - **{ - "fusion": clientify_fusion(examples.igh_myc, request.app.state.fusor), - "warnings": [], - } + fusion=clientify_fusion(examples.igh_myc, request.app.state.fusor), warnings=[] ) diff --git a/server/src/curfu/routers/lookup.py b/server/src/curfu/routers/lookup.py index 4e66948f..ea851ef6 100644 --- a/server/src/curfu/routers/lookup.py +++ b/server/src/curfu/routers/lookup.py @@ -1,4 +1,5 @@ """Provide routes for basic data lookup endpoints""" + from fastapi import APIRouter, Query, Request from curfu import LookupServiceError diff --git a/server/src/curfu/routers/meta.py b/server/src/curfu/routers/meta.py index 85211baf..506cd103 100644 --- a/server/src/curfu/routers/meta.py +++ b/server/src/curfu/routers/meta.py @@ -1,4 +1,5 @@ """Provide service meta information""" + from cool_seq_tool.version import __version__ as cool_seq_tool_version from fastapi import APIRouter from fusor import __version__ as fusor_version @@ -18,11 +19,8 @@ def get_service_info() -> ServiceInfoResponse: """Return service info.""" return ServiceInfoResponse( - **{ - "curfu_version": curfu_version, - # "vrs_python_version": vrs_version, - "cool_seq_tool_version": cool_seq_tool_version, - "fusor_version": fusor_version, - "warnings": [], - } + curfu_version=curfu_version, + cool_seq_tool_version=cool_seq_tool_version, + fusor_version=fusor_version, + warnings=[], ) diff --git a/server/src/curfu/routers/nomenclature.py b/server/src/curfu/routers/nomenclature.py index 710cdc84..55b83743 100644 --- a/server/src/curfu/routers/nomenclature.py +++ b/server/src/curfu/routers/nomenclature.py @@ -1,5 +1,4 @@ """Provide routes for nomenclature generation.""" -from typing import Dict from fastapi import APIRouter, Body, Request from fusor.exceptions import FUSORParametersException @@ -31,7 +30,7 @@ tags=[RouteTag.NOMENCLATURE], ) def generate_regulatory_element_nomenclature( - request: Request, regulatory_element: Dict = Body() + request: Request, regulatory_element: dict = Body() ) -> ResponseDict: """Build regulatory element nomenclature. @@ -46,7 +45,7 @@ def generate_regulatory_element_nomenclature( except ValidationError as e: error_msg = str(e) logger.warning( - f"Encountered ValidationError: {error_msg} for regulatory element: {regulatory_element}" # noqa: E501 + f"Encountered ValidationError: {error_msg} for regulatory element: {regulatory_element}" ) return {"warnings": [error_msg]} try: @@ -59,7 +58,7 @@ def generate_regulatory_element_nomenclature( ) return { "warnings": [ - f"Unable to validate regulatory element with provided parameters: {regulatory_element}" # noqa: E501 + f"Unable to validate regulatory element with provided parameters: {regulatory_element}" ] } return {"nomenclature": nomenclature} @@ -72,7 +71,7 @@ def generate_regulatory_element_nomenclature( response_model_exclude_none=True, tags=[RouteTag.NOMENCLATURE], ) -def generate_tx_segment_nomenclature(tx_segment: Dict = Body()) -> ResponseDict: +def generate_tx_segment_nomenclature(tx_segment: dict = Body()) -> ResponseDict: """Build transcript segment element nomenclature. \f @@ -101,7 +100,7 @@ def generate_tx_segment_nomenclature(tx_segment: Dict = Body()) -> ResponseDict: tags=[RouteTag.NOMENCLATURE], ) def generate_templated_seq_nomenclature( - request: Request, templated_sequence: Dict = Body() + request: Request, templated_sequence: dict = Body() ) -> ResponseDict: """Build templated sequence element nomenclature. \f @@ -115,7 +114,7 @@ def generate_templated_seq_nomenclature( except ValidationError as e: error_msg = str(e) logger.warning( - f"Encountered ValidationError: {error_msg} for templated sequence element: {templated_sequence}" # noqa: E501 + f"Encountered ValidationError: {error_msg} for templated sequence element: {templated_sequence}" ) return {"warnings": [error_msg]} try: @@ -128,7 +127,7 @@ def generate_templated_seq_nomenclature( ) return { "warnings": [ - f"Unable to validate templated sequence with provided parameters: {templated_sequence}" # noqa: E501 + f"Unable to validate templated sequence with provided parameters: {templated_sequence}" ] } return {"nomenclature": nomenclature} @@ -141,7 +140,7 @@ def generate_templated_seq_nomenclature( response_model_exclude_none=True, tags=[RouteTag.NOMENCLATURE], ) -def generate_gene_nomenclature(gene_element: Dict = Body()) -> ResponseDict: +def generate_gene_nomenclature(gene_element: dict = Body()) -> ResponseDict: """Build gene element nomenclature. \f :param request: the HTTP request context, supplied by FastAPI. Use to access @@ -177,7 +176,7 @@ def generate_gene_nomenclature(gene_element: Dict = Body()) -> ResponseDict: tags=[RouteTag.NOMENCLATURE], ) def generate_fusion_nomenclature( - request: Request, fusion: Dict = Body() + request: Request, fusion: dict = Body() ) -> ResponseDict: """Generate nomenclature for complete fusion. \f diff --git a/server/src/curfu/routers/utilities.py b/server/src/curfu/routers/utilities.py index 2ce7799c..bdfc650a 100644 --- a/server/src/curfu/routers/utilities.py +++ b/server/src/curfu/routers/utilities.py @@ -1,8 +1,8 @@ """Provide routes for app utility endpoints""" -import os + import tempfile from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import FileResponse @@ -28,7 +28,7 @@ response_model_exclude_none=True, tags=[RouteTag.UTILITIES], ) -def get_mane_transcripts(request: Request, term: str) -> Dict: +def get_mane_transcripts(request: Request, term: str) -> dict: """Get MANE transcripts for gene term. \f :param Request request: the HTTP request context, supplied by FastAPI. Use to access @@ -39,16 +39,15 @@ 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}"]} - elif not normalized.gene_descriptor.gene_id.lower().startswith("hgnc"): + if not normalized.gene_descriptor.gene_id.lower().startswith("hgnc"): return {"warnings": [f"No HGNC symbol: {term}"]} symbol = normalized.gene_descriptor.label - transcripts = request.app.state.fusor.cool_seq_tool.mane_transcript_mappings.get_gene_mane_data( # noqa: E501 + transcripts = request.app.state.fusor.cool_seq_tool.mane_transcript_mappings.get_gene_mane_data( symbol ) if not transcripts: return {"warnings": [f"No matching transcripts: {term}"]} - else: - return {"transcripts": transcripts} + return {"transcripts": transcripts} @router.get( @@ -60,12 +59,12 @@ def get_mane_transcripts(request: Request, term: str) -> Dict: ) async def get_genome_coords( request: Request, - gene: Optional[str] = None, - transcript: Optional[str] = None, - exon_start: Optional[int] = None, - exon_end: Optional[int] = None, - exon_start_offset: Optional[int] = None, - exon_end_offset: Optional[int] = None, + gene: str | None = None, + transcript: str | None = None, + exon_start: int | None = None, + exon_end: int | None = None, + exon_start_offset: int | None = None, + exon_end_offset: int | None = None, ) -> CoordsUtilsResponse: """Convert provided exon positions to genomic coordinates \f @@ -92,10 +91,10 @@ async def get_genome_coords( ) warnings.append(warning) if (exon_start is None) and (exon_start_offset is not None): - warning = "No start param: exon_start_offset parameter requires explicit exon_start parameter" # noqa: E501 + warning = "No start param: exon_start_offset parameter requires explicit exon_start parameter" warnings.append(warning) if (exon_end is None) and (exon_end_offset is not None): - warning = "No end param: exon_end_offset parameter requires explicit exon_end parameter" # noqa: E501 + warning = "No end param: exon_end_offset parameter requires explicit exon_end parameter" warnings.append(warning) if warnings: for warning in warnings: @@ -108,14 +107,16 @@ 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( # noqa: E501 - 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.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", + ) ) warnings = response.warnings if warnings: @@ -134,11 +135,11 @@ async def get_genome_coords( async def get_exon_coords( request: Request, chromosome: str, - start: Optional[int] = None, - end: Optional[int] = None, - strand: Optional[str] = None, - gene: Optional[str] = None, - transcript: Optional[str] = None, + start: int | None = None, + end: int | None = None, + strand: str | None = None, + gene: str | None = None, + transcript: str | None = None, ) -> CoordsUtilsResponse: """Convert provided genomic coordinates to exon coordinates \f @@ -152,7 +153,7 @@ async def get_exon_coords( :param Optional[str] transcript: transcript accession ID :return: response with exon coordinates if successful, or warnings if failed """ - warnings: List[str] = [] + warnings: list[str] = [] if start is None and end is None: warnings.append("Must provide start and/or end coordinates") if transcript is None and gene is None: @@ -169,11 +170,11 @@ 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( # noqa: E501 + response = await request.app.state.fusor.cool_seq_tool.genomic_to_transcript_exon_coordinates( chromosome, start=start, end=end, - strand=strand_validated, # type: ignore + strand=strand_validated, transcript=transcript, gene=gene, ) @@ -199,7 +200,7 @@ async def get_sequence_id(request: Request, sequence: str) -> SequenceIDResponse :param str 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, "ga4gh_id": None, "aliases": []} sr = request.app.state.fusor.cool_seq_tool.seqrepo_access sr_ids, errors = sr.translate_identifier(sequence) @@ -260,24 +261,23 @@ async def get_sequence( _, path = tempfile.mkstemp(suffix=".fasta") try: request.app.state.fusor.cool_seq_tool.get_fasta_file(sequence_id, Path(path)) - except KeyError: - resp = request.app.state.fusor.cool_seq_tool.seqrepo_access.translate_identifier( # noqa: E501 - sequence_id, "refseq" + except KeyError as ke: + resp = ( + request.app.state.fusor.cool_seq_tool.seqrepo_access.translate_identifier( + sequence_id, "refseq" + ) ) if len(resp[0]) < 1: raise HTTPException( status_code=404, detail="No sequence available for requested identifier" - ) - else: - try: - new_seq_id = resp[0][0].split(":")[1] - request.app.state.fusor.cool_seq_tool.get_fasta_file( - new_seq_id, Path(path) - ) - except KeyError: - raise HTTPException( - status_code=404, - detail="No sequence available for requested identifier", - ) - background_tasks.add_task(lambda p: os.unlink(p), path) + ) from ke + try: + new_seq_id = resp[0][0].split(":")[1] + request.app.state.fusor.cool_seq_tool.get_fasta_file(new_seq_id, Path(path)) + except KeyError as e: + raise HTTPException( + status_code=404, + detail="No sequence available for requested identifier", + ) from e + background_tasks.add_task(lambda p: Path(p).unlink(), path) return FileResponse(path, filename=f"{sequence_id}.FASTA") diff --git a/server/src/curfu/routers/validate.py b/server/src/curfu/routers/validate.py index a3fc0371..5087b810 100644 --- a/server/src/curfu/routers/validate.py +++ b/server/src/curfu/routers/validate.py @@ -1,5 +1,4 @@ """Provide validation endpoint to confirm correctness of fusion object structure.""" -from typing import Dict from fastapi import APIRouter, Body, Request from fusor.exceptions import FUSORParametersException @@ -16,7 +15,7 @@ response_model_exclude_none=True, tags=[RouteTag.VALIDATORS], ) -def validate_fusion(request: Request, fusion: Dict = Body()) -> ResponseDict: +def validate_fusion(request: Request, fusion: dict = Body()) -> ResponseDict: """Validate proposed Fusion object. Return warnings if invalid. \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR. diff --git a/server/src/curfu/schemas.py b/server/src/curfu/schemas.py index 9cd71c61..73cf5996 100644 --- a/server/src/curfu/schemas.py +++ b/server/src/curfu/schemas.py @@ -1,6 +1,7 @@ """Provide schemas for FastAPI responses.""" + from enum import Enum -from typing import Dict, List, Literal, Optional, Tuple, Union +from typing import Literal from cool_seq_tool.schemas import GenomicData from fusor.models import ( @@ -20,15 +21,13 @@ from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE from pydantic import BaseModel, Extra, Field, StrictInt, StrictStr, validator -ResponseWarnings = Optional[List[StrictStr]] +ResponseWarnings = list[StrictStr] | None -ResponseDict = Dict[ +ResponseDict = dict[ str, - Union[ - str, int, CURIE, List[str], List[Tuple[str, str, str, str]], FunctionalDomain - ], + str | int | CURIE | list[str] | list[tuple[str, str, str, str]] | FunctionalDomain, ] -Warnings = List[str] +Warnings = list[str] class ClientStructuralElement(BaseModel): @@ -41,56 +40,48 @@ class ClientStructuralElement(BaseModel): class ClientTranscriptSegmentElement(TranscriptSegmentElement, ClientStructuralElement): """TranscriptSegment element class used client-side.""" - input_type: Union[ - Literal["genomic_coords_gene"], - Literal["genomic_coords_tx"], - Literal["exon_coords_tx"], - ] - input_tx: Optional[str] - input_strand: Optional[Strand] - input_gene: Optional[str] - input_chr: Optional[str] - input_genomic_start: Optional[str] - input_genomic_end: Optional[str] - input_exon_start: Optional[str] - input_exon_start_offset: Optional[str] - input_exon_end: Optional[str] - input_exon_end_offset: Optional[str] + input_type: ( + 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 class ClientLinkerElement(LinkerElement, ClientStructuralElement): """Linker element class used client-side.""" - pass - class ClientTemplatedSequenceElement(TemplatedSequenceElement, ClientStructuralElement): """Templated sequence element used client-side.""" - input_chromosome: Optional[str] - input_start: Optional[str] - input_end: Optional[str] + input_chromosome: str | None + input_start: str | None + input_end: str | None class ClientGeneElement(GeneElement, ClientStructuralElement): """Gene element used client-side.""" - pass - class ClientUnknownGeneElement(UnknownGeneElement, ClientStructuralElement): """Unknown gene element used client-side.""" - pass - class ClientMultiplePossibleGenesElement( MultiplePossibleGenesElement, ClientStructuralElement ): """Multiple possible gene element used client-side.""" - pass - class ClientFunctionalDomain(FunctionalDomain): """Define functional domain object used client-side.""" @@ -124,28 +115,28 @@ class Config: class GeneElementResponse(Response): """Response model for gene element construction endoint.""" - element: Optional[GeneElement] + element: GeneElement | None class TxSegmentElementResponse(Response): """Response model for transcript segment element construction endpoint.""" - element: Optional[TranscriptSegmentElement] + element: TranscriptSegmentElement | None class TemplatedSequenceElementResponse(Response): """Response model for transcript segment element construction endpoint.""" - element: Optional[TemplatedSequenceElement] + element: TemplatedSequenceElement | None class NormalizeGeneResponse(Response): """Response model for gene normalization endpoint.""" term: StrictStr - concept_id: Optional[CURIE] - symbol: Optional[StrictStr] - cased: Optional[StrictStr] + concept_id: CURIE | None + symbol: StrictStr | None + cased: StrictStr | None class SuggestGeneResponse(Response): @@ -154,10 +145,10 @@ class SuggestGeneResponse(Response): term: StrictStr matches_count: int # complete term, normalized symbol, normalized concept ID, chromosome ID, strand - concept_id: Optional[List[Tuple[str, str, str, str, str]]] - symbol: Optional[List[Tuple[str, str, str, str, str]]] - prev_symbols: Optional[List[Tuple[str, str, str, str, str]]] - aliases: Optional[List[Tuple[str, str, str, str, str]]] + concept_id: list[tuple[str, str, str, str, str]] | None + symbol: list[tuple[str, str, str, str, str]] | None + prev_symbols: list[tuple[str, str, str, str, str]] | None + aliases: list[tuple[str, str, str, str, str]] | None class DomainParams(BaseModel): @@ -173,31 +164,31 @@ class DomainParams(BaseModel): class GetDomainResponse(Response): """Response model for functional domain constructor endpoint.""" - domain: Optional[FunctionalDomain] + domain: FunctionalDomain | None class AssociatedDomainResponse(Response): """Response model for domain ID autocomplete suggestion endpoint.""" gene_id: StrictStr - suggestions: Optional[List[DomainParams]] + suggestions: list[DomainParams] | None class ValidateFusionResponse(Response): """Response model for Fusion validation endpoint.""" - fusion: Optional[Fusion] + fusion: Fusion | None class ExonCoordsRequest(BaseModel): """Request model for genomic coordinates retrieval""" tx_ac: StrictStr - gene: Optional[StrictStr] = "" - exon_start: Optional[StrictInt] = 0 - exon_start_offset: Optional[StrictInt] = 0 - exon_end: Optional[StrictInt] = 0 - exon_end_offset: Optional[StrictInt] = 0 + 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 @validator("gene") def validate_gene(cls, v) -> str: @@ -217,16 +208,16 @@ def validate_number(cls, v) -> int: class CoordsUtilsResponse(Response): """Response model for genomic coordinates retrieval""" - coordinates_data: Optional[GenomicData] + coordinates_data: GenomicData | None class SequenceIDResponse(Response): """Response model for sequence ID retrieval endpoint.""" sequence: StrictStr - refseq_id: Optional[StrictStr] - ga4gh_id: Optional[StrictStr] - aliases: Optional[List[StrictStr]] + refseq_id: StrictStr | None + ga4gh_id: StrictStr | None + aliases: list[StrictStr] | None class ManeGeneTranscript(BaseModel): @@ -251,7 +242,7 @@ class ManeGeneTranscript(BaseModel): class GetTranscriptsResponse(Response): """Response model for MANE transcript retrieval endpoint.""" - transcripts: Optional[List[ManeGeneTranscript]] + transcripts: list[ManeGeneTranscript] | None class ServiceInfoResponse(Response): @@ -272,17 +263,15 @@ class ClientCategoricalFusion(CategoricalFusion): global FusionContext. """ - regulatory_element: Optional[ClientRegulatoryElement] = None - structural_elements: List[ - Union[ - ClientTranscriptSegmentElement, - ClientGeneElement, - ClientTemplatedSequenceElement, - ClientLinkerElement, - ClientMultiplePossibleGenesElement, - ] + regulatory_element: ClientRegulatoryElement | None = None + structural_elements: list[ + ClientTranscriptSegmentElement + | ClientGeneElement + | ClientTemplatedSequenceElement + | ClientLinkerElement + | ClientMultiplePossibleGenesElement ] - critical_functional_domains: Optional[List[ClientFunctionalDomain]] + critical_functional_domains: list[ClientFunctionalDomain] | None class ClientAssayedFusion(AssayedFusion): @@ -290,22 +279,20 @@ class ClientAssayedFusion(AssayedFusion): global FusionContext. """ - regulatory_element: Optional[ClientRegulatoryElement] = None - structural_elements: List[ - Union[ - ClientTranscriptSegmentElement, - ClientGeneElement, - ClientTemplatedSequenceElement, - ClientLinkerElement, - ClientUnknownGeneElement, - ] + regulatory_element: ClientRegulatoryElement | None = None + structural_elements: list[ + ClientTranscriptSegmentElement + | ClientGeneElement + | ClientTemplatedSequenceElement + | ClientLinkerElement + | ClientUnknownGeneElement ] class NomenclatureResponse(Response): """Response model for regulatory element nomenclature endpoint.""" - nomenclature: Optional[str] + nomenclature: str | None class RegulatoryElementResponse(Response): @@ -317,7 +304,7 @@ class RegulatoryElementResponse(Response): class DemoResponse(Response): """Response model for demo fusion object retrieval endpoints.""" - fusion: Union[ClientAssayedFusion, ClientCategoricalFusion] + fusion: ClientAssayedFusion | ClientCategoricalFusion class RouteTag(str, Enum): diff --git a/server/src/curfu/sequence_services.py b/server/src/curfu/sequence_services.py index b6535b84..a7a1585a 100644 --- a/server/src/curfu/sequence_services.py +++ b/server/src/curfu/sequence_services.py @@ -1,4 +1,5 @@ """Provide sequence ID generation services.""" + import logging logger = logging.getLogger("curfu") @@ -9,15 +10,15 @@ class InvalidInputError(Exception): """Provide exception for input validation.""" -def get_strand(input: str) -> int: +def get_strand(strand_input: str) -> int: """Validate strand arguments received from client. + :param str input: strand argument from client :return: correctly-formatted strand :raises InvalidInputException: if strand arg is invalid """ - if input == "+": + if strand_input == "+": return 1 - elif input == "-": + if strand_input == "-": return -1 - else: - raise InvalidInputError + raise InvalidInputError diff --git a/server/src/curfu/utils.py b/server/src/curfu/utils.py index ec610b52..44757c7b 100644 --- a/server/src/curfu/utils.py +++ b/server/src/curfu/utils.py @@ -1,7 +1,7 @@ """Miscellaneous helper functions.""" -import os + from pathlib import Path -from typing import List, TypeVar +from typing import TypeVar import boto3 from boto3.exceptions import ResourceLoadException @@ -24,18 +24,16 @@ def get_latest_s3_file(file_prefix: str) -> ObjectSummary: logger.info(f"Attempting S3 lookup for data file pattern {file_prefix}...") s3 = boto3.resource("s3", config=Config(region_name="us-east-2")) if not s3: - raise ResourceLoadException("Unable to initialize boto S3 resource") + msg = "Unable to initialize boto S3 resource" + raise ResourceLoadException(msg) bucket = sorted( - list( - s3.Bucket("vicc-services") - .objects.filter(Prefix=f"curfu/{file_prefix}") - .all() - ), + s3.Bucket("vicc-services").objects.filter(Prefix=f"curfu/{file_prefix}").all(), key=lambda f: f.key, reverse=True, ) if len(bucket) == 0: - raise FileNotFoundError(f"No files matching pattern {file_prefix} in bucket.") + msg = f"No files matching pattern {file_prefix} in bucket." + raise FileNotFoundError(msg) return bucket[0] @@ -45,9 +43,9 @@ def download_s3_file(bucket_object: ObjectSummary) -> Path: :param bucket_object: boto object representation of S3 file :return: Path to downloaded file """ - fname = os.path.basename(bucket_object.key) + fname = Path(bucket_object.key).name save_to = APP_ROOT / "data" / fname - with open(save_to, "wb") as f: + with save_to.open("wb") as f: try: bucket_object.Object().download_fileobj(f) except ClientError as e: @@ -57,7 +55,7 @@ def download_s3_file(bucket_object: ObjectSummary) -> Path: return save_to -def get_latest_data_file(file_prefix: str, local_files: List[Path]) -> Path: +def get_latest_data_file(file_prefix: str, local_files: list[Path]) -> Path: """Get path to latest version of given data file. Download from S3 if not available locally. @@ -67,10 +65,9 @@ def get_latest_data_file(file_prefix: str, local_files: List[Path]) -> Path: """ latest_local_file = sorted(local_files, reverse=True)[0] s3_object = get_latest_s3_file(file_prefix) - if os.path.basename(s3_object.key) > latest_local_file.name: + if Path(s3_object.key).name > latest_local_file.name: return download_s3_file(s3_object) - else: - return latest_local_file + return latest_local_file def get_data_file(filename_prefix: str) -> Path: @@ -87,5 +84,4 @@ def get_data_file(filename_prefix: str) -> Path: files = list(data_dir.glob(file_glob)) if not files: return download_s3_file(get_latest_s3_file(filename_prefix)) - else: - return get_latest_data_file(filename_prefix, files) + return get_latest_data_file(filename_prefix, files) diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 46c481b8..da8a95e8 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -1,11 +1,11 @@ """Provide core fixtures for testing Flask functions.""" + import asyncio -from typing import Callable, Dict +from collections.abc import Callable import pytest -from httpx import AsyncClient - from curfu.main import app, get_domain_services, get_gene_services, start_fusor +from httpx import AsyncClient @pytest.fixture(scope="session") @@ -27,7 +27,7 @@ async def async_client(): await client.aclose() -response_callback_type = Callable[[Dict, Dict], None] +response_callback_type = Callable[[dict, dict], None] @pytest.fixture(scope="session") @@ -36,9 +36,9 @@ async def check_response(async_client): async def check_response( query: str, - expected_response: Dict, + expected_response: dict, data_callback: response_callback_type, - **kwargs + **kwargs, ): """Check that requested URL provides expected response. :param str query: URL endpoint with included query terms diff --git a/server/tests/integration/test_complete.py b/server/tests/integration/test_complete.py index 0f2e6a6d..a4a12eb6 100644 --- a/server/tests/integration/test_complete.py +++ b/server/tests/integration/test_complete.py @@ -1,9 +1,10 @@ """Test lookup endpoints""" + import pytest from httpx import AsyncClient -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_complete_gene(async_client: AsyncClient): """Test /complete/gene endpoint""" response = await async_client.get("/api/complete/gene?term=NTRK") diff --git a/server/tests/integration/test_constructors.py b/server/tests/integration/test_constructors.py index 6b19697f..4e316613 100644 --- a/server/tests/integration/test_constructors.py +++ b/server/tests/integration/test_constructors.py @@ -1,15 +1,14 @@ """Test end-to-end correctness of constructor routes.""" -from typing import Dict import pytest -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_build_gene_element(check_response, alk_gene_element): """Test correct functioning of gene element construction route.""" def check_gene_element_response( - response: Dict, expected_response: Dict, expected_id: str = "unset" + response: dict, expected_response: dict, expected_id: str = "unset" ): assert ("element" in response) == ("element" in expected_response) if ("element" not in response) and ("element" not in expected_response): @@ -46,9 +45,9 @@ def check_gene_element_response( @pytest.fixture(scope="session") def check_tx_element_response(): - """Provide callback function to check correctness of transcript element constructor.""" # noqa: E501 D202 + """Provide callback function to check correctness of transcript element constructor.""" - def check_tx_element_response(response: Dict, expected_response: Dict): + def check_tx_element_response(response: dict, expected_response: dict): assert ("element" in response) == ("element" in expected_response) if ("element" not in response) and ("element" not in expected_response): assert "warnings" in response @@ -82,7 +81,7 @@ def check_tx_element_response(response: Dict, expected_response: Dict): def check_reg_element_response(): """Provide callback function check correctness of regulatory element constructor.""" - def check_re_response(response: Dict, expected_response: Dict): + def check_re_response(response: dict, expected_response: dict): assert ("regulatory_element" in response) == ( "regulatory_element" in expected_response ) @@ -111,7 +110,7 @@ def check_re_response(response: Dict, expected_response: Dict): def check_templated_sequence_response(): """Provide callback function to check templated sequence constructor response""" - def check_temp_seq_response(response: Dict, expected_response: Dict): + def check_temp_seq_response(response: dict, expected_response: dict): assert ("element" in response) == ("element" in expected_response) if ("element" not in response) and ("element" not in expected_response): assert "warnings" in response @@ -159,7 +158,7 @@ def check_temp_seq_response(response: Dict, expected_response: Dict): return check_temp_seq_response -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_build_tx_segment_ect( check_response, check_tx_element_response, ntrk1_tx_element_start ): @@ -167,7 +166,7 @@ async def test_build_tx_segment_ect( coordinates and transcript. """ await check_response( - "/api/construct/structural_element/tx_segment_ect?transcript=NM_002529.3&exon_start=2&exon_start_offset=1", # noqa: E501 + "/api/construct/structural_element/tx_segment_ect?transcript=NM_002529.3&exon_start=2&exon_start_offset=1", {"element": ntrk1_tx_element_start}, check_tx_element_response, ) @@ -181,13 +180,13 @@ 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", # noqa: E501 + "/api/construct/structural_element/tx_segment_ect?transcript=NM_0012529.3&exon_start=3", {"warnings": ["Unable to get exons for NM_0012529.3"]}, check_tx_element_response, ) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_build_segment_gct( check_response, check_tx_element_response, tpm3_tx_t_element ): @@ -195,18 +194,18 @@ 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=-", # noqa: E501 + "/api/construct/structural_element/tx_segment_gct?transcript=NM_152263.4&chromosome=NC_000001.11&start=154171416&end=154171417&strand=-", {"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=-", # noqa: E501 + "/api/construct/structural_element/tx_segment_gct?transcript=refseq%3ANM_152263.4&chromosome=NC_000001.11&start=154171416&end=154171417&strand=-", {"element": tpm3_tx_t_element}, check_tx_element_response, ) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_build_segment_gcg( check_response, check_tx_element_response, tpm3_tx_g_element ): @@ -214,13 +213,13 @@ 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=-", # noqa: E501 + "/api/construct/structural_element/tx_segment_gcg?gene=TPM3&chromosome=NC_000001.11&start=154171416&end=154171417&strand=-", {"element": tpm3_tx_g_element}, check_tx_element_response, ) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_build_reg_element(check_response, check_reg_element_response): """Test correctness of regulatory element constructor endpoint.""" await check_response( @@ -241,13 +240,13 @@ async def test_build_reg_element(check_response, check_reg_element_response): ) -@pytest.mark.asyncio +@pytest.mark.asyncio() 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=-", # noqa: E501 + "/api/construct/structural_element/templated_sequence?start=154171415&end=154171417&sequence_id=NC_000001.11&strand=-", { "element": { "type": "TemplatedSequenceElement", @@ -272,7 +271,7 @@ async def test_build_templated_sequence( ) await check_response( - "/api/construct/structural_element/templated_sequence?start=154171415&end=154171417&sequence_id=refseq%3ANC_000001.11&strand=-", # noqa: E501 + "/api/construct/structural_element/templated_sequence?start=154171415&end=154171417&sequence_id=refseq%3ANC_000001.11&strand=-", { "element": { "type": "TemplatedSequenceElement", diff --git a/server/tests/integration/test_demos.py b/server/tests/integration/test_demos.py index 21166f16..596f4ebf 100644 --- a/server/tests/integration/test_demos.py +++ b/server/tests/integration/test_demos.py @@ -1,9 +1,10 @@ """Test demo endpoints""" + import pytest from httpx import AsyncClient -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_demo(async_client: AsyncClient): """Test /api/demo/ endpoints. Probably not worth it to check individual property values, but the Pydantic models diff --git a/server/tests/integration/test_lookup.py b/server/tests/integration/test_lookup.py index 35c959ab..0ecf52a3 100644 --- a/server/tests/integration/test_lookup.py +++ b/server/tests/integration/test_lookup.py @@ -1,9 +1,10 @@ """Test lookup endpoints""" + import pytest from httpx import AsyncClient -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_normalize_gene(async_client: AsyncClient): """Test /api/lookup/gene endpoint""" response = await async_client.get("/api/lookup/gene?term=NTRK1") diff --git a/server/tests/integration/test_main.py b/server/tests/integration/test_main.py index 0a692e2d..8aacae8d 100644 --- a/server/tests/integration/test_main.py +++ b/server/tests/integration/test_main.py @@ -1,10 +1,11 @@ """Test main service routes.""" + import re import pytest -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_service_info(async_client): """Test /service_info endpoint @@ -15,7 +16,7 @@ async def test_service_info(async_client): 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-]+)*))?$" # noqa: E501 + 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"]) diff --git a/server/tests/integration/test_nomenclature.py b/server/tests/integration/test_nomenclature.py index 782d538d..7577e668 100644 --- a/server/tests/integration/test_nomenclature.py +++ b/server/tests/integration/test_nomenclature.py @@ -1,5 +1,4 @@ """Test /nomenclature/ endpoints.""" -from typing import Dict import pytest from fusor.examples import bcr_abl1 @@ -133,9 +132,9 @@ def templated_sequence_element(): } -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_regulatory_element_nomenclature( - async_client: AsyncClient, regulatory_element: Dict + async_client: AsyncClient, regulatory_element: dict ): """Test correctness of regulatory element nomenclature endpoint.""" response = await async_client.post( @@ -145,13 +144,13 @@ async def test_regulatory_element_nomenclature( assert response.json().get("nomenclature", "") == "reg_p@G1(hgnc:9339)" -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_tx_segment_nomenclature( async_client: AsyncClient, - ntrk1_tx_element_start: Dict, - epcam_5_prime: Dict, - epcam_3_prime: Dict, - epcam_invalid: Dict, + ntrk1_tx_element_start: dict, + epcam_5_prime: dict, + epcam_3_prime: dict, + epcam_invalid: dict, ): """Test correctness of transcript segment nomenclature response.""" response = await async_client.post( @@ -178,13 +177,13 @@ async def test_tx_segment_nomenclature( ) assert response.status_code == 200 assert response.json().get("warnings", []) == [ - "1 validation error for TranscriptSegmentElement\ntranscript\n field required (type=value_error.missing)" # noqa: E501 + "1 validation error for TranscriptSegmentElement\ntranscript\n field required (type=value_error.missing)" ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_gene_element_nomenclature( - async_client: AsyncClient, alk_gene_element: Dict + async_client: AsyncClient, alk_gene_element: dict ): """Test correctness of gene element nomenclature endpoint.""" response = await async_client.post("/api/nomenclature/gene", json=alk_gene_element) @@ -197,13 +196,13 @@ async def test_gene_element_nomenclature( ) 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)" # noqa: E501 + "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)" ] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_templated_sequence_nomenclature( - async_client: AsyncClient, templated_sequence_element: Dict + async_client: AsyncClient, templated_sequence_element: dict ): """Test correctness of templated sequence element endpoint.""" response = await async_client.post( @@ -235,11 +234,11 @@ async def test_templated_sequence_nomenclature( ) assert response.status_code == 200 assert response.json().get("warnings", []) == [ - "1 validation error for TemplatedSequenceElement\nstrand\n field required (type=value_error.missing)" # noqa: E501 + "1 validation error for TemplatedSequenceElement\nstrand\n field required (type=value_error.missing)" ] -@pytest.mark.asyncio +@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()) diff --git a/server/tests/integration/test_utilities.py b/server/tests/integration/test_utilities.py index e0d3d0f6..522dcbe4 100644 --- a/server/tests/integration/test_utilities.py +++ b/server/tests/integration/test_utilities.py @@ -1,16 +1,17 @@ """Test end-to-end correctness of utility routes.""" -from typing import Callable, Dict + +from collections.abc import Callable import pytest -response_callback_type = Callable[[Dict, Dict], None] +response_callback_type = Callable[[dict, dict], None] -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_get_mane_transcript(check_response): """Test MANE transcript retrieval endpoint.""" - def check_mane_response(response: Dict, expected_response: Dict): + def check_mane_response(response: dict, expected_response: dict): assert ("transcripts" in response) == ("transcripts" in expected_response) if not (response.get("transcripts")) and not ( expected_response.get("transcripts") @@ -73,11 +74,11 @@ def check_mane_response(response: Dict, expected_response: Dict): ) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_get_genomic_coords(check_response): """Test coordinates utility endpoint using genomic coords.""" - def check_genomic_coords_response(response: Dict, expected_response: Dict): + def check_genomic_coords_response(response: dict, expected_response: dict): assert ("coordinates_data" in response) == ( "coordinates_data" in expected_response ) @@ -109,21 +110,21 @@ def check_genomic_coords_response(response: Dict, expected_response: Dict): ) await check_response( - "/api/utilities/get_genomic?transcript=NM_002529.3&exon_start=1&exon_end=6&gene=FAKE_GENE", # noqa: E501 + "/api/utilities/get_genomic?transcript=NM_002529.3&exon_start=1&exon_end=6&gene=FAKE_GENE", { "warnings": [ - "Unable to find a result where NM_002529.3 has transcript coordinates 0 and 268 between an exon's start and end coordinates on gene FAKE_GENE" # noqa: E501 + "Unable to find a result where NM_002529.3 has transcript coordinates 0 and 268 between an exon's start and end coordinates on gene FAKE_GENE" ] }, check_genomic_coords_response, ) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_get_exon_coords(check_response): """Test /utilities/get_exon endpoint""" - def check_coords_response(response: Dict, expected_response: Dict): + def check_coords_response(response: dict, expected_response: dict): """Provide to check_response to test specific response params""" assert ("coordinates_data" in response) == ( "coordinates_data" in expected_response @@ -135,7 +136,7 @@ def check_coords_response(response: Dict, expected_response: Dict): assert response["coordinates_data"] == expected_response["coordinates_data"] await check_response( - "/api/utilities/get_exon?chromosome=1&transcript=NM_152263.3&start=154192135&strand=-", # noqa: E501 + "/api/utilities/get_exon?chromosome=1&transcript=NM_152263.3&start=154192135&strand=-", { "coordinates_data": { "gene": "TPM3", @@ -165,18 +166,18 @@ def check_coords_response(response: Dict, expected_response: Dict): "/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" # noqa: E501 + "Unable to find mane data for NC_000001.11 with position 154192130 on gene TPM3" ] }, check_coords_response, ) -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_get_sequence_id(check_response): """Test sequence ID lookup utility endpoint""" - def check_sequence_id_response(response: Dict, expected_response: Dict): + def check_sequence_id_response(response: dict, expected_response: dict): """Provide to check_response to test specific response params""" assert response["sequence"] == expected_response["sequence"] if response.get("ga4gh_id") or expected_response.get("ga4gh_id"): diff --git a/server/tests/integration/test_validate.py b/server/tests/integration/test_validate.py index b15605d0..7b18153c 100644 --- a/server/tests/integration/test_validate.py +++ b/server/tests/integration/test_validate.py @@ -1,5 +1,4 @@ """Test /validate endpoint.""" -from typing import Dict import pytest from httpx import AsyncClient @@ -199,13 +198,14 @@ def wrong_type_fusion(): } -async def check_validated_fusion_response(client, fixture: Dict, case_name: str): +async def check_validated_fusion_response(client, fixture: dict, case_name: str): """Run basic checks on fusion validation response. Todo: ---- * FUSOR should provide a "fusion equality" utility function -- incorporate it here when that's done + """ response = await client.post("/api/validate", json=fixture["input"]) @@ -219,7 +219,7 @@ async def check_validated_fusion_response(client, fixture: Dict, case_name: str) ), f"{case_name}: warnings incorrect" -@pytest.mark.asyncio +@pytest.mark.asyncio() async def test_validate_fusion( async_client: AsyncClient, alk_fusion, From 575baadbb0df01dc3c0258d5081e92ffebe96dba Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Tue, 16 Jul 2024 14:50:39 -0400 Subject: [PATCH 02/18] cicd: allow missing credentials (#291) --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d6bb6133..eb5fd42c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,7 @@ repos: - id: end-of-file-fixer - id: check-merge-conflict - id: detect-aws-credentials + args: [ --allow-missing-credentials ] - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.0 hooks: From e44ae678f5e09e2442592c1c006b8a66009cbae5 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 29 Jul 2024 10:09:58 -0400 Subject: [PATCH 03/18] chore: update ebextensions for downloading latest seqrepo instance (#292) * use 2024-02-20 SeqRepo instance --- .ebextensions/02_data_downloads.config | 52 +++++++------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/.ebextensions/02_data_downloads.config b/.ebextensions/02_data_downloads.config index 25ce6d75..b2b2b883 100644 --- a/.ebextensions/02_data_downloads.config +++ b/.ebextensions/02_data_downloads.config @@ -1,46 +1,22 @@ commands: - 01_install_postgresql_devel: - command: "yum install -y python-devel postgresql-devel" - 02_install_aws_cli: + 01_install_aws_cli: command: "yum install -y awscli" - 03_install_p7zip: - command: "yum install -y p7zip" - 04_export_eb_env_var: + 02_export_eb_env_var: command: "export $(cat /opt/elasticbeanstalk/deployment/env | xargs)" container_commands: - 01_cool_seq_tool_permissions: - test: test -d "/var/app/venv/staging-LQM1lest/lib/python3.11/site-packages/cool_seq_tool" - command: "chmod -R 777 /var/app/venv/staging-LQM1lest/lib/python3.11/site-packages/cool_seq_tool/data" + 01_s3_download: + test: test ! -d "/usr/local/share/seqrepo/2024-02-20" + command: "aws s3 cp s3://${AWS_BUCKET_NAME}/${AWS_SEQREPO_OBJECT} /usr/local/share/seqrepo.tar.gz --region us-east-2" - 02_seqrepo_download: - test: test ! -d "/usr/local/share/seqrepo" - command: "aws s3 cp s3://${AWS_BUCKET_NAME}/${AWS_SEQREPO_OBJECT} /usr/local/share/seqrepo.zip --region us-east-2" + 02_extract_seqrepo: + test: test -f "/usr/local/share/seqrepo.tar.gz" + command: "mkdir -p /usr/local/share/seqrepo/2024-02-20 && tar -xzvf /usr/local/share/seqrepo.tar.gz -C /usr/local/share/seqrepo/2024-02-20" - 03_p7zip_seqrepo: - test: test -f "/usr/local/share/seqrepo.zip" - command: "7za x /usr/local/share/seqrepo.zip -o/usr/local/share -y" + 03_seqrepo_zip_permission: + test: test -f "/usr/local/share/seqrepo.tar.gz" + command: "chmod +wr /usr/local/share/seqrepo.tar.gz" - 04_seqrepo_permission: - test: test -d "/usr/local/share/seqrepo" - command: "chmod -R 777 /usr/local/share/seqrepo" - - 05_macosx_permission: - test: test -d "/usr/local/share/__MACOSX" - command: "chmod -R +wr /usr/local/share/__MACOSX" - - 06_seqrepo_zip_permission: - test: test -f "/usr/local/share/seqrepo.zip" - command: "chmod +wr /usr/local/share/seqrepo.zip" - - 07_remove_macosx: - test: test -d "/usr/local/share/__MACOSX" - command: "rm -R /usr/local/share/__MACOSX" - - 08_remove_seqrepo_zip: - test: test -f "/usr/local/share/seqrepo.zip" - command: "rm /usr/local/share/seqrepo.zip" - - 09_data_permission: - test: test -d "/usr/local/share/seqrepo" - command: "chmod -R +wrx /usr/local/share/seqrepo" + 04_remove_seqrepo_zip: + test: test -f "/usr/local/share/seqrepo.tar.gz" + command: "rm /usr/local/share/seqrepo.tar.gz" From 133aad927bd7f9199b0334a420a74c50c6f945b9 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Sat, 3 Aug 2024 12:34:19 -0400 Subject: [PATCH 04/18] cicd: pin exact ruff version (#297) --- .pre-commit-config.yaml | 4 ++-- server/pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb5fd42c..4091f3d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,9 +8,9 @@ repos: - id: end-of-file-fixer - id: check-merge-conflict - id: detect-aws-credentials - args: [ --allow-missing-credentials ] + args: [--allow-missing-credentials] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + rev: v0.5.0 # ruff version hooks: - id: ruff-format - id: ruff diff --git a/server/pyproject.toml b/server/pyproject.toml index 5c3a601c..84f70f77 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -44,7 +44,7 @@ tests = [ ] dev = [ "psycopg2-binary", - "ruff", + "ruff == 0.5.0", "black", "pre-commit>=3.7.1", "gene-normalizer ~= 0.1.39", From e66d981d2d40bd7f5aa7535d0d10a467389aae03 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 5 Aug 2024 11:18:37 -0400 Subject: [PATCH 05/18] refactor: address config deprecation (#299) --- server/src/curfu/schemas.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/server/src/curfu/schemas.py b/server/src/curfu/schemas.py index 73cf5996..5976d939 100644 --- a/server/src/curfu/schemas.py +++ b/server/src/curfu/schemas.py @@ -19,7 +19,7 @@ UnknownGeneElement, ) from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE -from pydantic import BaseModel, Extra, Field, StrictInt, StrictStr, validator +from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr, validator ResponseWarnings = list[StrictStr] | None @@ -88,10 +88,7 @@ class ClientFunctionalDomain(FunctionalDomain): domain_id: str - class Config: - """Configure class.""" - - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class ClientRegulatoryElement(RegulatoryElement): @@ -106,10 +103,7 @@ class Response(BaseModel): warnings: ResponseWarnings - class Config: - """Configure class""" - - extra = Extra.forbid + model_config = ConfigDict(extra="forbid") class GeneElementResponse(Response): From 4becc20985e184373c9084a099f708e7bc32642c Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 5 Aug 2024 13:35:22 -0400 Subject: [PATCH 06/18] feat: use latest fastapi event management api (#298) --- server/src/curfu/main.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/server/src/curfu/main.py b/server/src/curfu/main.py index f56edef4..0f5df845 100644 --- a/server/src/curfu/main.py +++ b/server/src/curfu/main.py @@ -1,5 +1,8 @@ """Provide FastAPI application and route declarations.""" +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager + from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles @@ -124,15 +127,15 @@ def get_domain_services() -> DomainService: return domain_service -@app.on_event("startup") -async def startup() -> None: - """Get FUSOR reference""" +@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() - - -@app.on_event("shutdown") -async def shutdown() -> None: - """Clean up thread pool.""" + yield await app.state.fusor.cool_seq_tool.uta_db._connection_pool.close() # noqa: SLF001 From 2848394898b4a1e95265321bbc786cfb9108f091 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 5 Aug 2024 14:10:19 -0400 Subject: [PATCH 07/18] fix: update deprecated pytest async features (#300) --- server/tests/conftest.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/server/tests/conftest.py b/server/tests/conftest.py index da8a95e8..0a6275a9 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -1,19 +1,10 @@ """Provide core fixtures for testing Flask functions.""" -import asyncio from collections.abc import Callable import pytest from curfu.main import app, get_domain_services, get_gene_services, start_fusor -from httpx import AsyncClient - - -@pytest.fixture(scope="session") -def event_loop(request): - """Create an instance of the default event loop for each test case.""" - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() +from httpx import ASGITransport, AsyncClient @pytest.fixture(scope="session") @@ -22,7 +13,7 @@ async def async_client(): app.state.fusor = await start_fusor() app.state.genes = get_gene_services() app.state.domains = get_domain_services() - client = AsyncClient(app=app, base_url="http://test") + client = AsyncClient(transport=ASGITransport(app=app), base_url="http://test") yield client await client.aclose() From 195b86080274e9d8c04cfb79ee6698b6141d73b5 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 5 Aug 2024 14:33:56 -0400 Subject: [PATCH 08/18] docs: remove docstring types and clean up formatting (#301) --- server/src/curfu/devtools/__init__.py | 6 +-- server/src/curfu/devtools/build_interpro.py | 24 +++++------ server/src/curfu/gene_services.py | 12 +++--- server/src/curfu/routers/__init__.py | 1 + server/src/curfu/routers/complete.py | 16 ++++---- server/src/curfu/routers/demo.py | 22 +++++------ server/src/curfu/routers/lookup.py | 6 +-- server/src/curfu/routers/utilities.py | 44 ++++++++++----------- server/src/curfu/sequence_services.py | 4 +- 9 files changed, 68 insertions(+), 67 deletions(-) diff --git a/server/src/curfu/devtools/__init__.py b/server/src/curfu/devtools/__init__.py index 7863d01a..b504f076 100644 --- a/server/src/curfu/devtools/__init__.py +++ b/server/src/curfu/devtools/__init__.py @@ -8,9 +8,9 @@ def ftp_download(domain: str, path: str, fname: str, callback: Callable) -> None: """Acquire file via FTP. - :param str domain: domain name for remote file host - :param str path: path within host to desired file - :param str fname: name of desired file as provided on host + :param domain: domain name for remote file host + :param path: path within host to desired file + :param fname: name of desired file as provided on host """ try: with ftplib.FTP(domain) as ftp: diff --git a/server/src/curfu/devtools/build_interpro.py b/server/src/curfu/devtools/build_interpro.py index 17fddb8d..4c4c3366 100644 --- a/server/src/curfu/devtools/build_interpro.py +++ b/server/src/curfu/devtools/build_interpro.py @@ -27,7 +27,7 @@ def download_protein2ipr(output_dir: Path) -> None: """Download, unpack, and store Uniprot-InterPro translation table - :param Path output_dir: location to save file within + :param output_dir: location to save file within """ logger.info("Retrieving Uniprot mapping data from InterPro") @@ -109,7 +109,7 @@ def get_uniprot_refs() -> UniprotRefs: def download_uniprot_sprot(output_dir: Path) -> Path: """Retrieve UniProtKB data. - :param Path output_dir: directory to save UniProtKB data in. + :param output_dir: directory to save UniProtKB data in. """ logger.info("Retrieving UniProtKB data.") @@ -144,10 +144,10 @@ def get_interpro_uniprot_rels( """Process InterPro to UniProtKB relations, using UniProt references to connect genes with domains - :param Optional[path] protein_ipr_path: path to protein2ipr_YYYYMMDD.dat if given - :param Path output_dir: Path to save output data in - :param Set[str] domain_ids: InterPro domain IDs to use - :param Dict uniprot_refs: UniProt references from gene normalizer DB + :param protein_ipr_path: path to protein2ipr_YYYYMMDD.dat if given + :param output_dir: Path to save output data in + :param domain_ids: InterPro domain IDs to use + :param uniprot_refs: UniProt references from gene normalizer DB :return: Dict mapping Uniprot accession ID to collected domain data, """ if not protein_ipr_path: @@ -191,9 +191,9 @@ def get_protein_accessions( ) -> dict[tuple[str, str], str]: """Scan uniprot_sprot.xml and extract RefSeq protein accession identifiers for relevant Uniprot accessions. - :param Set[str] relevant_proteins: captured Uniprot accessions, for proteins coded + :param relevant_proteins: captured Uniprot accessions, for proteins coded by human genes and containing InterPro functional domains - :param Optional[Path] uniprot_sprot_path: path to local uniprot_sprot.xml file. + :param uniprot_sprot_path: path to local uniprot_sprot.xml file. :return: Dict where keys are tuple containing Uniprot accession ID and NCBI gene ID, and values are known RefSeq protein accessions """ @@ -279,11 +279,11 @@ def build_gene_domain_maps( """Produce the gene-to-domain lookup table at out_path using the Interpro-Uniprot translation table, the Interpro names table, and the VICC Gene Normalizer. - :param Set[str] interpro_types: types of interpro fields to check references for - :param Path protein_ipr_path: path to protein2ipr_{date}.dat if available - :param Optional[Path] uniprot_refs_path: path to existing uniprot_refs_.tsv + :param interpro_types: types of interpro fields to check references for + :param protein_ipr_path: path to protein2ipr_{date}.dat if available + :param uniprot_refs_path: path to existing uniprot_refs_.tsv file if available. - :param Path output_dir: location to save output file within. Defaults to app data + :param output_dir: location to save output file within. Defaults to app data directory. """ start_time = timer() diff --git a/server/src/curfu/gene_services.py b/server/src/curfu/gene_services.py index 5147a5c9..e8998310 100644 --- a/server/src/curfu/gene_services.py +++ b/server/src/curfu/gene_services.py @@ -52,10 +52,10 @@ def get_normalized_gene( term: str, normalizer: QueryHandler ) -> tuple[CURIE, str, str | CURIE | None]: """Get normalized ID given gene symbol/label/alias. - :param str term: user-entered gene term - :param QueryHandler normalizer: gene normalizer instance - :returns: concept ID, str, if successful - :raises ServiceWarning: if lookup fails + :param term: user-entered gene term + :param normalizer: gene normalizer instance + :return: concept ID, str, if successful + :raise ServiceWarning: if lookup fails """ response = normalizer.normalize(term) if response.match_type != MatchType.NO_MATCH: @@ -131,8 +131,8 @@ def _get_completion_results(term: str, lookup: dict) -> list[Suggestion]: def suggest_genes(self, query: str) -> dict[str, list[Suggestion]]: """Provide autocomplete suggestions based on submitted term. - :param str query: text entered by user - :returns: dict returning list containing any number of suggestion tuples, where + :param query: text entered by user + :return: dict returning list containing any number of suggestion tuples, where each is the correctly-cased term, normalized ID, normalized label, for each item type """ diff --git a/server/src/curfu/routers/__init__.py b/server/src/curfu/routers/__init__.py index 3f3fafcf..3ee0b306 100644 --- a/server/src/curfu/routers/__init__.py +++ b/server/src/curfu/routers/__init__.py @@ -3,6 +3,7 @@ def parse_identifier(identifier: str) -> str: """Restructure ID value to mesh with serverside requirements + :param transcript: user-submitted accession identifier :return: reformatted to conform to UTA requirements """ diff --git a/server/src/curfu/routers/complete.py b/server/src/curfu/routers/complete.py index ff5c9c33..67b1fdbd 100644 --- a/server/src/curfu/routers/complete.py +++ b/server/src/curfu/routers/complete.py @@ -25,11 +25,11 @@ def suggest_gene(request: Request, term: str = Query("")) -> ResponseDict: """Provide completion suggestions for term provided by user. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. - :param str term: entered gene term - :return: JSON response with suggestions listed, or warnings if unable to - provide suggestions. + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. + :param term: entered gene term + :return: JSON response with suggestions listed, or warnings if unable to provide + suggestions. """ response: ResponseDict = {"term": term} possible_matches = request.app.state.genes.suggest_genes(term) @@ -64,9 +64,9 @@ def suggest_gene(request: Request, term: str = Query("")) -> ResponseDict: def suggest_domain(request: Request, gene_id: str = Query("")) -> ResponseDict: """Provide possible domains associated with a given gene to be selected by a user. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. - :param str gene_id: normalized gene concept ID + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. + :param gene_id: normalized gene concept ID :return: JSON response with a list of possible domain name and ID options, or warning(s) if relevant """ diff --git a/server/src/curfu/routers/demo.py b/server/src/curfu/routers/demo.py index 2434479a..bd395c84 100644 --- a/server/src/curfu/routers/demo.py +++ b/server/src/curfu/routers/demo.py @@ -164,7 +164,7 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: def get_alk(request: Request) -> DemoResponse: """Retrieve ALK assayed fusion. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. """ return DemoResponse( @@ -182,8 +182,8 @@ def get_alk(request: Request) -> DemoResponse: def get_ewsr1(request: Request) -> DemoResponse: """Retrieve EWSR1 assayed fusion. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. """ return DemoResponse( fusion=clientify_fusion(examples.ewsr1, request.app.state.fusor), warnings=[] @@ -200,8 +200,8 @@ def get_ewsr1(request: Request) -> DemoResponse: def get_bcr_abl1(request: Request) -> DemoResponse: """Retrieve BCR-ABL1 categorical fusion. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. """ return DemoResponse( fusion=clientify_fusion(examples.bcr_abl1, request.app.state.fusor), warnings=[] @@ -218,8 +218,8 @@ def get_bcr_abl1(request: Request) -> DemoResponse: def get_tpm3_ntrk1(request: Request) -> DemoResponse: """Retrieve TPM3-NTRK1 assayed fusion. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. """ return DemoResponse( fusion=clientify_fusion(examples.tpm3_ntrk1, request.app.state.fusor), @@ -237,8 +237,8 @@ def get_tpm3_ntrk1(request: Request) -> DemoResponse: def get_tpm3_pdgfrb(request: Request) -> DemoResponse: """Retrieve TPM3-PDGFRB assayed fusion. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. """ return DemoResponse( fusion=clientify_fusion(examples.tpm3_pdgfrb, request.app.state.fusor), @@ -255,8 +255,8 @@ def get_tpm3_pdgfrb(request: Request) -> DemoResponse: ) def get_igh_myc(request: Request) -> DemoResponse: """Retrieve IGH-MYC assayed fusion. - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. """ return DemoResponse( fusion=clientify_fusion(examples.igh_myc, request.app.state.fusor), warnings=[] diff --git a/server/src/curfu/routers/lookup.py b/server/src/curfu/routers/lookup.py index ea851ef6..6ebaf038 100644 --- a/server/src/curfu/routers/lookup.py +++ b/server/src/curfu/routers/lookup.py @@ -18,9 +18,9 @@ def normalize_gene(request: Request, term: str = Query("")) -> ResponseDict: """Normalize gene term provided by user. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. - :param str term: gene symbol/alias/name/etc + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. + :param term: gene symbol/alias/name/etc :return: JSON response with normalized ID if successful and warnings otherwise """ response: ResponseDict = {"term": term} diff --git a/server/src/curfu/routers/utilities.py b/server/src/curfu/routers/utilities.py index bdfc650a..7f5d88da 100644 --- a/server/src/curfu/routers/utilities.py +++ b/server/src/curfu/routers/utilities.py @@ -31,9 +31,9 @@ def get_mane_transcripts(request: Request, term: str) -> dict: """Get MANE transcripts for gene term. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. - :param str term: gene term provided by user + :param term: gene term provided by user :return: Dict containing transcripts if lookup succeeds, or warnings upon failure """ normalized = request.app.state.fusor.gene_normalizer.normalize(term) @@ -68,14 +68,14 @@ async def get_genome_coords( ) -> CoordsUtilsResponse: """Convert provided exon positions to genomic coordinates \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. - :param Optional[str] gene: gene symbol/ID on which exons lie - :param Optional[str] transcript: transcript accession ID - :param Optional[int] exon_start: starting exon number - :param Optional[int] exon_end: ending exon number - :param int exon_start_offset: base offset count from starting exon - :param int exon_end_offset: base offset count from end exon + :param gene: gene symbol/ID on which exons lie + :param transcript: transcript accession ID + :param exon_start: starting exon number + :param exon_end: ending exon number + :param exon_start_offset: base offset count from starting exon + :param exon_end_offset: base offset count from end exon :return: CoordsUtilsResponse containing relevant data or warnings if unsuccesful """ warnings = [] @@ -143,14 +143,14 @@ async def get_exon_coords( ) -> CoordsUtilsResponse: """Convert provided genomic coordinates to exon coordinates \f - :param Request request: the HTTP request context, supplied by FastAPI. Use to access - FUSOR and UTA-associated tools. - :param str chromosome: chromosome, either as a number/X/Y or as an accession - :param Optional[int] start: genomic start position - :param Optional[int] end: genomic end position - :param Optional[str] strand: strand of genomic position - :param Optional[str] gene: gene symbol or ID - :param Optional[str] transcript: transcript accession ID + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. + :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 """ warnings: list[str] = [] @@ -195,9 +195,9 @@ async def get_exon_coords( async def get_sequence_id(request: Request, sequence: str) -> SequenceIDResponse: """Get GA4GH sequence ID and aliases given sequence sequence ID \f - :param Request request: the HTTP request context, supplied by FastAPI. Use - to access FUSOR and UTA-associated tools. - :param str sequence_id: user-provided sequence identifier to translate + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. + :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": []} @@ -251,8 +251,8 @@ async def get_sequence( ) -> FileResponse: """Get sequence for requested sequence ID. \f - :param Request request: the HTTP request context, supplied by FastAPI. Use - to access FUSOR and UTA-associated tools. + :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR + and UTA-associated tools. :param background_tasks: Starlette background tasks object. Use to clean up tempfile after get method returns. :param sequence_id: accession ID, sans namespace, eg `NM_152263.3` diff --git a/server/src/curfu/sequence_services.py b/server/src/curfu/sequence_services.py index a7a1585a..376a7a21 100644 --- a/server/src/curfu/sequence_services.py +++ b/server/src/curfu/sequence_services.py @@ -13,9 +13,9 @@ class InvalidInputError(Exception): def get_strand(strand_input: str) -> int: """Validate strand arguments received from client. - :param str input: strand argument from client + :param input: strand argument from client :return: correctly-formatted strand - :raises InvalidInputException: if strand arg is invalid + :raise InvalidInputException: if strand arg is invalid """ if strand_input == "+": return 1 From 381bd228382c94cd0f4af6a825206c0510075aae Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Wed, 7 Aug 2024 15:47:28 -0400 Subject: [PATCH 09/18] 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 265c80fde2d7907c8f380e1d612c052fa7ad3fa8 Mon Sep 17 00:00:00 2001 From: Katie Stahl Date: Mon, 12 Aug 2024 10:37:45 -0400 Subject: [PATCH 10/18] 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'" ], } From ba2699b835eb56c06c6a9f263a66a3d6f5aab4a6 Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Mon, 12 Aug 2024 12:09:15 -0400 Subject: [PATCH 11/18] cicd(fix): fix curfu path (#312) --- .ebextensions/01_build.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ebextensions/01_build.config b/.ebextensions/01_build.config index 382d6412..2bbcbb03 100644 --- a/.ebextensions/01_build.config +++ b/.ebextensions/01_build.config @@ -23,5 +23,5 @@ container_commands: command: "yarn --cwd client build" 03_mv_client: - test: test ! -d "server/curfu/build" - command: "cp -R client/build server/curfu/build" + test: test ! -d "server/src/curfu/build" + command: "cp -R client/build server/src/curfu/build" From 527dc5c5826c63efb39e8a55b4d58ec6e3d1187a Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 12 Aug 2024 12:18:28 -0400 Subject: [PATCH 12/18] refactor: remove unused seqrepo configuration (#308) --- README.md | 2 +- server/src/curfu/__init__.py | 3 --- server/src/curfu/devtools/build_gene_suggest.py | 5 +++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5609a50f..b4223d2e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ cd fusion-curation Ensure that the following data sources are available: - the [VICC Gene Normalization](https://github.com/cancervariants/gene-normalization) database, accessible from a DynamoDB-compliant service. Set the endpoint address with environment variable `GENE_NORM_DB_URL`; default value is `http://localhost:8000`. -- the [Biocommons SeqRepo](https://github.com/biocommons/biocommons.seqrepo) database. Provide local path with environment variable `SEQREPO_DATA_PATH`; default value is `/usr/local/share/seqrepo/latest`. +- the [Biocommons SeqRepo](https://github.com/biocommons/biocommons.seqrepo) database, used by `Cool-Seq-Tool`. The precise file location is configurable via the `SEQREPO_ROOT_DIR` variable, per the [documentation](https://coolseqtool.readthedocs.io/0.6.0/usage.html#environment-configuration). - the [Biocommons Universal Transcript Archive](https://github.com/biocommons/uta), by way of Genomic Med Lab's [Cool Seq Tool](https://github.com/GenomicMedLab/cool-seq-tool) package. Connection parameters to the Postgres database are set most easily as a [Libpq-compliant URL](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) under the environment variable `UTA_DB_URL`. Create a virtual environment for the server and install. Note: there's also a Pipfile so you can skip the virtualenv steps if you'd rather use a Pipenv instance instead of virtualenv/venv. I have been sticking with the latter because [Pipenv doesn't play well with entry points in development](https://stackoverflow.com/a/69225249), but if you aren't editing them in `setup.cfg`, then the former should be fine. diff --git a/server/src/curfu/__init__.py b/server/src/curfu/__init__.py index bb833ca9..7a993d75 100644 --- a/server/src/curfu/__init__.py +++ b/server/src/curfu/__init__.py @@ -50,9 +50,6 @@ else: UTA_DB_URL = "postgresql://uta_admin@localhost:5433/uta/uta_20210129" -# get local seqrepo location -SEQREPO_DATA_PATH = environ.get("SEQREPO_DATA_PATH", f"{APP_ROOT}/data/seqrepo/latest") - class LookupServiceError(Exception): """Custom Exception to use when lookups fail in curation services.""" diff --git a/server/src/curfu/devtools/build_gene_suggest.py b/server/src/curfu/devtools/build_gene_suggest.py index f4a1f449..54e8e581 100644 --- a/server/src/curfu/devtools/build_gene_suggest.py +++ b/server/src/curfu/devtools/build_gene_suggest.py @@ -7,10 +7,11 @@ import click from biocommons.seqrepo.seqrepo import SeqRepo +from cool_seq_tool.handlers.seqrepo_access import SEQREPO_ROOT_DIR from gene.database import create_db from gene.schemas import RecordType -from curfu import APP_ROOT, SEQREPO_DATA_PATH, logger +from curfu import APP_ROOT, logger class GeneSuggestionBuilder: @@ -22,7 +23,7 @@ class GeneSuggestionBuilder: def __init__(self) -> None: """Initialize class.""" self.gene_db = create_db() - self.sr = SeqRepo(SEQREPO_DATA_PATH) + self.sr = SeqRepo(SEQREPO_ROOT_DIR) self.genes = [] def _get_chromosome(self, record: dict) -> str | None: From 1a25af311c90b8f95ecb8c20e14923bf9392338e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 09:09:21 -0400 Subject: [PATCH 13/18] build(deps): bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /client (#276) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- client/yarn.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/yarn.lock b/client/yarn.lock index c57002ce..e7a4f06c 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2232,6 +2232,7 @@ "@types/react-is" "^16.7.1 || ^17.0.0" prop-types "^15.8.1" react-is "^18.2.0" + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -11025,9 +11026,9 @@ webidl-conversions@^6.1.0: integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" From 7682da0451f290eecaf0be1485cb44196c334eca Mon Sep 17 00:00:00 2001 From: Katie Stahl Date: Wed, 2 Oct 2024 13:36:01 -0400 Subject: [PATCH 14/18] revert conftest --- server/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/tests/conftest.py b/server/tests/conftest.py index b2254ad6..3f9e95a2 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -9,7 +9,7 @@ from httpx import ASGITransport, AsyncClient -@pytest_asyncio.fixture +@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() @@ -31,7 +31,7 @@ async def async_client(): response_callback_type = Callable[[dict, dict], None] -@pytest_asyncio.fixture +@pytest_asyncio.fixture(scope="session") async def check_response(async_client): """Provide base response check function. Use in individual tests.""" From cdd267b009e30c20eaa224c4173ff689eba49daf Mon Sep 17 00:00:00 2001 From: Katie Stahl Date: Wed, 2 Oct 2024 13:42:17 -0400 Subject: [PATCH 15/18] fix tests from merge --- server/tests/integration/test_constructors.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/server/tests/integration/test_constructors.py b/server/tests/integration/test_constructors.py index c9ff7afa..e26f5a94 100644 --- a/server/tests/integration/test_constructors.py +++ b/server/tests/integration/test_constructors.py @@ -148,7 +148,7 @@ async def test_build_tx_segment_ec( # test handle invalid transcript await check_response( - "/api/construct/structural_element/tx_segment_ect?transcript=NM_0012529.3&exon_start=3", + "/api/construct/structural_element/tx_segment_ec?transcript=NM_0012529.3&exon_start=3", {"warnings": ["No exons found given NM_0012529.3"]}, check_tx_element_response, ) @@ -171,12 +171,7 @@ async def test_build_segment_gc( 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", - {"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", + "/api/construct/structural_element/tx_segment_gc?transcript=NM_152263.4&chromosome=NC_000001.11&start=154171416&end=154171417", {"element": tpm3_tx_t_element}, check_tx_element_response, ) From dabdae28cbb0d6a9d881b262bba2315b82e19b09 Mon Sep 17 00:00:00 2001 From: Katie Stahl Date: Wed, 2 Oct 2024 13:43:53 -0400 Subject: [PATCH 16/18] update requirements --- requirements.txt | 188 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 127 insertions(+), 61 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7ac3ee85..640e0c8b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,90 +1,156 @@ 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 +aiofiles==23.1.0 +annotated-types==0.6.0 +anyio==3.7.1 +appdirs==1.4.4 +appnope==0.1.3 +asttokens==2.2.1 +async-lru==2.0.4 +asyncio-atexit==1.0.1 +asyncpg==0.28.0 +attrs==23.1.0 +backcall==0.2.0 +bcrypt==4.2.0 +beautifulsoup4==4.12.2 +biocommons.seqrepo==0.6.5 bioutils==0.5.8.post1 -black==24.8.0 -boto3==1.35.3 -botocore==1.35.3 +black==23.10.1 +boto3==1.28.11 +botocore==1.31.11 +bs4==0.0.1 +bump-pydantic==0.7.0 canonicaljson==2.0.0 -certifi==2024.7.4 +certifi==2023.7.22 +cffi==1.17.1 cfgv==3.4.0 -charset-normalizer==3.3.2 -click==8.1.7 +charset-normalizer==3.2.0 +click==8.1.6 coloredlogs==15.0.1 -configparser==7.1.0 +configparser==6.0.0 cool_seq_tool==0.7.1 -coverage==7.6.1 +coverage==7.3.2 +cryptography==43.0.1 +cssselect==1.2.0 +Cython==3.0.0 decorator==5.1.1 -distlib==0.3.8 -executing==2.0.1 -fastapi==0.112.1 -filelock==3.15.4 +Deprecated==1.2.14 +distlib==0.3.7 +docutils==0.20.1 +executing==1.2.0 +fake-useragent==1.1.3 +fastapi==0.100.0 +filelock==3.12.4 fusor==0.4.4 ga4gh.vrs==2.0.0a10 +ga4gh.vrsatile.pydantic==0.2.0 gene-normalizer==0.4.0 +googleapis-common-protos==1.65.0 h11==0.14.0 hgvs==1.5.4 httpcore==1.0.5 -httpx==0.27.0 +httpx==0.27.2 humanfriendly==10.0 -identify==2.6.0 -idna==3.7 -importlib_metadata==8.4.0 +identify==2.5.30 +idna==3.4 +importlib-metadata==6.8.0 +inflection==0.5.1 iniconfig==2.0.0 -ipython==8.26.0 -jedi==0.19.1 -Jinja2==3.1.4 +ipython==8.14.0 +jedi==0.18.2 +Jinja2==3.1.2 jmespath==1.0.1 -MarkupSafe==2.1.5 -matplotlib-inline==0.1.7 +jsonschema==3.2.0 +libcst==1.1.0 +lxml==4.9.3 +Markdown==3.4.4 +markdown-it-py==3.0.0 +MarkupSafe==2.1.3 +matplotlib-inline==0.1.6 +mdurl==0.1.2 mypy-extensions==1.0.0 -nodeenv==1.9.1 -packaging==24.1 +nest-asyncio==1.6.0 +nodeenv==1.8.0 +numpy==1.25.1 +opentelemetry-api==1.27.0 +opentelemetry-exporter-otlp-proto-common==1.27.0 +opentelemetry-exporter-otlp-proto-http==1.27.0 +opentelemetry-instrumentation==0.48b0 +opentelemetry-instrumentation-httpx==0.48b0 +opentelemetry-instrumentation-requests==0.48b0 +opentelemetry-instrumentation-threading==0.48b0 +opentelemetry-instrumentation-urllib==0.48b0 +opentelemetry-proto==1.27.0 +opentelemetry-sdk==1.27.0 +opentelemetry-semantic-conventions==0.48b0 +opentelemetry-util-http==0.48b0 +packaging==23.2 +pandas==2.0.3 +paramiko==3.5.0 +parse==1.19.1 Parsley==1.3 -parso==0.8.4 -pathspec==0.12.1 -pexpect==4.9.0 -platformdirs==4.2.2 -pluggy==1.5.0 -polars==1.5.0 +parso==0.8.3 +pathspec==0.11.2 +pexpect==4.8.0 +pickleshare==0.7.5 +platformdirs==3.11.0 +pluggy==1.3.0 +polars==1.3.0 pre-commit==3.8.0 -prompt_toolkit==3.0.47 -psycopg2==2.9.9 +prompt-toolkit==3.0.39 +protobuf==4.25.5 +psutil==5.9.8 +psycopg2==2.9.6 psycopg2-binary==2.9.9 ptyprocess==0.7.0 -pure_eval==0.2.3 +pure-eval==0.2.2 +pycparser==2.22 pydantic==2.4.2 +pydantic-to-typescript==1.0.10 pydantic-to-typescript2==1.0.4 pydantic_core==2.10.1 -Pygments==2.18.0 -pysam==0.22.1 -pytest==8.3.2 -pytest-asyncio==0.24.0 -pytest-cov==5.0.0 -python-dateutil==2.9.0.post0 -PyYAML==6.0.2 -requests==2.32.3 +pyee==8.2.2 +Pygments==2.15.1 +pyliftover==0.4 +PyNaCl==1.5.0 +pyppeteer==1.0.2 +pyquery==2.0.0 +pyrsistent==0.19.3 +pysam==0.21.0 +pysftp==0.2.9 +pytest==7.4.2 +pytest-asyncio==0.21.1 +pytest-cov==4.1.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 +rich==13.6.0 ruff==0.5.0 -s3transfer==0.10.2 +s3transfer==0.6.1 six==1.16.0 -sniffio==1.3.1 -sqlparse==0.5.1 -stack-data==0.6.3 -starlette==0.38.2 +sniffio==1.3.0 +soupsieve==2.4.1 +sqlparse==0.4.4 +stack-data==0.6.2 +starlette==0.27.0 +synapseclient==4.5.1 tabulate==0.9.0 tqdm==4.66.5 -traitlets==5.14.3 -typing_extensions==4.12.2 -urllib3==1.26.19 -uvicorn==0.30.6 -virtualenv==20.26.3 -wags_tails==0.1.4 -wcwidth==0.2.13 +traitlets==5.9.0 +typer==0.9.0 +typing-inspect==0.9.0 +typing_extensions==4.7.1 +tzdata==2023.3 +urllib3==1.26.20 +uta-tools==0.1.3 +uvicorn==0.23.1 +virtualenv==20.24.5 +w3lib==2.1.1 +wags_tails==0.1.3 +wcwidth==0.2.6 +websockets==10.4 +wrapt==1.16.0 yoyo-migrations==8.2.0 -zipp==3.20.0 +zipp==3.16.2 From 4951706a77d1d040d0a03076e61bec3b55e29624 Mon Sep 17 00:00:00 2001 From: Katie Stahl Date: Wed, 2 Oct 2024 13:45:23 -0400 Subject: [PATCH 17/18] remove debug code --- client/src/components/Pages/Summary/Main/Summary.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/components/Pages/Summary/Main/Summary.tsx b/client/src/components/Pages/Summary/Main/Summary.tsx index 265f2aae..976938ea 100644 --- a/client/src/components/Pages/Summary/Main/Summary.tsx +++ b/client/src/components/Pages/Summary/Main/Summary.tsx @@ -156,8 +156,6 @@ export const Summary: React.FC = ({ setVisibleTab }) => { setFormattedFusion(formattedFusion); }, [fusion]); - console.log(formattedFusion); - return ( <> {(!validationErrors || validationErrors.length === 0) && From d824f3d2f7d21ee044d9eda7a4e6a679dcb4ec10 Mon Sep 17 00:00:00 2001 From: Katie Stahl Date: Wed, 2 Oct 2024 13:52:24 -0400 Subject: [PATCH 18/18] use staging requirements.txt --- requirements.txt | 188 +++++++++++++++-------------------------------- 1 file changed, 61 insertions(+), 127 deletions(-) diff --git a/requirements.txt b/requirements.txt index 640e0c8b..7ac3ee85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,156 +1,90 @@ agct==0.1.0.dev2 -aiofiles==23.1.0 -annotated-types==0.6.0 -anyio==3.7.1 -appdirs==1.4.4 -appnope==0.1.3 -asttokens==2.2.1 -async-lru==2.0.4 -asyncio-atexit==1.0.1 -asyncpg==0.28.0 -attrs==23.1.0 -backcall==0.2.0 -bcrypt==4.2.0 -beautifulsoup4==4.12.2 -biocommons.seqrepo==0.6.5 +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 -black==23.10.1 -boto3==1.28.11 -botocore==1.31.11 -bs4==0.0.1 -bump-pydantic==0.7.0 +black==24.8.0 +boto3==1.35.3 +botocore==1.35.3 canonicaljson==2.0.0 -certifi==2023.7.22 -cffi==1.17.1 +certifi==2024.7.4 cfgv==3.4.0 -charset-normalizer==3.2.0 -click==8.1.6 +charset-normalizer==3.3.2 +click==8.1.7 coloredlogs==15.0.1 -configparser==6.0.0 +configparser==7.1.0 cool_seq_tool==0.7.1 -coverage==7.3.2 -cryptography==43.0.1 -cssselect==1.2.0 -Cython==3.0.0 +coverage==7.6.1 decorator==5.1.1 -Deprecated==1.2.14 -distlib==0.3.7 -docutils==0.20.1 -executing==1.2.0 -fake-useragent==1.1.3 -fastapi==0.100.0 -filelock==3.12.4 +distlib==0.3.8 +executing==2.0.1 +fastapi==0.112.1 +filelock==3.15.4 fusor==0.4.4 ga4gh.vrs==2.0.0a10 -ga4gh.vrsatile.pydantic==0.2.0 gene-normalizer==0.4.0 -googleapis-common-protos==1.65.0 h11==0.14.0 hgvs==1.5.4 httpcore==1.0.5 -httpx==0.27.2 +httpx==0.27.0 humanfriendly==10.0 -identify==2.5.30 -idna==3.4 -importlib-metadata==6.8.0 -inflection==0.5.1 +identify==2.6.0 +idna==3.7 +importlib_metadata==8.4.0 iniconfig==2.0.0 -ipython==8.14.0 -jedi==0.18.2 -Jinja2==3.1.2 +ipython==8.26.0 +jedi==0.19.1 +Jinja2==3.1.4 jmespath==1.0.1 -jsonschema==3.2.0 -libcst==1.1.0 -lxml==4.9.3 -Markdown==3.4.4 -markdown-it-py==3.0.0 -MarkupSafe==2.1.3 -matplotlib-inline==0.1.6 -mdurl==0.1.2 +MarkupSafe==2.1.5 +matplotlib-inline==0.1.7 mypy-extensions==1.0.0 -nest-asyncio==1.6.0 -nodeenv==1.8.0 -numpy==1.25.1 -opentelemetry-api==1.27.0 -opentelemetry-exporter-otlp-proto-common==1.27.0 -opentelemetry-exporter-otlp-proto-http==1.27.0 -opentelemetry-instrumentation==0.48b0 -opentelemetry-instrumentation-httpx==0.48b0 -opentelemetry-instrumentation-requests==0.48b0 -opentelemetry-instrumentation-threading==0.48b0 -opentelemetry-instrumentation-urllib==0.48b0 -opentelemetry-proto==1.27.0 -opentelemetry-sdk==1.27.0 -opentelemetry-semantic-conventions==0.48b0 -opentelemetry-util-http==0.48b0 -packaging==23.2 -pandas==2.0.3 -paramiko==3.5.0 -parse==1.19.1 +nodeenv==1.9.1 +packaging==24.1 Parsley==1.3 -parso==0.8.3 -pathspec==0.11.2 -pexpect==4.8.0 -pickleshare==0.7.5 -platformdirs==3.11.0 -pluggy==1.3.0 -polars==1.3.0 +parso==0.8.4 +pathspec==0.12.1 +pexpect==4.9.0 +platformdirs==4.2.2 +pluggy==1.5.0 +polars==1.5.0 pre-commit==3.8.0 -prompt-toolkit==3.0.39 -protobuf==4.25.5 -psutil==5.9.8 -psycopg2==2.9.6 +prompt_toolkit==3.0.47 +psycopg2==2.9.9 psycopg2-binary==2.9.9 ptyprocess==0.7.0 -pure-eval==0.2.2 -pycparser==2.22 +pure_eval==0.2.3 pydantic==2.4.2 -pydantic-to-typescript==1.0.10 pydantic-to-typescript2==1.0.4 pydantic_core==2.10.1 -pyee==8.2.2 -Pygments==2.15.1 -pyliftover==0.4 -PyNaCl==1.5.0 -pyppeteer==1.0.2 -pyquery==2.0.0 -pyrsistent==0.19.3 -pysam==0.21.0 -pysftp==0.2.9 -pytest==7.4.2 -pytest-asyncio==0.21.1 -pytest-cov==4.1.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 -rich==13.6.0 +Pygments==2.18.0 +pysam==0.22.1 +pytest==8.3.2 +pytest-asyncio==0.24.0 +pytest-cov==5.0.0 +python-dateutil==2.9.0.post0 +PyYAML==6.0.2 +requests==2.32.3 ruff==0.5.0 -s3transfer==0.6.1 +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 -synapseclient==4.5.1 +sniffio==1.3.1 +sqlparse==0.5.1 +stack-data==0.6.3 +starlette==0.38.2 tabulate==0.9.0 tqdm==4.66.5 -traitlets==5.9.0 -typer==0.9.0 -typing-inspect==0.9.0 -typing_extensions==4.7.1 -tzdata==2023.3 -urllib3==1.26.20 -uta-tools==0.1.3 -uvicorn==0.23.1 -virtualenv==20.24.5 -w3lib==2.1.1 -wags_tails==0.1.3 -wcwidth==0.2.6 -websockets==10.4 -wrapt==1.16.0 +traitlets==5.14.3 +typing_extensions==4.12.2 +urllib3==1.26.19 +uvicorn==0.30.6 +virtualenv==20.26.3 +wags_tails==0.1.4 +wcwidth==0.2.13 yoyo-migrations==8.2.0 -zipp==3.16.2 +zipp==3.20.0