From 7898492a42c0b8e3a298a88f2d387585ec29504f Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Thu, 24 Aug 2023 15:36:15 -0400 Subject: [PATCH 01/14] fix: display invalid exon errors (#246) --- .../TxSegmentElementInput.tsx | 17 +++++++++++++++++ .../Utilities/GetCoordinates/GetCoordinates.tsx | 3 --- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx index 1fb31dd2..8845f9ee 100644 --- a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx @@ -297,11 +297,28 @@ const TxSegmentCompInput: React.FC = ({ txSegmentResponse.warnings && txSegmentResponse.warnings?.length > 0 ) { + // transcript invalid const txWarning = `Unable to get exons for ${txAc}`; if (txSegmentResponse.warnings.includes(txWarning)) { setTxAcText("Unrecognized value"); } + // exon(s) invalid + if (startingExon !== undefined) { + const startWarning = `Exon ${startingExon} does not exist on ${txAc}`; + if (txSegmentResponse.warnings.includes(startWarning)) { + setStartingExonText("Invalid"); + } + } + if (endingExon !== undefined) { + const endWarning = `Exon ${endingExon} does not exist on ${txAc}`; + if (txSegmentResponse.warnings.includes(endWarning)) { + setEndingExonText("Invalid"); + } + } } else { + setTxAcText(""); + setStartingExonText(""); + setEndingExonText(""); const inputParams = { input_type: txInputType, input_tx: txAc, diff --git a/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx b/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx index f0b36c30..4521a918 100644 --- a/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx +++ b/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx @@ -152,7 +152,6 @@ const GetCoordinates: React.FC = () => { setResults(null); clearWarnings(); coordsResponse.warnings.forEach((warning) => { - console.log(warning); if (warning.startsWith("Found more than one accession")) { setChromosomeText("Complete ID required"); } else if (warning.startsWith("Unable to get exons for")) { @@ -169,8 +168,6 @@ const GetCoordinates: React.FC = () => { const exonPattern = /Exon (\d*) does not exist on (.*)/; const match = exonPattern.exec(warning); if (match) { - console.log(exonStart); - console.log(match[1]); if (exonStart === match[1]) { setExonStartText("Out of range"); } else if (exonEnd === match[1]) { From 998dd8143149fb3c4d63b5d5f19bf7318c356961 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Tue, 29 Aug 2023 12:09:02 -0400 Subject: [PATCH 02/14] build: use latest FUSOR prerelease (#258) --- requirements.txt | 2 +- server/setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ba435c4a..9db28ee1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ decorator==5.1.1 executing==1.2.0 fake-useragent==1.1.3 fastapi==0.100.0 -fusor==0.0.27 +fusor==0.0.28.dev0 ga4gh.vrs==0.8.4 ga4gh.vrsatile.pydantic==0.0.13 gene-normalizer==0.1.39 diff --git a/server/setup.cfg b/server/setup.cfg index 3c635989..98138733 100644 --- a/server/setup.cfg +++ b/server/setup.cfg @@ -16,7 +16,7 @@ install_requires = fastapi >= 0.72.0 aiofiles asyncpg - fusor ~= 0.0.27 + fusor ~= 0.0.28-dev0 sqlparse >= 0.4.2 urllib3 >= 1.26.5 click From 16ceed4d7a1b04441f1f0fd228a38055e3267882 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Tue, 29 Aug 2023 15:58:06 -0400 Subject: [PATCH 03/14] feat: optionally enable tooltips (#248) For now, tooltips are still enabled by default. Could revisit in the future if it seems unnecessary. --- .../Pages/CausativeEvent/CausativeEvent.tsx | 41 +++++------ .../Input/StructuralElementInputAccordion.tsx | 5 +- client/src/components/main/App/App.scss | 2 +- client/src/components/main/App/App.tsx | 29 ++++++-- client/src/components/main/App/AppMenu.tsx | 69 +++++++++++++++---- .../GeneAutocomplete/GeneAutocomplete.tsx | 3 +- .../main/shared/HelpTooltip/HelpTooltip.tsx | 8 ++- .../src/global/contexts/SettingsContext.tsx | 16 +++++ client/src/global/styles/theme.ts | 2 +- 9 files changed, 124 insertions(+), 51 deletions(-) create mode 100644 client/src/global/contexts/SettingsContext.tsx diff --git a/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx b/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx index 1c9eaac4..1e5c3aa1 100644 --- a/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx +++ b/client/src/components/Pages/CausativeEvent/CausativeEvent.tsx @@ -102,32 +102,23 @@ export const CausativeEvent: React.FC = () => { - - The type of event that generated the fusion. - - } + - - {["rearrangement", "trans-splicing", "read-through"].map( - (value, index) => ( - } - label={eventDisplayMap[value]} - key={index} - /> - ) - )} - - + {["rearrangement", "trans-splicing", "read-through"].map( + (value, index) => ( + } + label={eventDisplayMap[value]} + key={index} + /> + ) + )} + diff --git a/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx b/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx index 0753b150..f47cc2ca 100644 --- a/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx +++ b/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx @@ -115,7 +115,10 @@ const StructuralElementInputAccordion: React.FC< > {validated ? ( - + ) : ( diff --git a/client/src/components/main/App/App.scss b/client/src/components/main/App/App.scss index 2068049e..910af101 100644 --- a/client/src/components/main/App/App.scss +++ b/client/src/components/main/App/App.scss @@ -37,7 +37,7 @@ h3 { .MuiDrawer-paper { width: 160px; overflow-x: hidden !important; - background-color: #18252B; + background-color: #18252b; color: white !important; } } diff --git a/client/src/components/main/App/App.tsx b/client/src/components/main/App/App.tsx index c64152ad..483ec9e4 100644 --- a/client/src/components/main/App/App.tsx +++ b/client/src/components/main/App/App.tsx @@ -43,6 +43,11 @@ import LandingPage from "../Landing/LandingPage"; import AppMenu from "./AppMenu"; import DemoDropdown from "./DemoDropdown"; import { HelpPopover } from "../shared/HelpPopover/HelpPopover"; +import { + initialSettings, + SettingsContext, + SettingsType, +} from "../../../global/contexts/SettingsContext"; type ClientFusion = ClientCategoricalFusion | ClientAssayedFusion; @@ -69,6 +74,15 @@ const App = (): JSX.Element => { null ); const [selectedDemo, setSelectedDemo] = React.useState(""); + const [settings, setSettings] = useState(initialSettings); + + const handleSetSettings = (newSettings: SettingsType) => { + sessionStorage.setItem( + "fusion-builder-settings", + JSON.stringify(newSettings) + ); + setSettings(newSettings); + }; // TODO: implement open/closing of AppMenu. This variable will become a state variable const open = true; @@ -161,7 +175,10 @@ const App = (): JSX.Element => { * readability. */ const fusionIsEmpty = () => { - if (fusion?.structural_elements.length === 0 && fusion?.regulatory_element === undefined) { + if ( + fusion?.structural_elements.length === 0 && + fusion?.regulatory_element === undefined + ) { return true; } else if (fusion.structural_elements.length > 0) { return false; @@ -307,8 +324,8 @@ const App = (): JSX.Element => { ); return ( - <> - + +
{ - + {displayTool ? fusionsComponent : } @@ -392,8 +409,8 @@ const App = (): JSX.Element => { - - + + ); }; diff --git a/client/src/components/main/App/AppMenu.tsx b/client/src/components/main/App/AppMenu.tsx index ab729fb5..b1a5a74a 100644 --- a/client/src/components/main/App/AppMenu.tsx +++ b/client/src/components/main/App/AppMenu.tsx @@ -1,6 +1,13 @@ import React, { useEffect, useState } from "react"; import "./App.scss"; -import { Box, Typography, makeStyles, Link, Drawer } from "@material-ui/core"; +import { + Box, + Typography, + makeStyles, + Link, + Drawer, + Switch, +} from "@material-ui/core"; import { ServiceInfoResponse } from "../../../services/ResponseModels"; import { getInfo } from "../../../services/main"; @@ -16,28 +23,44 @@ const useStyles = makeStyles(() => ({ upperSection: { marginLeft: "10px", }, + drawerContainer: { + height: "100%", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + }, lowerSection: { marginBottom: "10px", display: "flex", - justifyContent: "center", + flexDirection: "column", }, - drawerContainer: { - height: "100%", + optionsContainer: { display: "flex", - flexDirection: "column", + flexDirection: "row", justifyContent: "space-between", + alignItems: "center", + margin: "0px 10px 0px 10px", + }, + optionsText: { fontSize: "0.8rem" }, + versionContainer: { + display: "flex", + justifyContent: "center", + paddingTop: "10px", }, versionText: { fontSize: "0.7rem", }, })); -interface AppMenuProps { - open?: boolean; -} - -export default function AppMenu(props: AppMenuProps): React.ReactElement { +export default function AppMenu({ + open, + settings, + setSettings, +}): React.ReactElement { const [serviceInfo, setServiceInfo] = useState({} as ServiceInfoResponse); + const [tooltipsEnabled, setTooltipsEnabled] = useState( + settings.enableToolTips + ); useEffect(() => { getInfo().then((infoResponse) => { @@ -46,10 +69,16 @@ export default function AppMenu(props: AppMenuProps): React.ReactElement { }, []); const classes = useStyles(); + + const handleTooltipsChange = (event) => { + setTooltipsEnabled(event.target.checked); + setSettings({ ...settings, enableToolTips: event.target.checked }); + }; + return ( @@ -127,9 +156,21 @@ export default function AppMenu(props: AppMenuProps): React.ReactElement { - - v{serviceInfo.curfu_version} - + + + Enable tooltips + + + + + + v{serviceInfo.curfu_version} + + diff --git a/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx b/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx index 710dc80a..05269330 100644 --- a/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx +++ b/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx @@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react"; import { TextField, Typography } from "@material-ui/core"; import Autocomplete from "@material-ui/lab/Autocomplete"; import { getGeneId, getGeneSuggestions } from "../../../../services/main"; -import { CSSProperties } from "@material-ui/core/styles/withStyles"; import { NormalizeGeneResponse, SuggestGeneResponse, @@ -207,7 +206,7 @@ export const GeneAutocomplete: React.FC = ({ variant="standard" label={promptText ? promptText : "Gene Symbol"} margin="dense" - style={{minWidth: "250px !important"}} + style={{ minWidth: "250px !important" }} error={geneText !== ""} helperText={geneText ? geneText : null} /> diff --git a/client/src/components/main/shared/HelpTooltip/HelpTooltip.tsx b/client/src/components/main/shared/HelpTooltip/HelpTooltip.tsx index 5397e006..32440638 100644 --- a/client/src/components/main/shared/HelpTooltip/HelpTooltip.tsx +++ b/client/src/components/main/shared/HelpTooltip/HelpTooltip.tsx @@ -1,5 +1,6 @@ import { makeStyles, Tooltip } from "@material-ui/core"; -import React from "react"; +import React, { useContext } from "react"; +import { SettingsContext } from "../../../../global/contexts/SettingsContext"; const useStylesBootstrap = makeStyles(() => ({ tooltip: { @@ -14,6 +15,8 @@ const useStylesBootstrap = makeStyles(() => ({ })); const BootstrapTooltip = ({ title, placement, children }) => { + const settings = useContext(SettingsContext); + const classes = useStylesBootstrap(); return ( @@ -22,6 +25,9 @@ const BootstrapTooltip = ({ title, placement, children }) => { placement={placement ? placement : "right"} title={title} classes={classes} + disableFocusListener={!settings.enableToolTips} + disableHoverListener={!settings.enableToolTips} + disableTouchListener={!settings.enableToolTips} > {children} diff --git a/client/src/global/contexts/SettingsContext.tsx b/client/src/global/contexts/SettingsContext.tsx new file mode 100644 index 00000000..00395312 --- /dev/null +++ b/client/src/global/contexts/SettingsContext.tsx @@ -0,0 +1,16 @@ +import { createContext } from "react"; + +export type SettingsType = { + enableToolTips: boolean; +}; + +export const defaultSettings = { + enableToolTips: true, +}; + +export const initialSettings = + sessionStorage.getItem("fusion-builder-settings") !== null + ? JSON.parse(sessionStorage.getItem("fusion-builder-settings") as string) + : defaultSettings; + +export const SettingsContext = createContext(defaultSettings); diff --git a/client/src/global/styles/theme.ts b/client/src/global/styles/theme.ts index 944c8bbc..4561b371 100644 --- a/client/src/global/styles/theme.ts +++ b/client/src/global/styles/theme.ts @@ -40,7 +40,7 @@ const theme = createTheme({ secondary: { main: COLORTHEMES.light["--secondary"], contrastText: COLORTHEMES.light["--white"], - } + }, }, }); From 95b95a7e15ff1014f02f2dc597117b0f64a527da Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Wed, 30 Aug 2023 09:52:22 -0400 Subject: [PATCH 04/14] style: use Ruff and Black (#259) --- .github/workflows/backend_checks.yml | 31 +++++++++++++ .gitignore | 1 - .pre-commit-config.yaml | 14 +++--- server/.flake8 | 20 --------- server/curfu/__init__.py | 11 ++--- server/curfu/cli.py | 9 ++-- server/curfu/devtools/__init__.py | 3 +- server/curfu/devtools/build_client_types.py | 3 +- server/curfu/devtools/gene.py | 11 +++-- server/curfu/devtools/interpro.py | 18 ++++---- server/curfu/domain_services.py | 10 +++-- server/curfu/gene_services.py | 15 +++---- server/curfu/main.py | 27 ++++++------ server/curfu/routers/complete.py | 13 +++--- server/curfu/routers/constructors.py | 19 ++++---- server/curfu/routers/demo.py | 39 ++++++++--------- server/curfu/routers/lookup.py | 9 ++-- server/curfu/routers/meta.py | 3 +- server/curfu/routers/nomenclature.py | 13 +++--- server/curfu/routers/utilities.py | 17 ++++---- server/curfu/routers/validate.py | 2 +- server/curfu/schemas.py | 19 ++++---- server/curfu/sequence_services.py | 6 +-- server/curfu/utils.py | 15 +++---- server/pyproject.toml | 43 +++++++++++++++++++ server/setup.cfg | 3 +- server/tests/conftest.py | 4 +- server/tests/integration/test_main.py | 8 ++-- server/tests/integration/test_nomenclature.py | 2 +- server/tests/integration/test_utilities.py | 3 +- server/tests/integration/test_validate.py | 3 +- 31 files changed, 217 insertions(+), 177 deletions(-) create mode 100644 .github/workflows/backend_checks.yml delete mode 100644 server/.flake8 create mode 100644 server/pyproject.toml diff --git a/.github/workflows/backend_checks.yml b/.github/workflows/backend_checks.yml new file mode 100644 index 00000000..dec028d0 --- /dev/null +++ b/.github/workflows/backend_checks.yml @@ -0,0 +1,31 @@ +name: backend_checks +on: [push, pull_request] +jobs: + build: + name: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install dependencies + run: pip install server/ + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: black + uses: psf/black@stable + with: + src: "./server" + + - name: ruff + uses: chartboost/ruff-action@v1 + with: + src: "./server" diff --git a/.gitignore b/.gitignore index 1b417e78..9af1cf3d 100644 --- a/.gitignore +++ b/.gitignore @@ -160,7 +160,6 @@ dynamodb_local_latest/* # Build files Pipfile.lock -pyproject.toml # client-side things curation/client/node_modules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba775bbf..c1a48b23 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,17 +2,21 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.4.0 hooks: - - id: flake8 - additional_dependencies: [flake8-docstrings] - args: ["--config=server/.flake8"] - id: check-added-large-files args: ["--maxkb=2000"] - id: detect-private-key + - id: trailing-whitespace + - id: end-of-file-fixer - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 23.7.0 hooks: - id: black - args: [--diff, --check] + language_version: python3.11 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.286 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-eslint rev: "v8.20.0" hooks: diff --git a/server/.flake8 b/server/.flake8 deleted file mode 100644 index 6b13a128..00000000 --- a/server/.flake8 +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -extend-ignore = D205, D400, I101, ANN101, ANN002, ANN003, W503, D301 -max-line-length = 100 -exclude = - .git - venv - __pycache__ - source - outputs - docs/* - analysis/* -inline-quotes = " -import-order-style = pep8 -application-import-names = - curation - tests -per-file-ignores = - tests/*:ANN001, ANN2 - *__init__.py:F401 -mypy-init-return = true diff --git a/server/curfu/__init__.py b/server/curfu/__init__.py index 523744b0..235e9429 100644 --- a/server/curfu/__init__.py +++ b/server/curfu/__init__.py @@ -1,11 +1,10 @@ """Fusion curation interface.""" -from pathlib import Path -from os import environ import logging +from os import environ +from pathlib import Path from .version import __version__ - # provide consistent paths APP_ROOT = Path(__file__).resolve().parents[0] @@ -51,13 +50,9 @@ SEQREPO_DATA_PATH = environ["SEQREPO_DATA_PATH"] -class ServiceWarning(Exception): +class LookupServiceError(Exception): """Custom Exception to use when lookups fail in curation services.""" - def __init__(self, *args, **kwargs): - """Initialize exception.""" - super().__init__(*args, **kwargs) - # define max acceptable matches for autocomplete suggestions MAX_SUGGESTIONS = 50 diff --git a/server/curfu/cli.py b/server/curfu/cli.py index d5c22595..72b19aca 100644 --- a/server/curfu/cli.py +++ b/server/curfu/cli.py @@ -1,15 +1,15 @@ """Provide command-line interface to application and associated utilities.""" import os -from typing import Optional from pathlib import Path +from typing import Optional import click -from curfu import APP_ROOT +from curfu import APP_ROOT from curfu.devtools import DEFAULT_INTERPRO_TYPES from curfu.devtools.build_client_types import build_client_types -from curfu.devtools.interpro import build_gene_domain_maps from curfu.devtools.gene import GeneSuggestionBuilder +from curfu.devtools.interpro import build_gene_domain_maps @click.command() @@ -27,9 +27,8 @@ def serve(port: int) -> None: @click.group() -def devtools(): +def devtools() -> None: """Provide setup utilities for constructing data for Fusion Curation app.""" - pass types_help = """ diff --git a/server/curfu/devtools/__init__.py b/server/curfu/devtools/__init__.py index d6241a8c..c1ba6c1e 100644 --- a/server/curfu/devtools/__init__.py +++ b/server/curfu/devtools/__init__.py @@ -1,10 +1,11 @@ """Utility functions for application setup.""" import ftplib +from typing import Callable from curfu import logger -def ftp_download(domain: str, path: str, fname: str, callback) -> None: +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 diff --git a/server/curfu/devtools/build_client_types.py b/server/curfu/devtools/build_client_types.py index a41abbca..8e2a940d 100644 --- a/server/curfu/devtools/build_client_types.py +++ b/server/curfu/devtools/build_client_types.py @@ -1,9 +1,10 @@ """Provide client type generation tooling.""" from pathlib import Path + from pydantic2ts.cli.script import generate_typescript_defs -def build_client_types(): +def build_client_types() -> None: """Construct type definitions for front-end client.""" client_dir = Path(__file__).resolve().parents[3] / "client" generate_typescript_defs( diff --git a/server/curfu/devtools/gene.py b/server/curfu/devtools/gene.py index 94b1e884..ddd2b07a 100644 --- a/server/curfu/devtools/gene.py +++ b/server/curfu/devtools/gene.py @@ -1,15 +1,14 @@ """Provide tools to build backend data relating to gene identification.""" -from typing import Dict, Tuple -from pathlib import Path from datetime import datetime as dt +from pathlib import Path from timeit import default_timer as timer +from typing import Dict, Tuple -from gene.query import QueryHandler import click +from gene.query import QueryHandler from curfu import APP_ROOT, logger - # type stub Map = Dict[str, Tuple[str, str, str]] @@ -27,7 +26,7 @@ class GeneSuggestionBuilder: alias_map = {} assoc_with_map = {} - def __init__(self): + def __init__(self) -> None: """Initialize class. TODO: think about how best to force prod environment @@ -108,7 +107,7 @@ def build_gene_suggest_maps(self, output_dir: Path = APP_ROOT / "data") -> None: break today = dt.strftime(dt.today(), "%Y%m%d") - for (map, name) in ( + for map, name in ( (self.xrefs_map, "xrefs"), (self.symbol_map, "symbols"), (self.label_map, "labels"), diff --git a/server/curfu/devtools/interpro.py b/server/curfu/devtools/interpro.py index 9f88c79b..d07ad739 100644 --- a/server/curfu/devtools/interpro.py +++ b/server/curfu/devtools/interpro.py @@ -1,16 +1,16 @@ """Provide utilities relating to data fetched from InterPro service.""" -import gzip -from typing import Tuple, Dict, Optional, Set -from pathlib import Path import csv -from datetime import datetime -from timeit import default_timer as timer +import gzip import os import shutil -import xml.etree.ElementTree as ET +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 -from gene.query import QueryHandler import click +from gene.query import QueryHandler from curfu import APP_ROOT, logger from curfu.devtools import ftp_download @@ -34,7 +34,7 @@ def download_protein2ipr(output_dir: Path) -> None: gz_file_path = output_dir / "protein2ipr.dat.gz" with open(gz_file_path, "w") as fp: - def writefile(data): + def writefile(data): # noqa fp.write(data) ftp_download( @@ -283,7 +283,7 @@ def build_gene_domain_maps( # get relevant Interpro IDs interpro_data_bin = [] - def get_interpro_data(data): + def get_interpro_data(data): # noqa interpro_data_bin.append(data) ftp_download( diff --git a/server/curfu/domain_services.py b/server/curfu/domain_services.py index 55511b03..ca1b08af 100644 --- a/server/curfu/domain_services.py +++ b/server/curfu/domain_services.py @@ -1,13 +1,15 @@ """Provide lookup services for functional domains. -TODO +Todo: +---- * domains file should be a JSON and pre-pruned to unique pairs * get_possible_domains shouldn't have to force uniqueness + """ -from typing import List, Dict import csv +from typing import Dict, List -from curfu import logger, ServiceWarning +from curfu import LookupServiceError, logger from curfu.utils import get_data_file @@ -56,5 +58,5 @@ def get_possible_domains(self, gene_id: str) -> List[Dict]: domains = self.domains[gene_id.lower()] except KeyError: logger.warning(f"Unable to retrieve associated domains for {gene_id}") - raise ServiceWarning + raise LookupServiceError return domains diff --git a/server/curfu/gene_services.py b/server/curfu/gene_services.py index f70ed5ce..f4c5f985 100644 --- a/server/curfu/gene_services.py +++ b/server/curfu/gene_services.py @@ -1,15 +1,14 @@ """Wrapper for required Gene Normalization services.""" -from typing import List, Tuple, Dict, Union import csv +from typing import Dict, List, Tuple, Union +from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE from gene.query import QueryHandler from gene.schemas import MatchType -from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE -from curfu import logger, ServiceWarning, MAX_SUGGESTIONS +from curfu import MAX_SUGGESTIONS, LookupServiceError, logger from curfu.utils import get_data_file - # term -> (normalized ID, normalized label) Map = Dict[str, Tuple[str, str, str]] @@ -51,13 +50,13 @@ def get_normalized_gene( if not gd or not gd.gene_id: msg = f"Unexpected null property in normalized response for `{term}`" logger.error(msg) - raise ServiceWarning(msg) + raise LookupServiceError(msg) concept_id = gd.gene_id symbol = gd.label if not symbol: msg = f"Unable to retrieve symbol for gene {concept_id}" logger.error(msg) - raise ServiceWarning(msg) + raise LookupServiceError(msg) term_lower = term.lower() term_cased = None if response.match_type == 100: @@ -100,7 +99,7 @@ def get_normalized_gene( else: warn = f"Lookup of gene term {term} failed." logger.warning(warn) - raise ServiceWarning(warn) + raise LookupServiceError(warn) def suggest_genes(self, query: str) -> Dict[str, List[Tuple[str, str, str]]]: """Provide autocomplete suggestions based on submitted term. @@ -153,6 +152,6 @@ def suggest_genes(self, query: str) -> Dict[str, List[Tuple[str, str, str]]]: if n > MAX_SUGGESTIONS: warn = f"Exceeds max matches: Got {n} possible matches for {query} (limit: {MAX_SUGGESTIONS})" # noqa: E501 logger.warning(warn) - raise ServiceWarning(warn) + raise LookupServiceError(warn) else: return suggestions diff --git a/server/curfu/main.py b/server/curfu/main.py index 7f083a98..9f8e811e 100644 --- a/server/curfu/main.py +++ b/server/curfu/main.py @@ -3,24 +3,23 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from starlette.templating import _TemplateResponse as TemplateResponse from fusor import FUSOR +from starlette.templating import _TemplateResponse as TemplateResponse from curfu import APP_ROOT -from curfu.version import __version__ as curfu_version -from curfu.gene_services import GeneService from curfu.domain_services import DomainService +from curfu.gene_services import GeneService from curfu.routers import ( - nomenclature, - utilities, - constructors, - lookup, complete, - validate, + constructors, demo, + lookup, meta, + nomenclature, + utilities, + validate, ) - +from curfu.version import __version__ as curfu_version fastapi_app = FastAPI( version=curfu_version, @@ -52,8 +51,7 @@ def serve_react_app(app: FastAPI) -> FastAPI: - """ - Wrap application initialization in Starlette route param converter. + """Wrap application initialization in Starlette route param converter. :param app: FastAPI application instance :return: application with React frontend mounted @@ -67,8 +65,7 @@ def serve_react_app(app: FastAPI) -> FastAPI: @app.get("/{full_path:path}") async def serve_react_app(request: Request, full_path: str) -> TemplateResponse: - """ - Add arbitrary path support to FastAPI service. + """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 @@ -118,7 +115,7 @@ def get_domain_services() -> DomainService: @app.on_event("startup") -async def startup(): +async def startup() -> None: """Get FUSOR reference""" app.state.fusor = await start_fusor() app.state.genes = get_gene_services() @@ -126,6 +123,6 @@ async def startup(): @app.on_event("shutdown") -async def shutdown(): +async def shutdown() -> None: """Clean up thread pool.""" await app.state.fusor.cool_seq_tool.uta_db._connection_pool.close() diff --git a/server/curfu/routers/complete.py b/server/curfu/routers/complete.py index b879a5d4..804a8633 100644 --- a/server/curfu/routers/complete.py +++ b/server/curfu/routers/complete.py @@ -1,11 +1,10 @@ """Provide routes for autocomplete/term suggestion methods""" -from typing import Dict, Any +from typing import Any, Dict -from fastapi import Query, APIRouter, Request - -from curfu import ServiceWarning -from curfu.schemas import ResponseDict, AssociatedDomainResponse, SuggestGeneResponse +from fastapi import APIRouter, Query, Request +from curfu import LookupServiceError +from curfu.schemas import AssociatedDomainResponse, ResponseDict, SuggestGeneResponse router = APIRouter() @@ -29,7 +28,7 @@ def suggest_gene(request: Request, term: str = Query("")) -> ResponseDict: try: possible_matches = request.app.state.genes.suggest_genes(term) response.update(possible_matches) - except ServiceWarning as e: + except LookupServiceError as e: response["warnings"] = [str(e)] return response @@ -53,6 +52,6 @@ def suggest_domain(request: Request, gene_id: str = Query("")) -> ResponseDict: try: possible_matches = request.app.state.domains.get_possible_domains(gene_id) response["suggestions"] = possible_matches - except ServiceWarning: + except LookupServiceError: response["warnings"] = [f"No associated domains for {gene_id}"] return response diff --git a/server/curfu/routers/constructors.py b/server/curfu/routers/constructors.py index f461a9f4..88120656 100644 --- a/server/curfu/routers/constructors.py +++ b/server/curfu/routers/constructors.py @@ -1,21 +1,21 @@ """Provide routes for element construction endpoints""" from typing import Optional -from fastapi import Query, Request, APIRouter +from fastapi import APIRouter, Query, Request +from fusor.models import DomainStatus, RegulatoryClass, Strand from pydantic import ValidationError -from fusor.models import RegulatoryClass, Strand, DomainStatus from curfu import logger +from curfu.routers import parse_identifier from curfu.schemas import ( GeneElementResponse, - RegulatoryElementResponse, - TxSegmentElementResponse, - TemplatedSequenceElementResponse, GetDomainResponse, + RegulatoryElementResponse, ResponseDict, + TemplatedSequenceElementResponse, + TxSegmentElementResponse, ) -from curfu.routers import parse_identifier -from curfu.sequence_services import get_strand, InvalidInputException +from curfu.sequence_services import InvalidInputError, get_strand router = APIRouter() @@ -28,6 +28,7 @@ ) def build_gene_element(request: Request, term: str = Query("")) -> GeneElementResponse: """Construct valid gene element given user-provided term. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -108,7 +109,7 @@ async def build_tx_segment_gct( if strand is not None: try: strand_validated = get_strand(strand) - except InvalidInputException: + except InvalidInputError: warning = f"Received invalid strand value: {strand}" logger.warning(warning) return TxSegmentElementResponse(warnings=[warning], element=None) @@ -155,7 +156,7 @@ async def build_tx_segment_gcg( if strand is not None: try: strand_validated = get_strand(strand) - except InvalidInputException: + except InvalidInputError: warning = f"Received invalid strand value: {strand}" logger.warning(warning) return TxSegmentElementResponse(warnings=[warning], element=None) diff --git a/server/curfu/routers/demo.py b/server/curfu/routers/demo.py index 702a5e76..6feabb8b 100644 --- a/server/curfu/routers/demo.py +++ b/server/curfu/routers/demo.py @@ -1,39 +1,39 @@ """Provide routes for accessing demo objects to client.""" -from uuid import uuid4 from typing import Union +from uuid import uuid4 from fastapi import APIRouter, Request -from fusor import examples, FUSOR +from fusor import FUSOR, examples from fusor.models import ( - RegulatoryElement, - StructuralElementType, - CategoricalFusion, AssayedFusion, + CategoricalFusion, FUSORTypes, + RegulatoryElement, + StructuralElementType, ) from fusor.nomenclature import ( - tx_segment_nomenclature, - templated_seq_nomenclature, gene_nomenclature, reg_element_nomenclature, + templated_seq_nomenclature, + tx_segment_nomenclature, ) from curfu.schemas import ( - DemoResponse, - ClientTranscriptSegmentElement, + ClientAssayedFusion, + ClientCategoricalFusion, + ClientGeneElement, ClientLinkerElement, + ClientMultiplePossibleGenesElement, ClientTemplatedSequenceElement, - ClientGeneElement, + ClientTranscriptSegmentElement, ClientUnknownGeneElement, - ClientMultiplePossibleGenesElement, - TranscriptSegmentElement, + DemoResponse, + GeneElement, LinkerElement, + MultiplePossibleGenesElement, TemplatedSequenceElement, - GeneElement, + TranscriptSegmentElement, UnknownGeneElement, - MultiplePossibleGenesElement, - ClientCategoricalFusion, - ClientAssayedFusion, ) router = APIRouter() @@ -63,8 +63,7 @@ def clientify_structural_element( element: ElementUnion, fusor_instance: FUSOR, ) -> ClientElementUnion: - """ - Add fields required by client to structural element object. + """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 @@ -112,8 +111,8 @@ def clientify_structural_element( def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: - """ - Add client-required properties to fusion object. + """Add client-required properties to fusion object. + :param fusion: fusion to append to :param fusor_instance: FUSOR object instance provided by FastAPI request context :return: completed client-ready fusion diff --git a/server/curfu/routers/lookup.py b/server/curfu/routers/lookup.py index 348b96e5..45854b5a 100644 --- a/server/curfu/routers/lookup.py +++ b/server/curfu/routers/lookup.py @@ -1,9 +1,8 @@ """Provide routes for basic data lookup endpoints""" -from fastapi import Query, Request, APIRouter - -from curfu import ServiceWarning -from curfu.schemas import ResponseDict, NormalizeGeneResponse +from fastapi import APIRouter, Query, Request +from curfu import LookupServiceError +from curfu.schemas import NormalizeGeneResponse, ResponseDict router = APIRouter() @@ -30,6 +29,6 @@ def normalize_gene(request: Request, term: str = Query("")) -> ResponseDict: response["concept_id"] = concept_id response["symbol"] = symbol response["cased"] = cased - except ServiceWarning as e: + except LookupServiceError as e: response["warnings"] = [str(e)] return response diff --git a/server/curfu/routers/meta.py b/server/curfu/routers/meta.py index db76b24f..7313440f 100644 --- a/server/curfu/routers/meta.py +++ b/server/curfu/routers/meta.py @@ -1,8 +1,7 @@ """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 -from cool_seq_tool.version import __version__ as cool_seq_tool_version from curfu.schemas import ServiceInfoResponse from curfu.version import __version__ as curfu_version diff --git a/server/curfu/routers/nomenclature.py b/server/curfu/routers/nomenclature.py index 1c38c4c2..291954b9 100644 --- a/server/curfu/routers/nomenclature.py +++ b/server/curfu/routers/nomenclature.py @@ -1,7 +1,8 @@ """Provide routes for nomenclature generation.""" from typing import Dict -from fastapi import Request, APIRouter, Body +from fastapi import APIRouter, Body, Request +from fusor.exceptions import FUSORParametersException from fusor.models import ( GeneElement, RegulatoryElement, @@ -14,13 +15,11 @@ templated_seq_nomenclature, tx_segment_nomenclature, ) -from fusor.exceptions import FUSORParametersException from pydantic import ValidationError from curfu import logger from curfu.schemas import NomenclatureResponse, ResponseDict - router = APIRouter() @@ -33,8 +32,8 @@ def generate_regulatory_element_nomenclature( request: Request, regulatory_element: Dict = Body() ) -> ResponseDict: - """ - Build regulatory element nomenclature. + """Build regulatory element nomenclature. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -72,8 +71,8 @@ def generate_regulatory_element_nomenclature( response_model_exclude_none=True, ) def generate_tx_segment_nomenclature(tx_segment: Dict = Body()) -> ResponseDict: - """ - Build transcript segment element nomenclature. + """Build transcript segment element nomenclature. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. diff --git a/server/curfu/routers/utilities.py b/server/curfu/routers/utilities.py index 89a0f6b0..4ffed21a 100644 --- a/server/curfu/routers/utilities.py +++ b/server/curfu/routers/utilities.py @@ -1,22 +1,21 @@ """Provide routes for app utility endpoints""" -from typing import Dict, Optional, List, Any -import tempfile import os +import tempfile from pathlib import Path +from typing import Any, Dict, List, Optional -from fastapi import APIRouter, Request, Query, HTTPException +from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import FileResponse +from gene import schemas as gene_schemas from starlette.background import BackgroundTasks -from gene import schemas as GeneSchemas from curfu import logger from curfu.schemas import ( - GetTranscriptsResponse, CoordsUtilsResponse, + GetTranscriptsResponse, SequenceIDResponse, ) -from curfu.sequence_services import get_strand, InvalidInputException - +from curfu.sequence_services import InvalidInputError, get_strand router = APIRouter() @@ -36,7 +35,7 @@ def get_mane_transcripts(request: Request, term: str) -> Dict: :return: Dict containing transcripts if lookup succeeds, or warnings upon failure """ normalized = request.app.state.fusor.gene_normalizer.normalize(term) - if normalized.match_type == GeneSchemas.MatchType.NO_MATCH: + 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"): return {"warnings": [f"No HGNC symbol: {term}"]} @@ -157,7 +156,7 @@ async def get_exon_coords( if strand is not None: try: strand_validated = get_strand(strand) - except InvalidInputException: + except InvalidInputError: warnings.append(f"Received invalid strand value: {strand}") else: strand_validated = strand diff --git a/server/curfu/routers/validate.py b/server/curfu/routers/validate.py index d4425c2e..5150c315 100644 --- a/server/curfu/routers/validate.py +++ b/server/curfu/routers/validate.py @@ -1,7 +1,7 @@ """Provide validation endpoint to confirm correctness of fusion object structure.""" from typing import Dict -from fastapi import Body, Request, APIRouter +from fastapi import APIRouter, Body, Request from fusor.exceptions import FUSORParametersException from curfu.schemas import ResponseDict, ValidateFusionResponse diff --git a/server/curfu/schemas.py b/server/curfu/schemas.py index 15971ec3..e714fc7b 100644 --- a/server/curfu/schemas.py +++ b/server/curfu/schemas.py @@ -1,24 +1,23 @@ """Provide schemas for FastAPI responses.""" -from typing import List, Optional, Tuple, Union, Literal, Dict +from typing import Dict, List, Literal, Optional, Tuple, Union -from pydantic import BaseModel, Field, StrictStr, StrictInt, validator, Extra -from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE +from cool_seq_tool.schemas import GenomicData from fusor.models import ( AssayedFusion, CategoricalFusion, + FunctionalDomain, Fusion, - TranscriptSegmentElement, - LinkerElement, - TemplatedSequenceElement, GeneElement, - UnknownGeneElement, + LinkerElement, MultiplePossibleGenesElement, RegulatoryElement, - FunctionalDomain, Strand, + TemplatedSequenceElement, + TranscriptSegmentElement, + UnknownGeneElement, ) -from cool_seq_tool.schemas import GenomicData - +from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE +from pydantic import BaseModel, Extra, Field, StrictInt, StrictStr, validator ResponseWarnings = Optional[List[StrictStr]] diff --git a/server/curfu/sequence_services.py b/server/curfu/sequence_services.py index 996b8a69..b6535b84 100644 --- a/server/curfu/sequence_services.py +++ b/server/curfu/sequence_services.py @@ -5,11 +5,9 @@ logger.setLevel(logging.DEBUG) -class InvalidInputException(Exception): +class InvalidInputError(Exception): """Provide exception for input validation.""" - pass - def get_strand(input: str) -> int: """Validate strand arguments received from client. @@ -22,4 +20,4 @@ def get_strand(input: str) -> int: elif input == "-": return -1 else: - raise InvalidInputException + raise InvalidInputError diff --git a/server/curfu/utils.py b/server/curfu/utils.py index b2dabaaf..3634bfff 100644 --- a/server/curfu/utils.py +++ b/server/curfu/utils.py @@ -1,15 +1,14 @@ """Miscellaneous helper functions.""" import os from pathlib import Path -from typing import TypeVar, List +from typing import List, TypeVar import boto3 from boto3.exceptions import ResourceLoadException -from botocore.exceptions import ClientError - from botocore.config import Config +from botocore.exceptions import ClientError -from curfu import logger, APP_ROOT +from curfu import APP_ROOT, logger ObjectSummary = TypeVar("ObjectSummary") @@ -58,9 +57,9 @@ def download_s3_file(bucket_object: ObjectSummary) -> 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 + """Get path to latest version of given data file. Download from S3 if not available locally. + :param file_prefix: leading pattern in filename (eg `gene_aliases`) :param local_files: local files matching pattern :return: path to up-to-date file @@ -74,8 +73,8 @@ def get_latest_data_file(file_prefix: str, local_files: List[Path]) -> Path: def get_data_file(filename_prefix: str) -> Path: - """ - Acquire most recent version of static data file. Download from S3 if not available locally. + """Acquire most recent version of static data file. Download from S3 if not available locally. + :param filename_prefix: leading text of filename, eg `gene_aliases_suggest`. Should not include filetype or date information. :return: Path to acquired file. diff --git a/server/pyproject.toml b/server/pyproject.toml new file mode 100644 index 00000000..e50d642d --- /dev/null +++ b/server/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta:__legacy__" + +[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"] + +# D205 - blank-line-after-summary +# 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" + +[tool.ruff.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 +# F401 - unused-import +"tests/*" = ["ANN001", "ANN2", "ANN102"] +"setup.py" = ["F821"] +"*__init__.py" = ["F401"] +"curfu/schemas.py" = ["ANN201", "N805", "ANN001"] +"curfu/routers/*" = ["D301"] +"curfu/cli.py" = ["D301"] diff --git a/server/setup.cfg b/server/setup.cfg index 98138733..18b8c7fe 100644 --- a/server/setup.cfg +++ b/server/setup.cfg @@ -33,8 +33,7 @@ tests = dev = psycopg2-binary - flake8 - flake8-docstrings + ruff black pre-commit pydantic-to-typescript diff --git a/server/tests/conftest.py b/server/tests/conftest.py index c1d4a340..46c481b8 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 import pytest -import asyncio from httpx import AsyncClient -from curfu.main import app, start_fusor, get_gene_services, get_domain_services +from curfu.main import app, get_domain_services, get_gene_services, start_fusor @pytest.fixture(scope="session") diff --git a/server/tests/integration/test_main.py b/server/tests/integration/test_main.py index f220fe6b..0a692e2d 100644 --- a/server/tests/integration/test_main.py +++ b/server/tests/integration/test_main.py @@ -15,10 +15,10 @@ 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 - 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"]) + 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 + 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( diff --git a/server/tests/integration/test_nomenclature.py b/server/tests/integration/test_nomenclature.py index 5fc39bfe..ef78fca0 100644 --- a/server/tests/integration/test_nomenclature.py +++ b/server/tests/integration/test_nomenclature.py @@ -2,8 +2,8 @@ from typing import Dict import pytest -from httpx import AsyncClient from fusor.examples import bcr_abl1 +from httpx import AsyncClient @pytest.fixture(scope="module") diff --git a/server/tests/integration/test_utilities.py b/server/tests/integration/test_utilities.py index 3652b4ba..e0d3d0f6 100644 --- a/server/tests/integration/test_utilities.py +++ b/server/tests/integration/test_utilities.py @@ -1,9 +1,8 @@ """Test end-to-end correctness of utility routes.""" -from typing import Dict, Callable +from typing import Callable, Dict import pytest - response_callback_type = Callable[[Dict, Dict], None] diff --git a/server/tests/integration/test_validate.py b/server/tests/integration/test_validate.py index f1f445d7..b15605d0 100644 --- a/server/tests/integration/test_validate.py +++ b/server/tests/integration/test_validate.py @@ -202,7 +202,8 @@ def wrong_type_fusion(): async def check_validated_fusion_response(client, fixture: Dict, case_name: str): """Run basic checks on fusion validation response. - TODO + Todo: + ---- * FUSOR should provide a "fusion equality" utility function -- incorporate it here when that's done """ From ce74f4bfa44ee8e53a5e8b6efb44c6a1c1e08004 Mon Sep 17 00:00:00 2001 From: katie stahl Date: Wed, 6 Sep 2023 13:58:56 -0400 Subject: [PATCH 05/14] fix: adding validation to disallow same-gene fusions (#251) (#263) * fix: adding validation to disallow same-gene fusions * remove console log * fix: adding validation to disallow same-gene fusions * fix: fixing console error for unique keys --- .../Pages/Summary/Invalid/Invalid.tsx | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/client/src/components/Pages/Summary/Invalid/Invalid.tsx b/client/src/components/Pages/Summary/Invalid/Invalid.tsx index 793dc850..97bca2dd 100644 --- a/client/src/components/Pages/Summary/Invalid/Invalid.tsx +++ b/client/src/components/Pages/Summary/Invalid/Invalid.tsx @@ -49,6 +49,26 @@ export const Invalid: React.FC = ({ })); const classes = useStyles(); + const duplicateGeneError = (duplicateGenes: string[]) => { + return ( + + 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.{" "} + setVisibleTab(0)}> + Edit elements to resolve. + + + ) + }; + const elementNumberError = ( Insufficient number of structural and regulatory elements. Per the{" "} @@ -146,6 +166,10 @@ export const Invalid: React.FC = ({ const CURIE_PATTERN = /^\w[^:]*:.+$/; + 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 checkErrors = () => { const errorElements: React.ReactFragment[] = []; if ( @@ -166,6 +190,9 @@ export const Invalid: React.FC = ({ errorElements.push(noGeneElementsError); } } + if (duplicateGenes.length > 0) { + errorElements.push(duplicateGeneError(duplicateGenes)) + } if (fusion.type == "AssayedFusion") { if ( !( @@ -210,10 +237,10 @@ export const Invalid: React.FC = ({ {checkErrors().map((error, index: number) => ( - <> + {index > 0 ? : <>} {error} - + ))} From c25747ae9ff5ff1c6d1437258bfe97e674a6ebd8 Mon Sep 17 00:00:00 2001 From: katie stahl Date: Thu, 14 Sep 2023 15:36:23 -0400 Subject: [PATCH 06/14] fix: preventing delete of fusion elements during fetch to fix race condition * fix: adding ref for mounted component to fix issue where race condition was causing deleted elements to reappear * undoing mounted ref changes * undoing mounted ref changes * fix: preventing delete of fusion elements during fetch to fix race condition * fix: race condition causing elements to get re-added * removing file --- .../Input/GeneElementInput/GeneElementInput.tsx | 8 ++++++-- .../Structure/Input/StaticElement/StaticElement.tsx | 5 +++-- .../Input/StructuralElementInputAccordion.tsx | 6 +++++- .../Structure/Input/StructuralElementInputProps.tsx | 1 + .../TemplatedSequenceElementInput.tsx | 6 ++++++ .../TxSegmentElementInput/TxSegmentElementInput.tsx | 12 ++++++++---- 6 files changed, 29 insertions(+), 9 deletions(-) diff --git a/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx b/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx index 5ceba851..7b34f4d7 100644 --- a/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/GeneElementInput/GeneElementInput.tsx @@ -9,7 +9,7 @@ import { getGeneElement, getGeneNomenclature, } from "../../../../../services/main"; -import ElementInputAccordion from "../StructuralElementInputAccordion"; +import StructuralElementInputAccordion from "../StructuralElementInputAccordion"; interface GeneElementInputProps extends StructuralElementInputProps { element: ClientGeneElement; @@ -28,12 +28,14 @@ const GeneElementInput: React.FC = ({ const [geneText, setGeneText] = useState(""); const validated = gene !== "" && geneText == ""; const [expanded, setExpanded] = useState(!validated); + const [pendingResponse, setPendingResponse] = useState(false); useEffect(() => { if (validated) buildGeneElement(); }, [gene, geneText]); const buildGeneElement = () => { + setPendingResponse(true) getGeneElement(gene).then((geneElementResponse) => { if ( geneElementResponse.warnings && @@ -56,6 +58,7 @@ const GeneElementInput: React.FC = ({ nomenclature: nomenclatureResponse.nomenclature, }; handleSave(index, clientGeneElement); + setPendingResponse(false) } } ); @@ -74,7 +77,7 @@ const GeneElementInput: React.FC = ({ /> ); - return ElementInputAccordion({ + return StructuralElementInputAccordion({ expanded, setExpanded, element, @@ -82,6 +85,7 @@ const GeneElementInput: React.FC = ({ inputElements, validated, icon, + pendingResponse }); }; diff --git a/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx b/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx index 9ff0d2b0..c97b963f 100644 --- a/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx +++ b/client/src/components/Pages/Structure/Input/StaticElement/StaticElement.tsx @@ -1,17 +1,18 @@ import { BaseStructuralElementProps } from "../StructuralElementInputProps"; -import CompInputAccordion from "../StructuralElementInputAccordion"; +import StructuralElementInputAccordion from "../StructuralElementInputAccordion"; const StaticElement: React.FC = ({ element, handleDelete, icon, }) => - CompInputAccordion({ + StructuralElementInputAccordion({ expanded: false, element, handleDelete, validated: true, icon, + pendingResponse: false }); export default StaticElement; diff --git a/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx b/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx index f47cc2ca..05b4fe92 100644 --- a/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx +++ b/client/src/components/Pages/Structure/Input/StructuralElementInputAccordion.tsx @@ -1,4 +1,4 @@ -import { Tooltip, makeStyles } from "@material-ui/core"; +import { Tooltip, makeStyles, CircularProgress } from "@material-ui/core"; import { styled } from "@mui/material/styles"; import IconButton, { IconButtonProps } from "@mui/material/IconButton"; import Card from "@mui/material/Card"; @@ -43,6 +43,7 @@ interface StructuralElementInputAccordionProps inputElements?: JSX.Element; validated: boolean; icon: JSX.Element; + pendingResponse?: boolean; } interface ExpandMoreProps extends IconButtonProps { @@ -71,6 +72,7 @@ const StructuralElementInputAccordion: React.FC< inputElements, validated, icon, + pendingResponse }) => { const classes = useStyles(); @@ -79,6 +81,8 @@ const StructuralElementInputAccordion: React.FC< : void; icon: JSX.Element; + pendingResponse?: boolean } export interface StructuralElementInputProps diff --git a/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx b/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx index c45e59f0..f7c60221 100644 --- a/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TemplatedSequenceElementInput/TemplatedSequenceElementInput.tsx @@ -18,6 +18,7 @@ interface TemplatedSequenceElementInputProps const TemplatedSequenceElementInput: React.FC< TemplatedSequenceElementInputProps > = ({ element, index, handleSave, handleDelete, icon }) => { + const [chromosome, setChromosome] = useState( element.input_chromosome || "" ); @@ -39,6 +40,8 @@ const TemplatedSequenceElementInput: React.FC< const [expanded, setExpanded] = useState(!validated); + const [pendingResponse, setPendingResponse] = useState(false); + useEffect(() => { if (inputComplete) { buildTemplatedSequenceElement(); @@ -64,6 +67,7 @@ const TemplatedSequenceElementInput: React.FC< ) { // TODO visible error handling setInputError("element validation unsuccessful"); + setPendingResponse(false) return; } else if (templatedSequenceResponse.element) { setInputError(""); @@ -83,6 +87,7 @@ const TemplatedSequenceElementInput: React.FC< } }); } + setPendingResponse(false) }); }; @@ -162,6 +167,7 @@ const TemplatedSequenceElementInput: React.FC< inputElements, validated, icon, + pendingResponse }); }; diff --git a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx index 8845f9ee..f806ff8f 100644 --- a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx @@ -19,7 +19,7 @@ import { } from "../../../../../services/main"; import { GeneAutocomplete } from "../../../../main/shared/GeneAutocomplete/GeneAutocomplete"; import { StructuralElementInputProps } from "../StructuralElementInputProps"; -import CompInputAccordion from "../StructuralElementInputAccordion"; +import StructuralElementInputAccordion from "../StructuralElementInputAccordion"; import { FusionContext } from "../../../../../global/contexts/FusionContext"; import StrandSwitch from "../../../../main/shared/StrandSwitch/StrandSwitch"; import HelpTooltip from "../../../../main/shared/HelpTooltip/HelpTooltip"; @@ -85,6 +85,8 @@ const TxSegmentCompInput: React.FC = ({ ); const [endingExonOffsetText, setEndingExonOffsetText] = useState(""); + const [pendingResponse, setPendingResponse] = useState(false); + /* Depending on this element's location in the structure array, the user needs to provide some kind of coordinate input for either one or both ends @@ -142,7 +144,7 @@ const TxSegmentCompInput: React.FC = ({ endingExon, startingExonOffset, endingExonOffset, - index, + index ]); const handleTxElementResponse = ( @@ -156,7 +158,6 @@ const TxSegmentCompInput: React.FC = ({ ...responseElement, ...inputParams, }; - if (!hasRequiredEnds) { finishedElement.nomenclature = "ERROR"; } else { @@ -170,6 +171,7 @@ const TxSegmentCompInput: React.FC = ({ } }); } + setPendingResponse(false) }; /** @@ -226,6 +228,7 @@ const TxSegmentCompInput: React.FC = ({ * Request construction of tx segment element from server and handle response */ const buildTranscriptSegmentElement = () => { + setPendingResponse(true) // fire constructor request switch (txInputType) { case InputType.gcg: @@ -665,7 +668,7 @@ const TxSegmentCompInput: React.FC = ({ ); - return CompInputAccordion({ + return StructuralElementInputAccordion({ expanded, setExpanded, element, @@ -673,6 +676,7 @@ const TxSegmentCompInput: React.FC = ({ inputElements, validated, icon, + pendingResponse }); }; From 3f1f9f7177a3a1bb2ac7206042635460145f653b Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 2 Oct 2023 12:18:17 -0400 Subject: [PATCH 07/14] chore: delete unused configs --- .flake8 | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 833ca122..00000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -ignore = D205, D400, D301, W503 -max-line-length=88 -exclude = - .git - __pycache__ - curation/version.py -per-file-ignores = - */tests/*:D403 From b4be59ae9a95f33861ddd3696ef6be0bd4a84aea Mon Sep 17 00:00:00 2001 From: katie stahl Date: Tue, 3 Oct 2023 15:30:04 -0400 Subject: [PATCH 08/14] =?UTF-8?q?feat:=20relax=20requirements=20on=20minim?= =?UTF-8?q?um=20information=20model=20components=20for=20=E2=80=A6=20(#264?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: relax requirements on minimum information model components for nomenclature generation * feat: relaxing requirements on minimum information model components * feat: relaxing requirements on minimum information model for nomenclature generation, updating fusor version to accomodate --- .../Pages/Summary/Invalid/Invalid.tsx | 81 ------------------- .../Pages/Summary/Readable/Readable.tsx | 7 +- requirements.txt | 2 +- server/setup.cfg | 2 +- 4 files changed, 7 insertions(+), 85 deletions(-) diff --git a/client/src/components/Pages/Summary/Invalid/Invalid.tsx b/client/src/components/Pages/Summary/Invalid/Invalid.tsx index 97bca2dd..8672978c 100644 --- a/client/src/components/Pages/Summary/Invalid/Invalid.tsx +++ b/client/src/components/Pages/Summary/Invalid/Invalid.tsx @@ -98,63 +98,6 @@ export const Invalid: React.FC = ({ ); - const noEventError = ( - - The causative event is not specified.{" "} - setVisibleTab(2)}> - Declare the event type - {" "} - to resolve. - - ); - - const noAssayError = ( - - No assay metadata is provided. You must{" "} - setVisibleTab(3)}> - identify the assay, detection method, and methodology that was used to - uncover the fusion - {" "} - in order to resolve. - - ); - - const assayIdCurieError = ( - - The provided assay ID is not a valid{" "} - - W3 CURIE - - .{" "} - setVisibleTab(3)}> - Update the assay ID - {" "} - to resolve. - - ); - - const assayMethodUriCurieError = ( - - The provided assay method URI is not a valid{" "} - - W3 CURIE - - .{" "} - setVisibleTab(3)}> - Update the method URI - {" "} - to resolve. - - ); - const unknownError = ( <> @@ -164,8 +107,6 @@ export const Invalid: React.FC = ({ ); - const CURIE_PATTERN = /^\w[^:]*:.+$/; - 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) @@ -193,28 +134,6 @@ export const Invalid: React.FC = ({ if (duplicateGenes.length > 0) { errorElements.push(duplicateGeneError(duplicateGenes)) } - if (fusion.type == "AssayedFusion") { - if ( - !( - fusion.assay && - fusion.assay.assay_name && - fusion.assay.assay_id && - fusion.assay.method_uri - ) - ) { - errorElements.push(noAssayError); - } else { - if (!fusion.assay.assay_id.match(CURIE_PATTERN)) { - errorElements.push(assayIdCurieError); - } - if (!fusion.assay.method_uri.match(CURIE_PATTERN)) { - errorElements.push(assayMethodUriCurieError); - } - } - if (!fusion.causative_event) { - errorElements.push(noEventError); - } - } if (errorElements.length == 0) { errorElements.push( <> diff --git a/client/src/components/Pages/Summary/Readable/Readable.tsx b/client/src/components/Pages/Summary/Readable/Readable.tsx index 1632cf83..291464f2 100644 --- a/client/src/components/Pages/Summary/Readable/Readable.tsx +++ b/client/src/components/Pages/Summary/Readable/Readable.tsx @@ -31,6 +31,9 @@ export const Readable: React.FC = ({ validatedFusion }) => { ); }, [validatedFusion]); + const assayName = fusion.assay?.assay_name ? fusion.assay.assay_name : "" + const assayId = fusion.assay?.assay_id ? `(${fusion.assay.assay_id})` : "" + /** * Render rows specific to assayed fusion fields * @returns React component containing table rows @@ -43,7 +46,7 @@ export const Readable: React.FC = ({ validatedFusion }) => { - {eventDisplayMap[fusion.causative_event.event_type] || ""} + {eventDisplayMap[fusion.causative_event?.event_type] || ""} @@ -52,7 +55,7 @@ export const Readable: React.FC = ({ validatedFusion }) => { Assay - {`${fusion.assay.assay_name} (${fusion.assay.assay_id})`} + {fusion.assay ? `${assayName} ${assayId}` : ""} diff --git a/requirements.txt b/requirements.txt index 9db28ee1..d19c649b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ decorator==5.1.1 executing==1.2.0 fake-useragent==1.1.3 fastapi==0.100.0 -fusor==0.0.28.dev0 +fusor==0.0.30.dev1 ga4gh.vrs==0.8.4 ga4gh.vrsatile.pydantic==0.0.13 gene-normalizer==0.1.39 diff --git a/server/setup.cfg b/server/setup.cfg index ce0bdcd5..cb825e63 100644 --- a/server/setup.cfg +++ b/server/setup.cfg @@ -16,7 +16,7 @@ install_requires = fastapi >= 0.72.0 aiofiles asyncpg - fusor ~= 0.0.28-dev0 + fusor ~= 0.0.30-dev1 sqlparse >= 0.4.2 urllib3 >= 1.26.5 click From 3b83fa9fec1c3fdd7d65f8331d8ff453fc3c2eb3 Mon Sep 17 00:00:00 2001 From: katie stahl Date: Mon, 9 Oct 2023 15:12:17 -0400 Subject: [PATCH 09/14] bugfix: removing identifier in front of chromosome on auto populate (#268) --- .../Input/TxSegmentElementInput/TxSegmentElementInput.tsx | 2 -- .../components/Utilities/GetCoordinates/GetCoordinates.tsx | 1 - .../main/shared/ChromosomeField/ChromosomeField.tsx | 7 +------ .../main/shared/GeneAutocomplete/GeneAutocomplete.tsx | 5 +++-- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx index 48e193b0..077808d0 100644 --- a/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx +++ b/client/src/components/Pages/Structure/Input/TxSegmentElementInput/TxSegmentElementInput.tsx @@ -439,9 +439,7 @@ const TxSegmentCompInput: React.FC = ({ diff --git a/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx b/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx index f24fedda..7e8f7431 100644 --- a/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx +++ b/client/src/components/Utilities/GetCoordinates/GetCoordinates.tsx @@ -252,7 +252,6 @@ const GetCoordinates: React.FC = () => { diff --git a/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx b/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx index b2b5cb09..26d02456 100644 --- a/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx +++ b/client/src/components/main/shared/ChromosomeField/ChromosomeField.tsx @@ -4,17 +4,13 @@ import HelpTooltip from "../HelpTooltip/HelpTooltip"; interface Props { fieldValue: string; - valueSetter: CallableFunction; errorText: string; - keyHandler: KeyboardEventHandler | undefined; width?: number | undefined; } const ChromosomeField: React.FC = ({ fieldValue, - valueSetter, errorText, - keyHandler, width, }) => { const useStyles = makeStyles(() => ({ @@ -42,11 +38,10 @@ const ChromosomeField: React.FC = ({ valueSetter(event.target.value)} error={errorText != ""} - onKeyDown={keyHandler} label="Chromosome" helperText={errorText != "" ? errorText : null} + contentEditable={false} className={classes.textField} /> diff --git a/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx b/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx index 85821a70..fbeb24b4 100644 --- a/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx +++ b/client/src/components/main/shared/GeneAutocomplete/GeneAutocomplete.tsx @@ -33,7 +33,7 @@ interface Props { setGene: CallableFunction; geneText: string; setGeneText: CallableFunction; - tooltipDirection: + tooltipDirection?: | "bottom" | "left" | "right" @@ -89,7 +89,8 @@ export const GeneAutocomplete: React.FC = ({ setGene(selection.value); setGeneValue(selection); if (setChromosome) { - setChromosome(selection.chromosome); + // substring is to remove identifier from beginning of chromosome (ex: result in NC_000007.14 instead of NCBI:NC_000007.14) + setChromosome(selection.chromosome?.substring(selection.chromosome.indexOf(":") + 1)); } if (setStrand) { setStrand(selection.strand); From 27a477f5bbca25183b81ff40e95e96c50d87d7ec Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Fri, 13 Oct 2023 11:39:47 -0400 Subject: [PATCH 10/14] docs: update API doc organization (#269) --- server/curfu/main.py | 13 ++++++++++++- server/curfu/routers/complete.py | 9 ++++++++- server/curfu/routers/constructors.py | 8 ++++++++ server/curfu/routers/demo.py | 7 +++++++ server/curfu/routers/lookup.py | 3 ++- server/curfu/routers/meta.py | 7 +++++-- server/curfu/routers/nomenclature.py | 7 ++++++- server/curfu/routers/utilities.py | 6 ++++++ server/curfu/routers/validate.py | 3 ++- server/curfu/schemas.py | 15 +++++++++++++++ 10 files changed, 71 insertions(+), 7 deletions(-) diff --git a/server/curfu/main.py b/server/curfu/main.py index 80187b0b..d4df661e 100644 --- a/server/curfu/main.py +++ b/server/curfu/main.py @@ -22,6 +22,17 @@ from curfu.version import __version__ as curfu_version fastapi_app = FastAPI( + title="Fusion Curation API", + description="Provide data functions to support [VICC Fusion Curation interface](fusion-builder.cancervariants.org/).", + contact={ + "name": "Alex H. Wagner", + "email": "Alex.Wagner@nationwidechildrens.org", + "url": "https://www.nationwidechildrens.org/specialties/institute-for-genomic-medicine/research-labs/wagner-lab", + }, + license={ + "name": "MIT", + "url": "https://github.com/cancervariants/fusion-curation/blob/main/LICENSE", + }, version=curfu_version, swagger_ui_parameters={"tryItOutEnabled": True}, docs_url="/docs", @@ -63,7 +74,7 @@ def serve_react_app(app: FastAPI) -> FastAPI: ) templates = Jinja2Templates(directory=BUILD_DIR.as_posix()) - @app.get("/{full_path:path}") + @app.get("/{full_path:path}", include_in_schema=False) async def serve_react_app(request: Request, full_path: str) -> TemplateResponse: """Add arbitrary path support to FastAPI service. diff --git a/server/curfu/routers/complete.py b/server/curfu/routers/complete.py index 7faa75a5..5b03bb71 100644 --- a/server/curfu/routers/complete.py +++ b/server/curfu/routers/complete.py @@ -4,7 +4,12 @@ from fastapi import APIRouter, Query, Request from curfu import MAX_SUGGESTIONS, LookupServiceError -from curfu.schemas import AssociatedDomainResponse, ResponseDict, SuggestGeneResponse +from curfu.schemas import ( + AssociatedDomainResponse, + ResponseDict, + RouteTag, + SuggestGeneResponse, +) router = APIRouter() @@ -14,6 +19,7 @@ operation_id="suggestGene", response_model=SuggestGeneResponse, response_model_exclude_none=True, + tags=[RouteTag.COMPLETION], ) def suggest_gene(request: Request, term: str = Query("")) -> ResponseDict: """Provide completion suggestions for term provided by user. @@ -52,6 +58,7 @@ def suggest_gene(request: Request, term: str = Query("")) -> ResponseDict: operation_id="suggestDomain", response_model=AssociatedDomainResponse, response_model_exclude_none=True, + tags=[RouteTag.COMPLETION], ) def suggest_domain(request: Request, gene_id: str = Query("")) -> ResponseDict: """Provide possible domains associated with a given gene to be selected by a user. diff --git a/server/curfu/routers/constructors.py b/server/curfu/routers/constructors.py index 88120656..fab4542e 100644 --- a/server/curfu/routers/constructors.py +++ b/server/curfu/routers/constructors.py @@ -12,6 +12,7 @@ GetDomainResponse, RegulatoryElementResponse, ResponseDict, + RouteTag, TemplatedSequenceElementResponse, TxSegmentElementResponse, ) @@ -25,6 +26,7 @@ operation_id="buildGeneElement", response_model=GeneElementResponse, response_model_exclude_none=True, + tags=[RouteTag.CONSTRUCTORS], ) def build_gene_element(request: Request, term: str = Query("")) -> GeneElementResponse: """Construct valid gene element given user-provided term. @@ -48,6 +50,7 @@ def build_gene_element(request: Request, term: str = Query("")) -> GeneElementRe operation_id="buildTranscriptSegmentElementECT", response_model=TxSegmentElementResponse, response_model_exclude_none=True, + tags=[RouteTag.CONSTRUCTORS], ) async def build_tx_segment_ect( request: Request, @@ -85,6 +88,7 @@ async def build_tx_segment_ect( operation_id="buildTranscriptSegmentElementGCT", response_model=TxSegmentElementResponse, response_model_exclude_none=True, + tags=[RouteTag.CONSTRUCTORS], ) async def build_tx_segment_gct( request: Request, @@ -132,6 +136,7 @@ async def build_tx_segment_gct( operation_id="buildTranscriptSegmentElementGCG", response_model=TxSegmentElementResponse, response_model_exclude_none=True, + tags=[RouteTag.CONSTRUCTORS], ) async def build_tx_segment_gcg( request: Request, @@ -179,6 +184,7 @@ async def build_tx_segment_gcg( operation_id="buildTemplatedSequenceElement", response_model=TemplatedSequenceElementResponse, response_model_exclude_none=True, + tags=[RouteTag.CONSTRUCTORS], ) def build_templated_sequence_element( request: Request, start: int, end: int, sequence_id: str, strand: str @@ -215,6 +221,7 @@ def build_templated_sequence_element( operation_id="getDomain", response_model=GetDomainResponse, response_model_exclude_none=True, + tags=[RouteTag.CONSTRUCTORS], ) def build_domain( request: Request, @@ -260,6 +267,7 @@ def build_domain( operation_id="getRegulatoryElement", response_model=RegulatoryElementResponse, response_model_exclude_none=True, + tags=[RouteTag.CONSTRUCTORS], ) def build_regulatory_element( request: Request, element_class: RegulatoryClass, gene_name: str diff --git a/server/curfu/routers/demo.py b/server/curfu/routers/demo.py index 6feabb8b..434f3e13 100644 --- a/server/curfu/routers/demo.py +++ b/server/curfu/routers/demo.py @@ -31,6 +31,7 @@ GeneElement, LinkerElement, MultiplePossibleGenesElement, + RouteTag, TemplatedSequenceElement, TranscriptSegmentElement, UnknownGeneElement, @@ -156,6 +157,7 @@ def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: operation_id="alkDemo", response_model=DemoResponse, response_model_exclude_none=True, + tags=[RouteTag.DEMOS], ) def get_alk(request: Request) -> DemoResponse: """Retrieve ALK assayed fusion. @@ -176,6 +178,7 @@ def get_alk(request: Request) -> DemoResponse: operation_id="ewsr1Demo", response_model=DemoResponse, response_model_exclude_none=True, + tags=[RouteTag.DEMOS], ) def get_ewsr1(request: Request) -> DemoResponse: """Retrieve EWSR1 assayed fusion. @@ -196,6 +199,7 @@ def get_ewsr1(request: Request) -> DemoResponse: operation_id="bcrAbl1Demo", response_model=DemoResponse, response_model_exclude_none=True, + tags=[RouteTag.DEMOS], ) def get_bcr_abl1(request: Request) -> DemoResponse: """Retrieve BCR-ABL1 categorical fusion. @@ -216,6 +220,7 @@ def get_bcr_abl1(request: Request) -> DemoResponse: operation_id="tpm3Ntrk1Demo", response_model=DemoResponse, response_model_exclude_none=True, + tags=[RouteTag.DEMOS], ) def get_tpm3_ntrk1(request: Request) -> DemoResponse: """Retrieve TPM3-NTRK1 assayed fusion. @@ -236,6 +241,7 @@ def get_tpm3_ntrk1(request: Request) -> DemoResponse: operation_id="tpm3PdgfrbDemo", response_model=DemoResponse, response_model_exclude_none=True, + tags=[RouteTag.DEMOS], ) def get_tpm3_pdgfrb(request: Request) -> DemoResponse: """Retrieve TPM3-PDGFRB assayed fusion. @@ -256,6 +262,7 @@ def get_tpm3_pdgfrb(request: Request) -> DemoResponse: operation_id="ighMycDemo", response_model=DemoResponse, response_model_exclude_none=True, + tags=[RouteTag.DEMOS], ) def get_igh_myc(request: Request) -> DemoResponse: """Retrieve IGH-MYC assayed fusion. diff --git a/server/curfu/routers/lookup.py b/server/curfu/routers/lookup.py index 45854b5a..4e66948f 100644 --- a/server/curfu/routers/lookup.py +++ b/server/curfu/routers/lookup.py @@ -2,7 +2,7 @@ from fastapi import APIRouter, Query, Request from curfu import LookupServiceError -from curfu.schemas import NormalizeGeneResponse, ResponseDict +from curfu.schemas import NormalizeGeneResponse, ResponseDict, RouteTag router = APIRouter() @@ -12,6 +12,7 @@ operation_id="normalizeGene", response_model=NormalizeGeneResponse, response_model_exclude_none=True, + tags=[RouteTag.LOOKUP], ) def normalize_gene(request: Request, term: str = Query("")) -> ResponseDict: """Normalize gene term provided by user. diff --git a/server/curfu/routers/meta.py b/server/curfu/routers/meta.py index 7313440f..2032de94 100644 --- a/server/curfu/routers/meta.py +++ b/server/curfu/routers/meta.py @@ -3,14 +3,17 @@ from fastapi import APIRouter from fusor import __version__ as fusor_version -from curfu.schemas import ServiceInfoResponse +from curfu.schemas import RouteTag, ServiceInfoResponse from curfu.version import __version__ as curfu_version router = APIRouter() @router.get( - "/api/service_info", operation_id="serviceInfo", response_model=ServiceInfoResponse + "/api/service_info", + operation_id="serviceInfo", + response_model=ServiceInfoResponse, + tags=[RouteTag.META], ) def get_service_info() -> ServiceInfoResponse: """Return service info.""" diff --git a/server/curfu/routers/nomenclature.py b/server/curfu/routers/nomenclature.py index 291954b9..710cdc84 100644 --- a/server/curfu/routers/nomenclature.py +++ b/server/curfu/routers/nomenclature.py @@ -18,7 +18,7 @@ from pydantic import ValidationError from curfu import logger -from curfu.schemas import NomenclatureResponse, ResponseDict +from curfu.schemas import NomenclatureResponse, ResponseDict, RouteTag router = APIRouter() @@ -28,6 +28,7 @@ operation_id="regulatoryElementNomenclature", response_model=NomenclatureResponse, response_model_exclude_none=True, + tags=[RouteTag.NOMENCLATURE], ) def generate_regulatory_element_nomenclature( request: Request, regulatory_element: Dict = Body() @@ -69,6 +70,7 @@ def generate_regulatory_element_nomenclature( operation_id="txSegmentNomenclature", response_model=NomenclatureResponse, response_model_exclude_none=True, + tags=[RouteTag.NOMENCLATURE], ) def generate_tx_segment_nomenclature(tx_segment: Dict = Body()) -> ResponseDict: """Build transcript segment element nomenclature. @@ -96,6 +98,7 @@ def generate_tx_segment_nomenclature(tx_segment: Dict = Body()) -> ResponseDict: operation_id="templatedSequenceNomenclature", response_model=NomenclatureResponse, response_model_exclude_none=True, + tags=[RouteTag.NOMENCLATURE], ) def generate_templated_seq_nomenclature( request: Request, templated_sequence: Dict = Body() @@ -136,6 +139,7 @@ def generate_templated_seq_nomenclature( operation_id="geneNomenclature", response_model=NomenclatureResponse, response_model_exclude_none=True, + tags=[RouteTag.NOMENCLATURE], ) def generate_gene_nomenclature(gene_element: Dict = Body()) -> ResponseDict: """Build gene element nomenclature. @@ -170,6 +174,7 @@ def generate_gene_nomenclature(gene_element: Dict = Body()) -> ResponseDict: operation_id="fusionNomenclature", response_model=NomenclatureResponse, response_model_exclude_none=True, + tags=[RouteTag.NOMENCLATURE], ) def generate_fusion_nomenclature( request: Request, fusion: Dict = Body() diff --git a/server/curfu/routers/utilities.py b/server/curfu/routers/utilities.py index 4ffed21a..2ce7799c 100644 --- a/server/curfu/routers/utilities.py +++ b/server/curfu/routers/utilities.py @@ -13,6 +13,7 @@ from curfu.schemas import ( CoordsUtilsResponse, GetTranscriptsResponse, + RouteTag, SequenceIDResponse, ) from curfu.sequence_services import InvalidInputError, get_strand @@ -25,6 +26,7 @@ operation_id="getMANETranscripts", response_model=GetTranscriptsResponse, response_model_exclude_none=True, + tags=[RouteTag.UTILITIES], ) def get_mane_transcripts(request: Request, term: str) -> Dict: """Get MANE transcripts for gene term. @@ -54,6 +56,7 @@ def get_mane_transcripts(request: Request, term: str) -> Dict: operation_id="getGenomicCoords", response_model=CoordsUtilsResponse, response_model_exclude_none=True, + tags=[RouteTag.UTILITIES], ) async def get_genome_coords( request: Request, @@ -126,6 +129,7 @@ async def get_genome_coords( operation_id="getExonCoords", response_model=CoordsUtilsResponse, response_model_exclude_none=True, + tags=[RouteTag.UTILITIES], ) async def get_exon_coords( request: Request, @@ -185,6 +189,7 @@ async def get_exon_coords( operation_id="getSequenceId", response_model=SequenceIDResponse, response_model_exclude_none=True, + tags=[RouteTag.UTILITIES], ) async def get_sequence_id(request: Request, sequence: str) -> SequenceIDResponse: """Get GA4GH sequence ID and aliases given sequence sequence ID @@ -234,6 +239,7 @@ async def get_sequence_id(request: Request, sequence: str) -> SequenceIDResponse description="Given a known accession identifier, retrieve sequence data and return" "as a FASTA file", response_class=FileResponse, + tags=[RouteTag.UTILITIES], ) async def get_sequence( request: Request, diff --git a/server/curfu/routers/validate.py b/server/curfu/routers/validate.py index 5150c315..a3fc0371 100644 --- a/server/curfu/routers/validate.py +++ b/server/curfu/routers/validate.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Body, Request from fusor.exceptions import FUSORParametersException -from curfu.schemas import ResponseDict, ValidateFusionResponse +from curfu.schemas import ResponseDict, RouteTag, ValidateFusionResponse router = APIRouter() @@ -14,6 +14,7 @@ operation_id="validateFusion", response_model=ValidateFusionResponse, response_model_exclude_none=True, + tags=[RouteTag.VALIDATORS], ) def validate_fusion(request: Request, fusion: Dict = Body()) -> ResponseDict: """Validate proposed Fusion object. Return warnings if invalid. diff --git a/server/curfu/schemas.py b/server/curfu/schemas.py index 41ca4bd4..9cd71c61 100644 --- a/server/curfu/schemas.py +++ b/server/curfu/schemas.py @@ -1,4 +1,5 @@ """Provide schemas for FastAPI responses.""" +from enum import Enum from typing import Dict, List, Literal, Optional, Tuple, Union from cool_seq_tool.schemas import GenomicData @@ -317,3 +318,17 @@ class DemoResponse(Response): """Response model for demo fusion object retrieval endpoints.""" fusion: Union[ClientAssayedFusion, ClientCategoricalFusion] + + +class RouteTag(str, Enum): + """Define tags for API routes.""" + + UTILITIES = "Utilities" + CONSTRUCTORS = "Constructors" + VALIDATORS = "Validators" + COMPLETION = "Completion" + NOMENCLATURE = "Nomenclature" + DEMOS = "Demos" + META = "Meta" + SERVICE = "Service" + LOOKUP = "Lookup" From f8fcfa0dbd51d099b3ba2adb3921eb21caa67162 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 15 Jul 2024 09:02:52 -0400 Subject: [PATCH 11/14] cicd: update github workflows/templates (#284) --- .github/ISSUE_TEMPLATE/bug-report.yaml | 85 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature-request.yaml | 60 +++++++++++++++ .github/workflows/backend_checks.yml | 6 +- .github/workflows/pr-priority-label.yaml | 23 ++++++ .github/workflows/stale.yaml | 27 +++++++ 5 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yaml create mode 100644 .github/workflows/pr-priority-label.yaml create mode 100644 .github/workflows/stale.yaml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml new file mode 100644 index 00000000..5b3f7c8d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -0,0 +1,85 @@ +name: Bug Report +description: Report a bug. +labels: ["bug"] +body: + - type: textarea + attributes: + label: Describe the bug + description: Provide a clear and concise description of what the bug is. + validations: + required: true + - type: textarea + attributes: + label: Steps to reproduce + description: Provide detailed steps to replicate the bug. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: true + - type: textarea + attributes: + label: Expected behavior + description: What did you expect to happen? + validations: + required: true + - type: textarea + attributes: + label: Current behavior + description: | + What actually happened? + + Include full errors, stack traces, and/or relevant logs. + validations: + required: true + - type: textarea + attributes: + label: Possible reason(s) + description: Provide any insights into what might be causing the issue. + validations: + required: false + - type: textarea + attributes: + label: Suggested fix + description: Provide any suggestions on how to resolve the bug. + validations: + required: false + - type: textarea + attributes: + label: Branch, commit, and/or version + description: Provide the branch, commit, and/or version you're using. + placeholder: | + branch: issue-1 + commit: abc123d + validations: + required: true + - type: textarea + attributes: + label: Screenshots + description: If applicable, add screenshots with descriptions to help explain your problem. + validations: + required: false + - type: textarea + attributes: + label: Environment details + description: Provide environment details (OS name and version, etc). + validations: + required: true + - type: textarea + attributes: + label: Additional details + description: Provide any other additional details about the problem. + validations: + required: false + - type: dropdown + attributes: + label: Contribution + description: Can you contribute to the development of this feature? + options: + - "Yes, I can create a PR for this fix." + - "Yes, but I can only provide ideas and feedback." + - "No, I cannot contribute." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature-request.yaml b/.github/ISSUE_TEMPLATE/feature-request.yaml new file mode 100644 index 00000000..2e1ae644 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yaml @@ -0,0 +1,60 @@ +name: Feature Request +description: Suggest an idea for this project. +labels: ["enhancement"] +body: + - type: textarea + attributes: + label: Feature description + description: Provide a clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + attributes: + label: Use case + description: | + Why do you need this feature? For example: "I'm always frustrated when..." + validations: + required: true + - type: textarea + attributes: + label: Proposed solution + description: Provide proposed solution. + validations: + required: false + - type: textarea + attributes: + label: Alternatives considered + description: Describe any alternative solutions you've considered. + validations: + required: false + - type: textarea + attributes: + label: Implementation details + description: Provide any technical details on how the feature might be implemented. + validations: + required: false + - type: textarea + attributes: + label: Potential Impact + description: | + Discuss any potential impacts of this feature on existing functionality or performance, if known. + Will this feature cause breaking changes? + What challenges might arise? + validations: + required: false + - type: textarea + attributes: + label: Additional context + description: Provide any other context or screenshots about the feature. + validations: + required: false + - type: dropdown + attributes: + label: Contribution + description: Can you contribute to the development of this feature? + options: + - "Yes, I can create a PR for this feature." + - "Yes, but I can only provide ideas and feedback." + - "No, I cannot contribute." + validations: + required: false diff --git a/.github/workflows/backend_checks.yml b/.github/workflows/backend_checks.yml index dec028d0..6f243f49 100644 --- a/.github/workflows/backend_checks.yml +++ b/.github/workflows/backend_checks.yml @@ -7,8 +7,8 @@ jobs: strategy: fail-fast: false steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: 3.11 @@ -18,7 +18,7 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: black uses: psf/black@stable diff --git a/.github/workflows/pr-priority-label.yaml b/.github/workflows/pr-priority-label.yaml new file mode 100644 index 00000000..0bef462d --- /dev/null +++ b/.github/workflows/pr-priority-label.yaml @@ -0,0 +1,23 @@ +name: Pull Request Has Priority Label +on: + pull_request: + types: [opened, labeled, unlabeled, synchronize] +jobs: + pr-priority-label: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + outputs: + status: ${{ steps.check-labels.outputs.status }} + steps: + - id: check-labels + uses: mheap/github-action-required-labels@v5 + with: + mode: exactly + count: 1 + labels: "priority:*" + use_regex: true + add_comment: true + message: "PRs require a priority label. Please add one." + exit_type: failure diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 00000000..6a7b1339 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,27 @@ +name: "Stalebot for issues and PRs" + +on: + schedule: + - cron: "30 13 * * 1-5" + +jobs: + stale-high-priority: + uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml + with: + days-before-issue-stale: 90 + days-before-pr-stale: 1 + labels: priority:high + + stale-medium-priority: + uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml + with: + days-before-issue-stale: 135 + days-before-pr-stale: 3 + labels: priority:medium + + stale-low-priority: + uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml + with: + days-before-issue-stale: 180 + days-before-pr-stale: 7 + labels: priority:low From 2548408285248afcc39fcfecab4e5969edfe28b5 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Mon, 15 Jul 2024 09:03:04 -0400 Subject: [PATCH 12/14] cicd: update precommit (#286) --- .pre-commit-config.yaml | 6 ++++-- server/setup.cfg | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c1a48b23..5ebc9143 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v1.4.0 + rev: v4.6.0 # pre-commit-hooks version hooks: - id: check-added-large-files - args: ["--maxkb=2000"] - id: detect-private-key - id: trailing-whitespace - id: end-of-file-fixer + - id: check-merge-conflict + - id: detect-aws-credentials - repo: https://github.com/psf/black rev: 23.7.0 hooks: @@ -34,3 +35,4 @@ repos: rev: "v2.7.1" hooks: - id: prettier +minimum_pre_commit_version: 3.7.1 diff --git a/server/setup.cfg b/server/setup.cfg index cb825e63..f5a8b3ad 100644 --- a/server/setup.cfg +++ b/server/setup.cfg @@ -35,7 +35,7 @@ dev = psycopg2-binary ruff black - pre-commit + pre-commit >= 3.7.1 gene-normalizer ~= 0.1.39 pydantic-to-typescript From cb83cb339fcba59feb8383da34a25eeaf4874b37 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Tue, 16 Jul 2024 08:56:18 -0400 Subject: [PATCH 13/14] build!: define in pyproject.toml (#285) * use `src/` layout * define build configs in `pyproject.toml` using `setuptools-scm` rather than `version.py` * update some associated metadata --- .gitignore | 8 +-- LICENSE | 2 +- Procfile | 2 +- server/curfu/version.py | 2 - server/pyproject.toml | 71 ++++++++++++++++++- server/setup.cfg | 50 ------------- server/setup.py | 5 -- server/{ => src}/curfu/__init__.py | 8 ++- server/{ => src}/curfu/cli.py | 0 server/{ => src}/curfu/devtools/__init__.py | 0 .../curfu/devtools/build_client_types.py | 0 .../curfu/devtools/build_gene_suggest.py | 0 .../curfu/devtools/build_interpro.py | 0 server/{ => src}/curfu/domain_services.py | 0 server/{ => src}/curfu/gene_services.py | 0 server/{ => src}/curfu/main.py | 2 +- server/{ => src}/curfu/routers/__init__.py | 0 server/{ => src}/curfu/routers/complete.py | 0 .../{ => src}/curfu/routers/constructors.py | 0 server/{ => src}/curfu/routers/demo.py | 0 server/{ => src}/curfu/routers/lookup.py | 0 server/{ => src}/curfu/routers/meta.py | 2 +- .../{ => src}/curfu/routers/nomenclature.py | 0 server/{ => src}/curfu/routers/utilities.py | 0 server/{ => src}/curfu/routers/validate.py | 0 server/{ => src}/curfu/schemas.py | 0 server/{ => src}/curfu/sequence_services.py | 0 server/{ => src}/curfu/utils.py | 0 28 files changed, 84 insertions(+), 68 deletions(-) delete mode 100644 server/curfu/version.py delete mode 100644 server/setup.cfg delete mode 100644 server/setup.py rename server/{ => src}/curfu/__init__.py (88%) rename server/{ => src}/curfu/cli.py (100%) rename server/{ => src}/curfu/devtools/__init__.py (100%) rename server/{ => src}/curfu/devtools/build_client_types.py (100%) rename server/{ => src}/curfu/devtools/build_gene_suggest.py (100%) rename server/{ => src}/curfu/devtools/build_interpro.py (100%) rename server/{ => src}/curfu/domain_services.py (100%) rename server/{ => src}/curfu/gene_services.py (100%) rename server/{ => src}/curfu/main.py (98%) rename server/{ => src}/curfu/routers/__init__.py (100%) rename server/{ => src}/curfu/routers/complete.py (100%) rename server/{ => src}/curfu/routers/constructors.py (100%) rename server/{ => src}/curfu/routers/demo.py (100%) rename server/{ => src}/curfu/routers/lookup.py (100%) rename server/{ => src}/curfu/routers/meta.py (93%) rename server/{ => src}/curfu/routers/nomenclature.py (100%) rename server/{ => src}/curfu/routers/utilities.py (100%) rename server/{ => src}/curfu/routers/validate.py (100%) rename server/{ => src}/curfu/schemas.py (100%) rename server/{ => src}/curfu/sequence_services.py (100%) rename server/{ => src}/curfu/utils.py (100%) diff --git a/.gitignore b/.gitignore index 9af1cf3d..d25a2f73 100644 --- a/.gitignore +++ b/.gitignore @@ -91,7 +91,7 @@ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: -# .python-version +**/.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -176,8 +176,8 @@ yarn-debug.log* yarn-error.log* frontend/public/ frontend/node_modules -node_modules/ +**/node_modules/ +**/.yarn # Data -server/curfu/data -client/node_modules +server/src/curfu/data diff --git a/LICENSE b/LICENSE index 859f3c0e..8cb143ef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2022 Alex H. Wagner +Copyright (c) 2021-2024 Genomic Medicine Lab Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Procfile b/Procfile index d2d9113c..1687f997 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: sh -c 'cd ./server/ && gunicorn -k uvicorn.workers.UvicornWorker curfu.main:app --timeout 1000 --log-level debug' +web: sh -c 'cd ./server/src/ && gunicorn -k uvicorn.workers.UvicornWorker curfu.main:app --timeout 1000 --log-level debug' diff --git a/server/curfu/version.py b/server/curfu/version.py deleted file mode 100644 index b53ed92c..00000000 --- a/server/curfu/version.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Provide app version.""" -__version__ = "0.2.0-rc.3" diff --git a/server/pyproject.toml b/server/pyproject.toml index e50d642d..c9456bda 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -1,6 +1,73 @@ +[project] +name = "curfu" +authors = [ + {name = "Alex Wagner", email = "alex.wagner@nationwidechildrens.org"}, + {name = "Kori Kuzma", email = "kori.kuzma@nationwidechildrens.org"}, + {name = "James Stevenson", email = "james.stevenson@nationwidechildrens.org"}, + {name = "Katie Stahl", email = "kathryn.stahl@nationwidechildrens.org"}, + {name = "Jeremy Arbesfeld", email = "jeremy.arbesfeld@nationwidechildrens.org"} +] +readme = "README.md" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.10" +description = "Curation tool for gene fusions" +license = {file = "../LICENSE"} +dependencies = [ + "fastapi >= 0.72.0", + "aiofiles", + "asyncpg", + "fusor ~= 0.0.30-dev1", + "sqlparse >= 0.4.2", + "urllib3 >= 1.26.5", + "click", + "jinja2", + "boto3", +] +dynamic = ["version"] + +[project.optional-dependencies] +tests = [ + "pytest", + "pytest-asyncio >= 0.19.0", + "pytest-cov", + "coverage", + "httpx", +] +dev = [ + "psycopg2-binary", + "ruff", + "black", + "pre-commit>=3.7.1", + "gene-normalizer ~= 0.1.39", + "pydantic-to-typescript", +] + +[project.scripts] +curfu_devtools = "curfu.cli:devtools" +curfu = "curfu.cli:serve" + [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta:__legacy__" +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools_scm] + +[tool.pytest.ini_options] +addopts = "--cov=src --cov-report term-missing" +testpaths = ["tests"] [tool.black] line-length = 88 diff --git a/server/setup.cfg b/server/setup.cfg deleted file mode 100644 index f5a8b3ad..00000000 --- a/server/setup.cfg +++ /dev/null @@ -1,50 +0,0 @@ -[metadata] -name = curfu -description = Curation tool for gene fusions -long_description = file:README.md -long_description_content_type = text/markdown -author = Wagner Lab, Nationwide Childrens Hospital -license = MIT - -[options] -packages = find: -python_requires = >=3.8 -zip_safe = False -include_package_data = True - -install_requires = - fastapi >= 0.72.0 - aiofiles - asyncpg - fusor ~= 0.0.30-dev1 - sqlparse >= 0.4.2 - urllib3 >= 1.26.5 - click - jinja2 - boto3 - -[options.extras_require] -tests = - pytest - pytest-asyncio >= 0.19.0 - pytest-cov - coverage - httpx - -dev = - psycopg2-binary - ruff - black - pre-commit >= 3.7.1 - gene-normalizer ~= 0.1.39 - pydantic-to-typescript - - -[options.entry_points] -console_scripts = - curfu_devtools = curfu.cli:devtools - curfu = curfu.cli:serve - -[tool:pytest] -addopts = --disable-warnings --cov-report term-missing --cov-config=.coveragerc --cov=curfu -asyncio_mode = auto diff --git a/server/setup.py b/server/setup.py deleted file mode 100644 index a3fd140d..00000000 --- a/server/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Defines how metakb is packaged and distributed.""" -from setuptools import setup - -exec(open("curfu/version.py").read()) -setup(version=__version__) # noqa: F821 diff --git a/server/curfu/__init__.py b/server/src/curfu/__init__.py similarity index 88% rename from server/curfu/__init__.py rename to server/src/curfu/__init__.py index 235e9429..405d6d56 100644 --- a/server/curfu/__init__.py +++ b/server/src/curfu/__init__.py @@ -1,9 +1,15 @@ """Fusion curation interface.""" import logging +from importlib.metadata import PackageNotFoundError, version from os import environ from pathlib import Path -from .version import __version__ +try: + __version__ = version("curfu") +except PackageNotFoundError: + __version__ = "unknown" +finally: + del version, PackageNotFoundError # provide consistent paths APP_ROOT = Path(__file__).resolve().parents[0] diff --git a/server/curfu/cli.py b/server/src/curfu/cli.py similarity index 100% rename from server/curfu/cli.py rename to server/src/curfu/cli.py diff --git a/server/curfu/devtools/__init__.py b/server/src/curfu/devtools/__init__.py similarity index 100% rename from server/curfu/devtools/__init__.py rename to server/src/curfu/devtools/__init__.py diff --git a/server/curfu/devtools/build_client_types.py b/server/src/curfu/devtools/build_client_types.py similarity index 100% rename from server/curfu/devtools/build_client_types.py rename to server/src/curfu/devtools/build_client_types.py diff --git a/server/curfu/devtools/build_gene_suggest.py b/server/src/curfu/devtools/build_gene_suggest.py similarity index 100% rename from server/curfu/devtools/build_gene_suggest.py rename to server/src/curfu/devtools/build_gene_suggest.py diff --git a/server/curfu/devtools/build_interpro.py b/server/src/curfu/devtools/build_interpro.py similarity index 100% rename from server/curfu/devtools/build_interpro.py rename to server/src/curfu/devtools/build_interpro.py diff --git a/server/curfu/domain_services.py b/server/src/curfu/domain_services.py similarity index 100% rename from server/curfu/domain_services.py rename to server/src/curfu/domain_services.py diff --git a/server/curfu/gene_services.py b/server/src/curfu/gene_services.py similarity index 100% rename from server/curfu/gene_services.py rename to server/src/curfu/gene_services.py diff --git a/server/curfu/main.py b/server/src/curfu/main.py similarity index 98% rename from server/curfu/main.py rename to server/src/curfu/main.py index d4df661e..a14cb86b 100644 --- a/server/curfu/main.py +++ b/server/src/curfu/main.py @@ -7,6 +7,7 @@ from starlette.templating import _TemplateResponse as TemplateResponse from curfu import APP_ROOT +from curfu import __version__ as curfu_version from curfu.domain_services import DomainService from curfu.gene_services import GeneService from curfu.routers import ( @@ -19,7 +20,6 @@ utilities, validate, ) -from curfu.version import __version__ as curfu_version fastapi_app = FastAPI( title="Fusion Curation API", diff --git a/server/curfu/routers/__init__.py b/server/src/curfu/routers/__init__.py similarity index 100% rename from server/curfu/routers/__init__.py rename to server/src/curfu/routers/__init__.py diff --git a/server/curfu/routers/complete.py b/server/src/curfu/routers/complete.py similarity index 100% rename from server/curfu/routers/complete.py rename to server/src/curfu/routers/complete.py diff --git a/server/curfu/routers/constructors.py b/server/src/curfu/routers/constructors.py similarity index 100% rename from server/curfu/routers/constructors.py rename to server/src/curfu/routers/constructors.py diff --git a/server/curfu/routers/demo.py b/server/src/curfu/routers/demo.py similarity index 100% rename from server/curfu/routers/demo.py rename to server/src/curfu/routers/demo.py diff --git a/server/curfu/routers/lookup.py b/server/src/curfu/routers/lookup.py similarity index 100% rename from server/curfu/routers/lookup.py rename to server/src/curfu/routers/lookup.py diff --git a/server/curfu/routers/meta.py b/server/src/curfu/routers/meta.py similarity index 93% rename from server/curfu/routers/meta.py rename to server/src/curfu/routers/meta.py index 2032de94..85211baf 100644 --- a/server/curfu/routers/meta.py +++ b/server/src/curfu/routers/meta.py @@ -3,8 +3,8 @@ from fastapi import APIRouter from fusor import __version__ as fusor_version +from curfu import __version__ as curfu_version from curfu.schemas import RouteTag, ServiceInfoResponse -from curfu.version import __version__ as curfu_version router = APIRouter() diff --git a/server/curfu/routers/nomenclature.py b/server/src/curfu/routers/nomenclature.py similarity index 100% rename from server/curfu/routers/nomenclature.py rename to server/src/curfu/routers/nomenclature.py diff --git a/server/curfu/routers/utilities.py b/server/src/curfu/routers/utilities.py similarity index 100% rename from server/curfu/routers/utilities.py rename to server/src/curfu/routers/utilities.py diff --git a/server/curfu/routers/validate.py b/server/src/curfu/routers/validate.py similarity index 100% rename from server/curfu/routers/validate.py rename to server/src/curfu/routers/validate.py diff --git a/server/curfu/schemas.py b/server/src/curfu/schemas.py similarity index 100% rename from server/curfu/schemas.py rename to server/src/curfu/schemas.py diff --git a/server/curfu/sequence_services.py b/server/src/curfu/sequence_services.py similarity index 100% rename from server/curfu/sequence_services.py rename to server/src/curfu/sequence_services.py diff --git a/server/curfu/utils.py b/server/src/curfu/utils.py similarity index 100% rename from server/curfu/utils.py rename to server/src/curfu/utils.py From c0a447814bd56f6d95af8c81cda8e8618beaadc0 Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Tue, 16 Jul 2024 09:01:57 -0400 Subject: [PATCH 14/14] cicd: fix stalebot (#287) --- .github/workflows/stale.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 6a7b1339..66bb8613 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -6,21 +6,21 @@ on: jobs: stale-high-priority: - uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml + uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml@main with: days-before-issue-stale: 90 days-before-pr-stale: 1 labels: priority:high stale-medium-priority: - uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml + uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml@main with: days-before-issue-stale: 135 days-before-pr-stale: 3 labels: priority:medium stale-low-priority: - uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml + uses: genomicmedlab/software-templates/.github/workflows/reusable-stale.yaml@main with: days-before-issue-stale: 180 days-before-pr-stale: 7